diff --git a/src/Serilog.Formatting.Compact.Reader/LogEventReader.cs b/src/Serilog.Formatting.Compact.Reader/LogEventReader.cs index 5f3be60..a955bf5 100644 --- a/src/Serilog.Formatting.Compact.Reader/LogEventReader.cs +++ b/src/Serilog.Formatting.Compact.Reader/LogEventReader.cs @@ -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; @@ -34,7 +34,6 @@ public class LogEventReader : IDisposable static readonly MessageTemplateParser Parser = new(); static readonly Rendering[] NoRenderings = []; readonly TextReader _text; - readonly JsonSerializer _serializer; int _lineNumber; @@ -42,11 +41,9 @@ public class LogEventReader : IDisposable /// Construct a . /// /// Text to read from. - /// If specified, a JSON serializer used when converting event documents. - public LogEventReader(TextReader text, JsonSerializer? serializer = null) + public LogEventReader(TextReader text) { _text = text ?? throw new ArgumentNullException(nameof(text)); - _serializer = serializer ?? CreateSerializer(); } /// @@ -127,63 +124,58 @@ public bool TryRead([NotNullWhen(true)] out LogEvent? evt) /// Read a single log event from a JSON-encoded document. /// /// The event in compact-JSON. - /// If specified, a JSON serializer used when converting event documents. /// The log event. /// The data format is invalid. - 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); + } } - /// /// Read a single log event from an already-deserialized JSON object. /// /// The deserialized compact-JSON event. /// The log event. /// The data format is invalid. - 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); @@ -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() .Where(t => t.Format != null) - .Zip(renderedByIndex, (t, rd) => new Rendering(t.PropertyName, t.Format!, rd.Value()!)) + .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 => { @@ -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()!; + 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(); - return true; - case JTokenType.Integer: - eventId = token.Value(); + 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()!.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()!; - 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; } } diff --git a/src/Serilog.Formatting.Compact.Reader/PropertyFactory.cs b/src/Serilog.Formatting.Compact.Reader/PropertyFactory.cs index 03ad63b..3d52f3e 100644 --- a/src/Serilog.Formatting.Compact.Reader/PropertyFactory.cs +++ b/src/Serilog.Formatting.Compact.Reader/PropertyFactory.cs @@ -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; @@ -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)) @@ -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()); + 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()!.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); } diff --git a/src/Serilog.Formatting.Compact.Reader/Serilog.Formatting.Compact.Reader.csproj b/src/Serilog.Formatting.Compact.Reader/Serilog.Formatting.Compact.Reader.csproj index fd772a2..421b7de 100644 --- a/src/Serilog.Formatting.Compact.Reader/Serilog.Formatting.Compact.Reader.csproj +++ b/src/Serilog.Formatting.Compact.Reader/Serilog.Formatting.Compact.Reader.csproj @@ -33,7 +33,7 @@ - + diff --git a/test/Serilog.Formatting.Compact.Reader.Tests/LogEventReaderTests.cs b/test/Serilog.Formatting.Compact.Reader.Tests/LogEventReaderTests.cs index 4c36b5c..d25f4ac 100644 --- a/test/Serilog.Formatting.Compact.Reader.Tests/LogEventReaderTests.cs +++ b/test/Serilog.Formatting.Compact.Reader.Tests/LogEventReaderTests.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; using Serilog.Events; using System; using System.Collections.Generic; @@ -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(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); } diff --git a/test/Serilog.Formatting.Compact.Reader.Tests/PropertyFactoryTests.cs b/test/Serilog.Formatting.Compact.Reader.Tests/PropertyFactoryTests.cs index 040723a..2fe9ae3 100644 --- a/test/Serilog.Formatting.Compact.Reader.Tests/PropertyFactoryTests.cs +++ b/test/Serilog.Formatting.Compact.Reader.Tests/PropertyFactoryTests.cs @@ -1,5 +1,5 @@ -using Newtonsoft.Json.Linq; -using Serilog.Events; +using Serilog.Events; +using System.Text.Json; using Xunit; namespace Serilog.Formatting.Compact.Reader.Tests; @@ -11,7 +11,8 @@ public void PropertiesAreConstructed() { const string name = "Test"; const string value = "Value"; - var p = PropertyFactory.CreateProperty(name, new JValue(value), null); + using var jd = JsonDocument.Parse($"{{\"{name}\": \"{value}\"}}"); + var p = PropertyFactory.CreateProperty(name, jd.RootElement.GetProperty(name), null); Assert.Equal(name, p.Name); var s = Assert.IsType(p.Value); Assert.Equal(value, s.Value); @@ -21,7 +22,8 @@ public void PropertiesAreConstructed() public void InvalidPropertyNamesAreSubstituted() { const string name = ""; - var p = PropertyFactory.CreateProperty(name, new JValue((object)null), null); + using var jd = JsonDocument.Parse("null"); + var p = PropertyFactory.CreateProperty(name, jd.RootElement, null); Assert.NotEqual(name, p.Name); } } \ No newline at end of file