Skip to content

Newtonsoft.Json -> System.Text.Json #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 59 additions & 83 deletions src/Serilog.Formatting.Compact.Reader/LogEventReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;
using System.Text.Json;
using Serilog.Events;
using Serilog.Parsing;

Expand All @@ -34,19 +34,16 @@ public class LogEventReader : IDisposable
static readonly MessageTemplateParser Parser = new();
static readonly Rendering[] NoRenderings = [];
readonly TextReader _text;
readonly JsonSerializer _serializer;

int _lineNumber;

/// <summary>
/// Construct a <see cref="LogEventReader"/>.
/// </summary>
/// <param name="text">Text to read from.</param>
/// <param name="serializer">If specified, a JSON serializer used when converting event documents.</param>
public LogEventReader(TextReader text, JsonSerializer? serializer = null)
public LogEventReader(TextReader text)
{
_text = text ?? throw new ArgumentNullException(nameof(text));
_serializer = serializer ?? CreateSerializer();
}

/// <inheritdoc/>
Expand Down Expand Up @@ -127,63 +124,58 @@ public bool TryRead([NotNullWhen(true)] out LogEvent? evt)
/// Read a single log event from a JSON-encoded document.
/// </summary>
/// <param name="document">The event in compact-JSON.</param>
/// <param name="serializer">If specified, a JSON serializer used when converting event documents.</param>
/// <returns>The log event.</returns>
/// <exception cref="InvalidDataException">The data format is invalid.</exception>
public static LogEvent ReadFromString(string document, JsonSerializer? serializer = null)
public static LogEvent ReadFromString(string document)
{
if (document == null) throw new ArgumentNullException(nameof(document));

serializer ??= CreateSerializer();
object? result;
JsonDocument? data = null;
try
{
using var reader = new JsonTextReader(new StringReader(document));
result = serializer.Deserialize(reader);
data = JsonDocument.Parse(document);
}
catch (Exception ex)
catch (JsonException ex)
{
throw new InvalidDataException("The document could not be deserialized.", ex);
}

if (result is not JObject jObject)
throw new InvalidDataException("The document is not a complete JSON object.");

return ReadFromJObject(jObject);
using (data)
{
if (data == null || data.RootElement.ValueKind != JsonValueKind.Object)
throw new InvalidDataException($"The document is not a complete JSON object.");
return ReadFromJObject(data.RootElement);
}
}

/// <summary>
/// Read a single log event from an already-deserialized JSON object.
/// </summary>
/// <param name="jObject">The deserialized compact-JSON event.</param>
/// <returns>The log event.</returns>
/// <exception cref="InvalidDataException">The data format is invalid.</exception>
public static LogEvent ReadFromJObject(JObject jObject)
public static LogEvent ReadFromJObject(in JsonElement jObject)
{
if (jObject == null) throw new ArgumentNullException(nameof(jObject));
if (jObject.ValueKind != JsonValueKind.Object) throw new ArgumentException(nameof(jObject));
return ReadFromJObject(1, jObject);
}

LogEvent ParseLine(string line)
{
object? data;
JsonDocument? data = null;
try
{
using var reader = new JsonTextReader(new StringReader(line));
data = _serializer.Deserialize(reader);
data = JsonDocument.Parse(line);
}
catch (Exception ex)
catch (JsonException)
{
throw new InvalidDataException($"The data on line {_lineNumber} could not be deserialized.", ex);
}

if (data is not JObject fields)
throw new InvalidDataException($"The data on line {_lineNumber} is not a complete JSON object.");

return ReadFromJObject(_lineNumber, fields);
using (data)
{
if (data == null || data.RootElement.ValueKind != JsonValueKind.Object)
throw new InvalidDataException($"The data on line {_lineNumber} is not a complete JSON object.");
return ReadFromJObject(_lineNumber, data.RootElement);
}
}

static LogEvent ReadFromJObject(int lineNumber, JObject jObject)
static LogEvent ReadFromJObject(int lineNumber, in JsonElement jObject)
{
var timestamp = GetRequiredTimestampField(lineNumber, jObject, ClefFields.Timestamp);

Expand All @@ -206,31 +198,31 @@ static LogEvent ReadFromJObject(int lineNumber, JObject jObject)
ActivityTraceId traceId = default;
if (TryGetOptionalField(lineNumber, jObject, ClefFields.TraceId, out var tr))
traceId = ActivityTraceId.CreateFromString(tr.AsSpan());

ActivitySpanId spanId = default;
if (TryGetOptionalField(lineNumber, jObject, ClefFields.SpanId, out var sp))
spanId = ActivitySpanId.CreateFromString(sp.AsSpan());

var parsedTemplate = messageTemplate == null ?
new MessageTemplate([]) :
Parser.Parse(messageTemplate);

var renderings = NoRenderings;

if (jObject.TryGetValue(ClefFields.Renderings, out var r))
if (jObject.TryGetProperty(ClefFields.Renderings, out var r))
{
if (r is not JArray renderedByIndex)
if (!(r.ValueKind == JsonValueKind.Array))
throw new InvalidDataException($"The `{ClefFields.Renderings}` value on line {lineNumber} is not an array as expected.");

renderings = parsedTemplate.Tokens
.OfType<PropertyToken>()
.Where(t => t.Format != null)
.Zip(renderedByIndex, (t, rd) => new Rendering(t.PropertyName, t.Format!, rd.Value<string>()!))
.Zip(r.EnumerateArray(), (t, rd) => new Rendering(t.PropertyName, t.Format!, rd.GetString()!))
.ToArray();
}

var properties = jObject
.Properties()
.EnumerateObject()
.Where(f => !ClefFields.All.Contains(f.Name))
.Select(f =>
{
Expand All @@ -248,75 +240,59 @@ static LogEvent ReadFromJObject(int lineNumber, JObject jObject)
return new LogEvent(timestamp, level, exception, parsedTemplate, properties, traceId, spanId);
}

static bool TryGetOptionalField(int lineNumber, JObject data, string field, [NotNullWhen(true)] out string? value)
static bool TryGetOptionalField(int lineNumber, in JsonElement data, string field, [NotNullWhen(true)] out string? value)
{
if (!data.TryGetValue(field, out var token) || token.Type == JTokenType.Null)
if (!data.TryGetProperty(field, out var prop) || prop.ValueKind == JsonValueKind.Null)
{
value = null;
return false;
}

if (token.Type != JTokenType.String)
if (prop.ValueKind != JsonValueKind.String)
throw new InvalidDataException($"The value of `{field}` on line {lineNumber} is not in a supported format.");

value = token.Value<string>()!;
value = prop.GetString()!;
return true;
}

static bool TryGetOptionalEventId(int lineNumber, JObject data, string field, out object? eventId)
static bool TryGetOptionalEventId(int lineNumber, in JsonElement data, string field, out object? eventId)
{
if (!data.TryGetValue(field, out var token) || token.Type == JTokenType.Null)
if (!data.TryGetProperty(field, out var prop) || prop.ValueKind == JsonValueKind.Null)
{
eventId = null;
return false;
}

switch (token.Type)
switch (prop.ValueKind)
{
case JTokenType.String:
eventId = token.Value<string>();
return true;
case JTokenType.Integer:
eventId = token.Value<uint>();
case JsonValueKind.String:
eventId = prop.GetString();
return true;
default:
throw new InvalidDataException(
$"The value of `{field}` on line {lineNumber} is not in a supported format.");
case JsonValueKind.Number:
if (prop.TryGetUInt32(out var v))
{
eventId = v;
return true;
}
break;
}

throw new InvalidDataException(
$"The value of `{field}` on line {lineNumber} is not in a supported format.");
}

static DateTimeOffset GetRequiredTimestampField(int lineNumber, JObject data, string field)
static DateTimeOffset GetRequiredTimestampField(int lineNumber, in JsonElement data, string field)
{
if (!data.TryGetValue(field, out var token) || token.Type == JTokenType.Null)
if (!data.TryGetProperty(field, out var prop) || prop.ValueKind == JsonValueKind.Null)
throw new InvalidDataException($"The data on line {lineNumber} does not include the required `{field}` field.");

if (token.Type == JTokenType.Date)
{
var dt = token.Value<JValue>()!.Value;
if (dt is DateTimeOffset offset)
return offset;

return (DateTime)dt!;
}
else
{
if (token.Type != JTokenType.String)
throw new InvalidDataException($"The value of `{field}` on line {lineNumber} is not in a supported format.");

var text = token.Value<string>()!;
if (!DateTimeOffset.TryParse(text, out var offset))
throw new InvalidDataException($"The value of `{field}` on line {lineNumber} is not in a supported timestamp format.");
if (prop.ValueKind != JsonValueKind.String)
throw new InvalidDataException($"The value of `{field}` on line {lineNumber} is not in a supported format.");

return offset;
}
}
var text = prop.GetString()!;
if (!DateTimeOffset.TryParse(text, out var offset))
throw new InvalidDataException($"The value of `{field}` on line {lineNumber} is not in a supported timestamp format.");

static JsonSerializer CreateSerializer()
{
return JsonSerializer.Create(new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None,
Culture = CultureInfo.InvariantCulture
});
return offset;
}
}
62 changes: 50 additions & 12 deletions src/Serilog.Formatting.Compact.Reader/PropertyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Newtonsoft.Json.Linq;
using System.Text.Json;
using Serilog.Events;

namespace Serilog.Formatting.Compact.Reader;
Expand All @@ -22,7 +22,7 @@ static class PropertyFactory
const string TypeTagPropertyName = "$type";
const string InvalidPropertyNameSubstitute = "(unnamed)";

public static LogEventProperty CreateProperty(string name, JToken value, Rendering[]? renderings)
public static LogEventProperty CreateProperty(string name, in JsonElement value, Rendering[]? renderings)
{
// The format allows (does not disallow) empty/null property names, but Serilog cannot represent them.
if (!LogEventProperty.IsValidName(name))
Expand All @@ -31,27 +31,65 @@ public static LogEventProperty CreateProperty(string name, JToken value, Renderi
return new LogEventProperty(name, CreatePropertyValue(value, renderings));
}

static LogEventPropertyValue CreatePropertyValue(JToken value, Rendering[]? renderings)
static LogEventPropertyValue CreatePropertyValue(in JsonElement value, Rendering[]? renderings)
{
if (value.Type == JTokenType.Null)
if (value.ValueKind == JsonValueKind.Null)
return new ScalarValue(null);

if (value is JObject obj)
if (value.ValueKind == JsonValueKind.Object)
{
obj.TryGetValue(TypeTagPropertyName, out var tt);
string? tts = null;
if (value.TryGetProperty(TypeTagPropertyName, out var tt))
tts = tt.GetString();
return new StructureValue(
obj.Properties().Where(kvp => kvp.Name != TypeTagPropertyName).Select(kvp => CreateProperty(kvp.Name, kvp.Value, null)),
tt?.Value<string>());
value.EnumerateObject().Where(kvp => kvp.Name != TypeTagPropertyName).Select(kvp => CreateProperty(kvp.Name, kvp.Value, null)),
tts);
}

if (value is JArray arr)
if (value.ValueKind == JsonValueKind.Array)
{
return new SequenceValue(arr.Select(v => CreatePropertyValue(v, null)));
return new SequenceValue(value.EnumerateArray().Select(v => CreatePropertyValue(v, null)));
}

var raw = value.Value<JValue>()!.Value;
object? raw = null;
switch (value.ValueKind)
{
case JsonValueKind.String:
raw = value.GetString(); break;
case JsonValueKind.True:
case JsonValueKind.False:
raw = value.GetBoolean(); break;
case JsonValueKind.Number:
{
if (value.TryGetInt64(out var x))
{
raw = x; break;
}
}
{
if (value.TryGetUInt64(out var x))
{
raw = x; break;
}
}
{
if (value.TryGetDouble(out var x))
{
raw = x; break;
}
}
{
if (value.TryGetDecimal(out var x))
{
raw = x; break;
}
}
break;
}
if (raw == null)
raw = value.GetRawText();

return renderings != null && renderings.Length != 0 ?
return renderings != null && renderings.Length != 0 ?
new RenderableScalarValue(raw, renderings) :
new ScalarValue(raw);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Nullable" Version="1.3.1" PrivateAssets="All" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text.Json;
using Serilog.Events;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -72,8 +71,8 @@ public void MessagesAreEscapedIntoTemplates()
public void HandlesDefaultJsonNetSerialization()
{
const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@m\":\"Hello\"}";
var jObject = JsonConvert.DeserializeObject<JObject>(document);
var evt = LogEventReader.ReadFromJObject(jObject);
using var jd = JsonDocument.Parse(document);
var evt = LogEventReader.ReadFromJObject(jd.RootElement);

Assert.Equal(DateTimeOffset.Parse("2016-10-12T04:20:58.0554314Z"), evt.Timestamp);
}
Expand Down
Loading