Skip to content

Commit

Permalink
Expose Serialize(StringBuilder) overload (#82)
Browse files Browse the repository at this point in the history
Co-authored-by: Rolf Kristensen <sweaty1@hotmail.com>
  • Loading branch information
Mpdreamz and snakefoot authored Jun 3, 2020
1 parent ad3187e commit 267e324
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 69 deletions.
43 changes: 22 additions & 21 deletions src/Elastic.CommonSchema.NLog/EcsLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,42 +117,43 @@ public EcsLayout()
[ArrayParameter(typeof(TargetPropertyWithContext), "tag")]
public IList<TargetPropertyWithContext> Tags { get; } = new List<TargetPropertyWithContext>();

protected override void RenderFormattedMessage(LogEventInfo logEventInfo, StringBuilder target)
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
{
var ecsEvent = new Base
{
Timestamp = logEventInfo.TimeStamp,
Message = logEventInfo.FormattedMessage,
Timestamp = logEvent.TimeStamp,
Message = logEvent.FormattedMessage,
Ecs = new Ecs { Version = Base.Version },
Log = GetLog(logEventInfo),
Event = GetEvent(logEventInfo),
Metadata = GetMetadata(logEventInfo),
Process = GetProcess(logEventInfo),
Trace = GetTrace(logEventInfo),
Transaction = GetTransaction(logEventInfo),
Error = GetError(logEventInfo.Exception),
Tags = GetTags(logEventInfo),
Labels = GetLabels(logEventInfo),
Agent = GetAgent(logEventInfo),
Server = GetServer(logEventInfo),
Host = GetHost(logEventInfo)
Log = GetLog(logEvent),
Event = GetEvent(logEvent),
Metadata = GetMetadata(logEvent),
Process = GetProcess(logEvent),
Trace = GetTrace(logEvent),
Transaction = GetTransaction(logEvent),
Error = GetError(logEvent.Exception),
Tags = GetTags(logEvent),
Labels = GetLabels(logEvent),
Agent = GetAgent(logEvent),
Server = GetServer(logEvent),
Host = GetHost(logEvent)
};

//Give any deriving classes a chance to enrich the event
EnrichEvent(logEventInfo,ref ecsEvent);
EnrichEvent(logEvent, ref ecsEvent);
//Allow programmatical actions to enrich before serializing
EnrichAction?.Invoke(ecsEvent, logEventInfo);
var output = ecsEvent.Serialize();
target.Append(output);
EnrichAction?.Invoke(ecsEvent, logEvent);

ecsEvent.Serialize(target);
}

/// <summary>
/// Override to supplement the ECS event parsing
/// </summary>
/// <param name="logEventInfo">The original log event</param>
/// <param name="logEvent">The original log event</param>
/// <param name="ecsEvent">The EcsEvent to modify</param>
/// <returns>Enriched ECS Event</returns>
/// <remarks>Destructive for performance</remarks>
protected virtual void EnrichEvent(LogEventInfo logEventInfo,ref Base ecsEvent)
protected virtual void EnrichEvent(LogEventInfo logEvent, ref Base ecsEvent)
{
}

Expand Down
55 changes: 14 additions & 41 deletions src/Elastic.CommonSchema/Base.Serialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,15 @@
// See the LICENSE file in the project root for more information

using System;
using System.Buffers;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Elastic.CommonSchema.Serialization;
using static Elastic.CommonSchema.Serialization.JsonConfiguration;

namespace Elastic.CommonSchema.Serialization
{
/// <summary>
/// This static class allows you to deserialize subclasses of <see cref="Base"/>
/// If you are dealing with <see cref="Base"/> directly you do not need to use this class,
/// use <see cref="Base.Deserialize(string)"/> and the overloads instead.
/// </summary>
/// <remarks>
/// This class should only be used for advanced use cases, for simpler use cases you can utilise the <see cref="Base.Metadata"/> property.
/// </remarks>
/// <typeparam name="TBase">Type of the <see cref="Base"/> subclass</typeparam>
public static class EcsSerializerFactory<TBase> where TBase : Base, new()
{
public static ValueTask<TBase> DeserializeAsync(Stream stream, CancellationToken ctx = default) =>
JsonSerializer.DeserializeAsync<TBase>(stream, SerializerOptions, ctx);

public static TBase Deserialize(string json) => JsonSerializer.Deserialize<TBase>(json, SerializerOptions);

public static TBase Deserialize(ReadOnlySpan<byte> json) => JsonSerializer.Deserialize<TBase>(json, SerializerOptions);

public static TBase Deserialize(Stream stream)
{
using var ms = new MemoryStream();
var buffer = ArrayPool<byte>.Shared.Rent(1024);
var total = 0;
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
total += read;
}
var span = ms.TryGetBuffer(out var segment)
? new ReadOnlyMemory<byte>(segment.Array, segment.Offset, total).Span
: new ReadOnlyMemory<byte>(ms.ToArray()).Span;
return Deserialize(span);
}
}
}

namespace Elastic.CommonSchema
{
public partial class Base
Expand Down Expand Up @@ -96,16 +57,28 @@ public static ValueTask<Base> DeserializeAsync(Stream stream, CancellationToken

public byte[] SerializeToUtf8Bytes() => JsonSerializer.SerializeToUtf8Bytes(this, GetType(), SerializerOptions);

private static ReusableUtf8JsonWriter _reusableJsonWriter;
private static ReusableUtf8JsonWriter ReusableJsonWriter => _reusableJsonWriter ??= new ReusableUtf8JsonWriter();

public StringBuilder Serialize(StringBuilder stringBuilder)
{
using var reusableWriter = ReusableJsonWriter.AllocateJsonWriter(stringBuilder);
reusableWriter.Serialize(this);
return stringBuilder;
}

public void Serialize(Stream stream)
{
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Indented = false
});
JsonSerializer.Serialize(writer, this, JsonConfiguration.SerializerOptions);
JsonSerializer.Serialize(writer, this, SerializerOptions);
}

internal void Serialize(Utf8JsonWriter writer) => JsonSerializer.Serialize(writer, this, SerializerOptions);

public Task SerializeAsync(Stream stream, CancellationToken ctx = default) =>
JsonSerializer.SerializeAsync(stream, this, GetType(), SerializerOptions, ctx);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.CommonSchema/Elastic.CommonSchema.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), build.bat))\src\PublishArtifacts.build.props"/>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), build.bat))\src\PublishArtifacts.build.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net461</TargetFrameworks>
<Title>Elastic Common Schema (ECS) Types</Title>
Expand Down
45 changes: 45 additions & 0 deletions src/Elastic.CommonSchema/Serialization/EcsSerializerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Buffers;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace Elastic.CommonSchema.Serialization
{
/// <summary>
/// This static class allows you to deserialize subclasses of <see cref="Base"/>
/// If you are dealing with <see cref="Base"/> directly you do not need to use this class,
/// use <see cref="Base.Deserialize(string)"/> and the overloads instead.
/// </summary>
/// <remarks>
/// This class should only be used for advanced use cases, for simpler use cases you can utilise the <see cref="Base.Metadata"/> property.
/// </remarks>
/// <typeparam name="TBase">Type of the <see cref="Base"/> subclass</typeparam>
public static class EcsSerializerFactory<TBase> where TBase : Base, new()
{
public static ValueTask<TBase> DeserializeAsync(Stream stream, CancellationToken ctx = default) =>
JsonSerializer.DeserializeAsync<TBase>(stream, JsonConfiguration.SerializerOptions, ctx);

public static TBase Deserialize(string json) => JsonSerializer.Deserialize<TBase>(json, JsonConfiguration.SerializerOptions);

public static TBase Deserialize(ReadOnlySpan<byte> json) => JsonSerializer.Deserialize<TBase>(json, JsonConfiguration.SerializerOptions);

public static TBase Deserialize(Stream stream)
{
using var ms = new MemoryStream();
var buffer = ArrayPool<byte>.Shared.Rent(1024);
var total = 0;
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
total += read;
}
var span = ms.TryGetBuffer(out var segment)
? new ReadOnlyMemory<byte>(segment.Array, segment.Offset, total).Span
: new ReadOnlyMemory<byte>(ms.ToArray()).Span;
return Deserialize(span);
}
}
}
89 changes: 89 additions & 0 deletions src/Elastic.CommonSchema/Serialization/ReusableUtf8JsonWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System;
using System.IO;
using System.Text;
using System.Text.Json;

namespace Elastic.CommonSchema.Serialization
{
internal sealed class ReusableUtf8JsonWriter
{
private Utf8JsonWriter _cachedJsonWriter;
private readonly MemoryStream _cachedMemoryStream;
private readonly char[] _cachedEncodingBuffer;

public ReusableUtf8JsonWriter()
{
_cachedMemoryStream = new MemoryStream(4 * 1024);
_cachedJsonWriter = new Utf8JsonWriter(_cachedMemoryStream);
_cachedEncodingBuffer = new char[1024];
}

public ReusableJsonWriter AllocateJsonWriter(StringBuilder text)
{
var writer = System.Threading.Interlocked.Exchange(ref _cachedJsonWriter, null);
return new ReusableJsonWriter(this, writer, text);
}

public ReusableJsonWriter NewJsonWriter(StringBuilder text) =>
new ReusableJsonWriter(this, new Utf8JsonWriter(new MemoryStream()), text);

private void Return(Utf8JsonWriter writer, StringBuilder output)
{
writer.Flush();

if (_cachedMemoryStream.Length > 0)
{
if (!_cachedMemoryStream.TryGetBuffer(out var byteArray))
byteArray = new ArraySegment<byte>(_cachedMemoryStream.GetBuffer(), 0, (int)_cachedMemoryStream.Length);

CopyToStringBuilder(byteArray, _cachedEncodingBuffer, output);
}

writer.Reset();
_cachedMemoryStream.Position = 0;
_cachedMemoryStream.SetLength(0);
System.Threading.Interlocked.Exchange(ref _cachedJsonWriter, writer);
}

private static void CopyToStringBuilder(ArraySegment<byte> byteArray, char[] encodingBuffer, StringBuilder output)
{
for (var i = 0; i < byteArray.Count; i += encodingBuffer.Length)
{
var byteCount = Math.Min(byteArray.Count - i, encodingBuffer.Length);
var charCount = Encoding.UTF8.GetChars(byteArray.Array, byteArray.Offset + i, byteCount, encodingBuffer, 0);
output.Append(encodingBuffer, 0, charCount);
}
}

internal readonly struct ReusableJsonWriter : IDisposable
{
private readonly ReusableUtf8JsonWriter _owner;
private readonly Utf8JsonWriter _writer;
private readonly StringBuilder _output;

public ReusableJsonWriter(ReusableUtf8JsonWriter owner, Utf8JsonWriter writer, StringBuilder output)
{
_writer = writer;
_owner = writer != null ? owner : null;
_output = output;
}

public void Serialize(Base ecsEvent)
{
if (_writer != null)
ecsEvent.Serialize(_writer);
else
{
var result = ecsEvent.Serialize();
_output.Append(result);
}
}

public void Dispose() => _owner?.Return(_writer, _output);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), build.bat))\src\Library.build.props"/>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), build.bat))\src\Library.build.props" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoBogus" Version="2.8.2"/>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1"/>
<PackageReference Include="Bogus" Version="29.0.2"/>
<PackageReference Include="AutoBogus" Version="2.8.2" />
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Include="Bogus" Version="29.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Elastic.CommonSchema\Elastic.CommonSchema.csproj"/>
<ProjectReference Include="..\..\src\Elastic.CommonSchema\Elastic.CommonSchema.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Text;
using AutoBogus;
using BenchmarkDotNet.Attributes;

namespace Elastic.CommonSchema.Benchmarks
{
[UnicodeConsoleLogger, MemoryDiagnoser, ThreadingDiagnoser]
public class SerializingStringBuilderBase
{
[Benchmark]
public StringBuilder Empty()
{
var ecs = new Base();
return ecs.Serialize(new StringBuilder());
}
[Benchmark]
public StringBuilder Minimal()
{
var ecs = new Base
{
Timestamp = DateTimeOffset.UtcNow,
Log = new Log
{
Level = "Debug"
},
Message = "hello world!"
};
return ecs.Serialize(new StringBuilder());
}
[Benchmark]
public StringBuilder Complex()
{
var ecs = new Base
{
Timestamp = DateTimeOffset.UtcNow,
Log = new Log
{
Level = "Debug", Logger = "Logger",
Origin = new LogOrigin
{
File = new OriginFile { Line = 12, Name = "file.cs"}, Function = "Complex"
},
Original = "new log line",
Syslog = new LogSyslog {
Facility = new SyslogFacility
{
Code = 12, Name = "syslog"
}, Priority = 12, Severity = new SyslogSeverity()
{
Code = 12, Name = "asd"
},
}
},
Message = "hello world!",
Agent = new Agent
{
Name = "test"
}

};
return ecs.Serialize(new StringBuilder());
}

public static readonly Base FullInstance = new AutoFaker<Base>().Generate();

[Benchmark]
public StringBuilder Full() => FullInstance.Serialize(new StringBuilder());
}
}
2 changes: 1 addition & 1 deletion tests/Elastic.CommonSchema.NLog.Tests/OutputTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void LogMultiple() => TestLogger((logger, getLogEvents) =>
{
logger.Info("My log message!");
logger.Info("Test output to NLog!");
Action sketchy = () => throw new Exception("I threw up.");
void sketchy() => throw new Exception("I threw up.");
var exception = Record.Exception(sketchy);
logger.Error(exception, "Here is an error.");
Assert.NotNull(exception);
Expand Down

0 comments on commit 267e324

Please sign in to comment.