diff --git a/Build.ps1 b/Build.ps1 index 3d939cc..2284f51 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,10 +1,10 @@ -echo "build: Build started" +Write-Output "build: Build started" Push-Location $PSScriptRoot if(Test-Path .\artifacts) { - echo "build: Cleaning .\artifacts" - Remove-Item .\artifacts -Force -Recurse + Write-Output "build: Cleaning ./artifacts" + Remove-Item ./artifacts -Force -Recurse } & dotnet restore --no-cache @@ -18,31 +18,33 @@ $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($c echo "build: Package version suffix is $suffix" echo "build: Build version suffix is $buildSuffix" -foreach ($src in ls src/*) { +foreach ($src in Get-ChildItem src/*) { Push-Location $src - echo "build: Packaging project in $src" + Write-Output "build: Packaging project in $src" - & dotnet build -c Release --version-suffix=$buildSuffix + & dotnet build -c Release --version-suffix=$buildSuffix -p:ContinuousIntegrationBuild=true + if($LASTEXITCODE -ne 0) { throw "Build failed" } + if ($suffix) { - & dotnet pack -c Release --include-source -o ..\..\artifacts --version-suffix=$suffix --no-build + & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --no-build } else { - & dotnet pack -c Release --include-source -o ..\..\artifacts --no-build + & dotnet pack -c Release -o ..\..\artifacts --no-build } - if($LASTEXITCODE -ne 0) { exit 1 } + if($LASTEXITCODE -ne 0) { throw "Packaging failed" } Pop-Location } -foreach ($test in ls test/*.Tests) { +foreach ($test in Get-ChildItem test/*.Tests) { Push-Location $test - echo "build: Testing project in $test" + Write-Output "build: Testing project in $test" & dotnet test -c Release - if($LASTEXITCODE -ne 0) { exit 3 } + if($LASTEXITCODE -ne 0) { throw "Testing failed" } Pop-Location } -Pop-Location +Pop-Location \ No newline at end of file diff --git a/README.md b/README.md index 7bf556b..cdc789b 100644 --- a/README.md +++ b/README.md @@ -7,49 +7,46 @@ This package reads (deserializes) JSON log files created by [Serilog.Formatting. Log events are written to a file using `CompactJsonFormatter`: ```csharp -using (var fileLog = new LoggerConfiguration() +await using var fileLog = new LoggerConfiguration() .WriteTo.File(new CompactJsonFormatter(), "log.clef") - .CreateLogger()) + .CreateLogger(); + +fileLog.Information("Hello, {@User}", new { Name = "nblumhardt", Id = 101 }); +fileLog.Information("Number {N:x8}", 42); +fileLog.Warning("Tags are {Tags}", new[] { "test", "orange" }); + +try { - fileLog.Information("Hello, {@User}", new { Name = "nblumhardt", Id = 101 }); - fileLog.Information("Number {N:x8}", 42); - fileLog.Warning("Tags are {Tags}", new[] { "test", "orange" }); - - try - { - throw new DivideByZeroException(); - } - catch(Exception ex) - { - fileLog.Error(ex, "Something failed"); - } + throw new DivideByZeroException(); +} +catch(Exception ex) +{ + fileLog.Error(ex, "Something failed"); } ``` This creates a log file with content similar to: ```json -{"@t":"2016-10-12T04:46:58.0554314Z","@mt":"Hello, {@User}","User":{"Name":"nblumhardt","Id":101}} -{"@t":"2016-10-12T04:46:58.0684369Z","@mt":"Number {N:x8}","@r":["0000002a"],"N":42} -{"@t":"2016-10-12T04:46:58.0724384Z","@mt":"Tags are {Tags}","@l":"Warning","Tags":["test","orange"]} -{"@t":"2016-10-12T04:46:58.0904378Z","@mt":"Something failed","@l":"Error", "@x":"System.DivideByZer..."} +{"@t":"2024-10-12T04:46:58.0554314Z","@mt":"Hello, {@User}","User":{"Name":"nblumhardt","Id":101}} +{"@t":"2024-10-12T04:46:58.0684369Z","@mt":"Number {N:x8}","@r":["0000002a"],"N":42} +{"@t":"2024-10-12T04:46:58.0724384Z","@mt":"Tags are {Tags}","@l":"Warning","Tags":["test","orange"]} +{"@t":"2024-10-12T04:46:58.0904378Z","@mt":"Something failed","@l":"Error", "@x":"System.DivideByZer..."} ``` An instance of `LogEventReader` converts each line of the log file back into a `LogEvent`, which can be manipulated, rendered, or written through another Serilog sink: ```csharp -using (var console = new LoggerConfiguration() - .WriteTo.LiterateConsole() - .CreateLogger()) -{ - using (var clef = File.OpenText("log.clef")) - { - var reader = new LogEventReader(clef); - LogEvent evt; - while (reader.TryRead(out evt)) - console.Write(evt); - } -} +await using var console = new LoggerConfiguration() + .WriteTo.Console() + .CreateLogger(); + +await using var clef = File.OpenText("log.clef")) + +var reader = new LogEventReader(clef); + +while (reader.TryRead(out var evt)) + console.Write(evt); ``` Output from the logger: diff --git a/appveyor.yml b/appveyor.yml index c156929..4836a3c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,32 +1,22 @@ version: '{build}' skip_tags: true image: Visual Studio 2022 -install: - - ps: mkdir -Force ".\build\" | Out-Null build_script: -- ps: ./Build.ps1 -test: off + - pwsh: ./Build.ps1 artifacts: -- path: artifacts/Serilog.*.nupkg -only_commits: - files: - - serilog-sinks-compact-reader.sln - - src/Serilog.Sinks.Compact.Reader/ - - Build.ps1 - - assets/ - - test/Serilog.Formatting.Compact.Reader.Tests/ + - path: artifacts/Serilog.*.nupkg + - path: artifacts/Serilog.*.snupkg deploy: -- provider: NuGet - api_key: - secure: bg8cOj0trljnQUuVcpbplFOcgB/3xdCrtuuCzNf0e8Yq8IbpOiKIwow630Ox+pQR - skip_symbols: true - on: - branch: /^(main|dev)$/ -- provider: GitHub - auth_token: - secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX - artifact: /Serilog.*\.nupkg/ - tag: v$(appveyor_build_version) - on: - branch: main - + - provider: NuGet + api_key: + secure: bg8cOj0trljnQUuVcpbplFOcgB/3xdCrtuuCzNf0e8Yq8IbpOiKIwow630Ox+pQR + skip_symbols: true + on: + branch: /^(main|dev)$/ + - provider: GitHub + auth_token: + secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX + artifact: /Serilog.*\.nupkg/ + tag: v$(appveyor_build_version) + on: + branch: main diff --git a/example/RoundTrip/Program.cs b/example/RoundTrip/Program.cs index 93a7213..7a34e4b 100644 --- a/example/RoundTrip/Program.cs +++ b/example/RoundTrip/Program.cs @@ -4,44 +4,36 @@ using System.IO; using Serilog.Formatting.Compact; -namespace RoundTrip -{ - public class Program - { - public static void Main(string[] args) - { - using (var fileLog = new LoggerConfiguration() - .WriteTo.File(new CompactJsonFormatter(), "log.clef") - .CreateLogger()) - { - fileLog.Information("Hello, {@User}", new { Name = "nblumhardt", Id = 101 }); - fileLog.Information("Number {N:x8}", 42); - fileLog.Information("String {S}", "Yes"); - fileLog.Warning("Tags are {Tags}", new[] { "test", "orange" }); - try - { - throw new DivideByZeroException(); - } - catch(Exception ex) - { - fileLog.Error(ex, "Something failed"); - } - } +await using (var fileLog = new LoggerConfiguration() + .WriteTo.File(new CompactJsonFormatter(), "log.clef") + .CreateLogger()) +{ + fileLog.Information("Hello, {@User}", new { Name = "nblumhardt", Id = 101 }); + fileLog.Information("Number {N:x8}", 42); + fileLog.Information("String {S}", "Yes"); + fileLog.Warning("Tags are {Tags}", new[] { "test", "orange" }); - using (var console = new LoggerConfiguration() - .WriteTo.Console() - .CreateLogger()) - { - using (var clef = File.OpenText("log.clef")) - { - var reader = new LogEventReader(clef); - while (reader.TryRead(out var evt)) - console.Write(evt); - } - } + try + { + throw new DivideByZeroException(); + } + catch(Exception ex) + { + fileLog.Error(ex, "Something failed"); + } +} - File.Delete("log.clef"); - } +await using (var console = new LoggerConfiguration() + .WriteTo.Console() + .CreateLogger()) +{ + using (var clef = File.OpenText("log.clef")) + { + var reader = new LogEventReader(clef); + while (reader.TryRead(out var evt)) + console.Write(evt); } } + +File.Delete("log.clef"); diff --git a/example/RoundTrip/Properties/AssemblyInfo.cs b/example/RoundTrip/Properties/AssemblyInfo.cs deleted file mode 100644 index 3d54887..0000000 --- a/example/RoundTrip/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Example")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("961d5124-bbb2-4022-945e-1190bf6be0c9")] diff --git a/example/RoundTrip/RoundTrip.csproj b/example/RoundTrip/RoundTrip.csproj index e3fd35a..59fba20 100644 --- a/example/RoundTrip/RoundTrip.csproj +++ b/example/RoundTrip/RoundTrip.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 RoundTrip Exe RoundTrip @@ -15,9 +15,9 @@ - - - + + + diff --git a/global.json b/global.json deleted file mode 100644 index 1d19247..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "5.0.202", - "rollForward": "latestMajor" - } -} diff --git a/serilog-formatting-compact-reader.sln b/serilog-formatting-compact-reader.sln index 2a1f998..e54713b 100644 --- a/serilog-formatting-compact-reader.sln +++ b/serilog-formatting-compact-reader.sln @@ -13,7 +13,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{70BA4A README.md = README.md .gitattributes = .gitattributes .gitignore = .gitignore - global.json = global.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "asset", "asset", "{71DD3779-3435-42D0-BA02-AEA04EE5B1E5}" diff --git a/src/Serilog.Formatting.Compact.Reader/ClefFields.cs b/src/Serilog.Formatting.Compact.Reader/ClefFields.cs index 866b956..f3b7d11 100644 --- a/src/Serilog.Formatting.Compact.Reader/ClefFields.cs +++ b/src/Serilog.Formatting.Compact.Reader/ClefFields.cs @@ -12,44 +12,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +namespace Serilog.Formatting.Compact.Reader; -namespace Serilog.Formatting.Compact.Reader +static class ClefFields { - static class ClefFields + public const string Timestamp = "@t"; + public const string MessageTemplate = "@mt"; + public const string Level = "@l"; + public const string Exception = "@x"; + public const string Renderings = "@r"; + public const string EventId = "@i"; + public const string Message = "@m"; + public const string TraceId = "@tr"; + public const string SpanId = "@sp"; + + public static readonly string[] All = [Timestamp, MessageTemplate, Level, Exception, Renderings, EventId, Message, TraceId, SpanId + ]; + + const string Prefix = "@"; + const string EscapedInitialAt = "@@"; + + public static string Unescape(string name) { - public const string Timestamp = "@t"; - public const string MessageTemplate = "@mt"; - public const string Level = "@l"; - public const string Exception = "@x"; - public const string Renderings = "@r"; - public const string EventId = "@i"; - public const string Message = "@m"; - public const string TraceId = "@tr"; - public const string SpanId = "@sp"; + if (name.StartsWith(EscapedInitialAt)) + return name.Substring(1); - public static readonly string[] All = { Timestamp, MessageTemplate, Level, Exception, Renderings, EventId, Message, TraceId, SpanId }; - - const string Prefix = "@"; - const string EscapedInitialAt = "@@"; - - public static string Unescape(string name) - { - if (name.StartsWith(EscapedInitialAt)) - return name.Substring(1); - - return name; - } - - public static bool IsUnrecognized(string name) - { - return !name.StartsWith(EscapedInitialAt) && - name.StartsWith(Prefix) && - !All.Contains(name); - } + return name; } -} + public static bool IsUnrecognized(string name) + { + return !name.StartsWith(EscapedInitialAt) && + name.StartsWith(Prefix) && + !All.Contains(name); + } +} \ No newline at end of file diff --git a/src/Serilog.Formatting.Compact.Reader/LogEventReader.cs b/src/Serilog.Formatting.Compact.Reader/LogEventReader.cs index 233c9ed..7cb8fc1 100644 --- a/src/Serilog.Formatting.Compact.Reader/LogEventReader.cs +++ b/src/Serilog.Formatting.Compact.Reader/LogEventReader.cs @@ -12,236 +12,233 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog.Events; using Serilog.Parsing; -using System.Linq; + // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedMember.Global -namespace Serilog.Formatting.Compact.Reader +namespace Serilog.Formatting.Compact.Reader; + +/// +/// Reads files produced by Serilog.Formatting.Compact.CompactJsonFormatter. Events +/// are expected to be encoded as newline-separated JSON documents. +/// +public class LogEventReader : IDisposable { + static readonly MessageTemplateParser Parser = new(); + static readonly Rendering[] NoRenderings = []; + readonly TextReader _text; + readonly JsonSerializer _serializer; + + int _lineNumber; + /// - /// Reads files produced by Serilog.Formatting.Compact.CompactJsonFormatter. Events - /// are expected to be encoded as newline-separated JSON documents. + /// Construct a . /// - public class LogEventReader : IDisposable + /// Text to read from. + /// If specified, a JSON serializer used when converting event documents. + public LogEventReader(TextReader text, JsonSerializer? serializer = null) { - static readonly MessageTemplateParser Parser = new MessageTemplateParser(); - static readonly Rendering[] NoRenderings = Array.Empty(); - readonly TextReader _text; - readonly JsonSerializer _serializer; - - int _lineNumber; - - /// - /// Construct a . - /// - /// Text to read from. - /// If specified, a JSON serializer used when converting event documents. - public LogEventReader(TextReader text, JsonSerializer serializer = null) - { - _text = text ?? throw new ArgumentNullException(nameof(text)); - _serializer = serializer ?? CreateSerializer(); - } + _text = text ?? throw new ArgumentNullException(nameof(text)); + _serializer = serializer ?? CreateSerializer(); + } - /// - public void Dispose() - { - _text.Dispose(); - } + /// + public void Dispose() + { + _text.Dispose(); + } - /// - /// Read a line from the input. Blank lines are skipped. - /// - /// - /// True if an event could be read; false if the end-of-file was encountered. - /// The data format is invalid. - public bool TryRead(out LogEvent evt) + /// + /// Read a line from the input. Blank lines are skipped. + /// + /// + /// True if an event could be read; false if the end-of-file was encountered. + /// The data format is invalid. + public bool TryRead([NotNullWhen(true)] out LogEvent? evt) + { + var line = _text.ReadLine(); + _lineNumber++; + while (string.IsNullOrWhiteSpace(line)) { - var line = _text.ReadLine(); - _lineNumber++; - while (string.IsNullOrWhiteSpace(line)) + if (line == null) { - if (line == null) - { - evt = null; - return false; - } - line = _text.ReadLine(); - _lineNumber++; + evt = null; + return false; } + line = _text.ReadLine(); + _lineNumber++; + } - var data = _serializer.Deserialize(new JsonTextReader(new StringReader(line))); - if (!(data is JObject fields)) - throw new InvalidDataException($"The data on line {_lineNumber} is not a complete JSON object."); + var data = _serializer.Deserialize(new JsonTextReader(new StringReader(line))); + if (data is not JObject fields) + throw new InvalidDataException($"The data on line {_lineNumber} is not a complete JSON object."); - evt = ReadFromJObject(_lineNumber, fields); - return true; - } + evt = ReadFromJObject(_lineNumber, fields); + return true; + } - /// - /// 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. - public static LogEvent ReadFromString(string document, JsonSerializer serializer = null) - { - if (document == null) throw new ArgumentNullException(nameof(document)); + /// + /// 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. + public static LogEvent ReadFromString(string document, JsonSerializer? serializer = null) + { + if (document == null) throw new ArgumentNullException(nameof(document)); - serializer ??= CreateSerializer(); - var jObject = serializer.Deserialize(new JsonTextReader(new StringReader(document))); - return ReadFromJObject(jObject); + serializer ??= CreateSerializer(); + var jObject = serializer.Deserialize(new JsonTextReader(new StringReader(document))); + return ReadFromJObject(jObject!); + } - } + /// + /// Read a single log event from an already-deserialized JSON object. + /// + /// The deserialized compact-JSON event. + /// The log event. + public static LogEvent ReadFromJObject(JObject jObject) + { + if (jObject == null) throw new ArgumentNullException(nameof(jObject)); + return ReadFromJObject(1, jObject); + } - /// - /// Read a single log event from an already-deserialized JSON object. - /// - /// The deserialized compact-JSON event. - /// The log event. - public static LogEvent ReadFromJObject(JObject jObject) + static LogEvent ReadFromJObject(int lineNumber, JObject jObject) + { + var timestamp = GetRequiredTimestampField(lineNumber, jObject, ClefFields.Timestamp); + + string? messageTemplate; + if (TryGetOptionalField(lineNumber, jObject, ClefFields.MessageTemplate, out var mt)) + messageTemplate = mt; + else if (TryGetOptionalField(lineNumber, jObject, ClefFields.Message, out var m)) + messageTemplate = MessageTemplateSyntax.Escape(m); + else + messageTemplate = null; + + var level = LogEventLevel.Information; + if (TryGetOptionalField(lineNumber, jObject, ClefFields.Level, out var l)) + level = (LogEventLevel)Enum.Parse(typeof(LogEventLevel), l, true); + + Exception? exception = null; + if (TryGetOptionalField(lineNumber, jObject, ClefFields.Exception, out var ex)) + exception = new TextException(ex); + + 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 == null) throw new ArgumentNullException(nameof(jObject)); - return ReadFromJObject(1, jObject); + if (r is not JArray renderedByIndex) + 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()!)) + .ToArray(); } - static LogEvent ReadFromJObject(int lineNumber, JObject jObject) - { - var timestamp = GetRequiredTimestampField(lineNumber, jObject, ClefFields.Timestamp); - - string messageTemplate; - if (TryGetOptionalField(lineNumber, jObject, ClefFields.MessageTemplate, out var mt)) - messageTemplate = mt; - else if (TryGetOptionalField(lineNumber, jObject, ClefFields.Message, out var m)) - messageTemplate = MessageTemplateSyntax.Escape(m); - else - messageTemplate = null; - - var level = LogEventLevel.Information; - if (TryGetOptionalField(lineNumber, jObject, ClefFields.Level, out var l)) - level = (LogEventLevel)Enum.Parse(typeof(LogEventLevel), l, true); - - Exception exception = null; - if (TryGetOptionalField(lineNumber, jObject, ClefFields.Exception, out var ex)) - exception = new TextException(ex); - - 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(Enumerable.Empty()) : - Parser.Parse(messageTemplate); - - var renderings = NoRenderings; - - if (jObject.TryGetValue(ClefFields.Renderings, out var r)) - { - if (!(r is JArray renderedByIndex)) - 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())) - .ToArray(); - } - - var properties = jObject - .Properties() - .Where(f => !ClefFields.All.Contains(f.Name)) - .Select(f => - { - var name = ClefFields.Unescape(f.Name); - var renderingsByFormat = renderings.Length != 0 ? renderings.Where(rd => rd.Name == name).ToArray() : NoRenderings; - return PropertyFactory.CreateProperty(name, f.Value, renderingsByFormat); - }) - .ToList(); - - if (TryGetOptionalEventId(lineNumber, jObject, ClefFields.EventId, out var eventId)) + var properties = jObject + .Properties() + .Where(f => !ClefFields.All.Contains(f.Name)) + .Select(f => { - properties.Add(new LogEventProperty("@i", new ScalarValue(eventId))); - } + var name = ClefFields.Unescape(f.Name); + var renderingsByFormat = renderings.Length != 0 ? renderings.Where(rd => rd.Name == name).ToArray() : NoRenderings; + return PropertyFactory.CreateProperty(name, f.Value, renderingsByFormat); + }) + .ToList(); - return new LogEvent(timestamp, level, exception, parsedTemplate, properties, traceId, spanId); + if (TryGetOptionalEventId(lineNumber, jObject, ClefFields.EventId, out var eventId)) + { + properties.Add(new LogEventProperty("@i", new ScalarValue(eventId))); } - static bool TryGetOptionalField(int lineNumber, JObject data, string field, out string value) + return new LogEvent(timestamp, level, exception, parsedTemplate, properties, traceId, spanId); + } + + static bool TryGetOptionalField(int lineNumber, JObject data, string field, [NotNullWhen(true)] out string? value) + { + if (!data.TryGetValue(field, out var token) || token.Type == JTokenType.Null) { - if (!data.TryGetValue(field, out var token) || token.Type == JTokenType.Null) - { - value = null; - return false; - } + value = null; + return false; + } - if (token.Type != JTokenType.String) - throw new InvalidDataException($"The value of `{field}` on line {lineNumber} is not in a supported format."); + if (token.Type != JTokenType.String) + throw new InvalidDataException($"The value of `{field}` on line {lineNumber} is not in a supported format."); - value = token.Value(); - return true; - } + value = token.Value()!; + return true; + } - static bool TryGetOptionalEventId(int lineNumber, JObject data, string field, out object eventId) + static bool TryGetOptionalEventId(int lineNumber, JObject data, string field, out object? eventId) + { + if (!data.TryGetValue(field, out var token) || token.Type == JTokenType.Null) { - if (!data.TryGetValue(field, out var token) || token.Type == JTokenType.Null) - { - eventId = null; - return false; - } - - switch (token.Type) - { - case JTokenType.String: - eventId = token.Value(); - return true; - case JTokenType.Integer: - eventId = token.Value(); - return true; - default: - throw new InvalidDataException( - $"The value of `{field}` on line {lineNumber} is not in a supported format."); - } + eventId = null; + return false; } - static DateTimeOffset GetRequiredTimestampField(int lineNumber, JObject data, string field) + switch (token.Type) { - if (!data.TryGetValue(field, out var token) || token.Type == JTokenType.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; + case JTokenType.String: + eventId = token.Value(); + return true; + case JTokenType.Integer: + eventId = token.Value(); + return true; + default: + throw new InvalidDataException( + $"The value of `{field}` on line {lineNumber} is not in a supported format."); + } + } - return (DateTime)dt!; - } + static DateTimeOffset GetRequiredTimestampField(int lineNumber, JObject data, string field) + { + if (!data.TryGetValue(field, out var token) || token.Type == JTokenType.Null) + throw new InvalidDataException($"The data on line {lineNumber} does not include the required `{field}` field."); - if (token.Type != JTokenType.String) - throw new InvalidDataException($"The value of `{field}` on line {lineNumber} is not in a supported format."); + if (token.Type == JTokenType.Date) + { + var dt = token.Value()!.Value; + if (dt is DateTimeOffset offset) + return offset; - var text = token.Value(); - return DateTimeOffset.Parse(text); + return (DateTime)dt!; } - static JsonSerializer CreateSerializer() + 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()!; + return DateTimeOffset.Parse(text); + } + + static JsonSerializer CreateSerializer() + { + return JsonSerializer.Create(new JsonSerializerSettings { - return JsonSerializer.Create(new JsonSerializerSettings - { - DateParseHandling = DateParseHandling.None, - Culture = CultureInfo.InvariantCulture - }); - } + DateParseHandling = DateParseHandling.None, + Culture = CultureInfo.InvariantCulture + }); } -} +} \ No newline at end of file diff --git a/src/Serilog.Formatting.Compact.Reader/MessageTemplateSyntax.cs b/src/Serilog.Formatting.Compact.Reader/MessageTemplateSyntax.cs index 5564b16..b3c4117 100644 --- a/src/Serilog.Formatting.Compact.Reader/MessageTemplateSyntax.cs +++ b/src/Serilog.Formatting.Compact.Reader/MessageTemplateSyntax.cs @@ -1,14 +1,11 @@ -using System; +namespace Serilog.Formatting.Compact.Reader; -namespace Serilog.Formatting.Compact.Reader +static class MessageTemplateSyntax { - static class MessageTemplateSyntax + public static string Escape(string text) { - public static string Escape(string text) - { if (text == null) throw new ArgumentNullException(nameof(text)); return text.Replace("{", "{{").Replace("}", "}}"); } - } -} +} \ No newline at end of file diff --git a/src/Serilog.Formatting.Compact.Reader/PropertyFactory.cs b/src/Serilog.Formatting.Compact.Reader/PropertyFactory.cs index a67005a..03ad63b 100644 --- a/src/Serilog.Formatting.Compact.Reader/PropertyFactory.cs +++ b/src/Serilog.Formatting.Compact.Reader/PropertyFactory.cs @@ -14,48 +14,45 @@ using Newtonsoft.Json.Linq; using Serilog.Events; -using System.Collections.Generic; -using System.Linq; -namespace Serilog.Formatting.Compact.Reader +namespace Serilog.Formatting.Compact.Reader; + +static class PropertyFactory { - static class PropertyFactory + const string TypeTagPropertyName = "$type"; + const string InvalidPropertyNameSubstitute = "(unnamed)"; + + public static LogEventProperty CreateProperty(string name, JToken value, Rendering[]? renderings) { - const string TypeTagPropertyName = "$type"; - const string InvalidPropertyNameSubstitute = "(unnamed)"; + // The format allows (does not disallow) empty/null property names, but Serilog cannot represent them. + if (!LogEventProperty.IsValidName(name)) + name = InvalidPropertyNameSubstitute; + + return new LogEventProperty(name, CreatePropertyValue(value, renderings)); + } - public static LogEventProperty CreateProperty(string name, JToken value, Rendering[] renderings) + static LogEventPropertyValue CreatePropertyValue(JToken value, Rendering[]? renderings) + { + if (value.Type == JTokenType.Null) + return new ScalarValue(null); + + if (value is JObject obj) { - // The format allows (does not disallow) empty/null property names, but Serilog cannot represent them. - if (!LogEventProperty.IsValidName(name)) - name = InvalidPropertyNameSubstitute; - - return new LogEventProperty(name, CreatePropertyValue(value, renderings)); + obj.TryGetValue(TypeTagPropertyName, out var tt); + return new StructureValue( + obj.Properties().Where(kvp => kvp.Name != TypeTagPropertyName).Select(kvp => CreateProperty(kvp.Name, kvp.Value, null)), + tt?.Value()); } - static LogEventPropertyValue CreatePropertyValue(JToken value, Rendering[] renderings) + if (value is JArray arr) { - if (value.Type == JTokenType.Null) - return new ScalarValue(null); - - if (value is JObject obj) - { - obj.TryGetValue(TypeTagPropertyName, out var tt); - return new StructureValue( - obj.Properties().Where(kvp => kvp.Name != TypeTagPropertyName).Select(kvp => CreateProperty(kvp.Name, kvp.Value, null)), - tt?.Value()); - } - - if (value is JArray arr) - { - return new SequenceValue(arr.Select(v => CreatePropertyValue(v, null))); - } - - var raw = value.Value().Value; - - return renderings != null && renderings.Length != 0 ? - new RenderableScalarValue(raw, renderings) : - new ScalarValue(raw); + return new SequenceValue(arr.Select(v => CreatePropertyValue(v, null))); } + + var raw = value.Value()!.Value; + + return renderings != null && renderings.Length != 0 ? + new RenderableScalarValue(raw, renderings) : + new ScalarValue(raw); } -} +} \ No newline at end of file diff --git a/src/Serilog.Formatting.Compact.Reader/RenderableScalarValue.cs b/src/Serilog.Formatting.Compact.Reader/RenderableScalarValue.cs index 0e4d411..e569ee6 100644 --- a/src/Serilog.Formatting.Compact.Reader/RenderableScalarValue.cs +++ b/src/Serilog.Formatting.Compact.Reader/RenderableScalarValue.cs @@ -12,32 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.IO; using Serilog.Events; -using System.Collections.Generic; -namespace Serilog.Formatting.Compact.Reader +namespace Serilog.Formatting.Compact.Reader; + +class RenderableScalarValue : ScalarValue { - class RenderableScalarValue : ScalarValue - { - readonly Dictionary _renderings = new Dictionary(); + readonly Dictionary _renderings = new(); - public RenderableScalarValue(object value, IEnumerable renderings) - : base(value) - { - if (renderings == null) throw new ArgumentNullException(nameof(renderings)); - foreach (var rendering in renderings) - _renderings[rendering.Format] = rendering.Rendered; - } + public RenderableScalarValue(object? value, IEnumerable renderings) + : base(value) + { + if (renderings == null) throw new ArgumentNullException(nameof(renderings)); + foreach (var rendering in renderings) + _renderings[rendering.Format] = rendering.Rendered; + } - public override void Render(TextWriter output, string format = null, IFormatProvider formatProvider = null) - { - string rendering; - if (format != null && _renderings.TryGetValue(format, out rendering)) - output.Write(rendering); - else - base.Render(output, format, formatProvider); - } + public override void Render(TextWriter output, string? format = null, IFormatProvider? formatProvider = null) + { + if (format != null && _renderings.TryGetValue(format, out var rendering)) + output.Write(rendering); + else + base.Render(output, format, formatProvider); } } \ No newline at end of file diff --git a/src/Serilog.Formatting.Compact.Reader/Rendering.cs b/src/Serilog.Formatting.Compact.Reader/Rendering.cs index 1d7a74c..880a824 100644 --- a/src/Serilog.Formatting.Compact.Reader/Rendering.cs +++ b/src/Serilog.Formatting.Compact.Reader/Rendering.cs @@ -12,19 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Serilog.Formatting.Compact.Reader +namespace Serilog.Formatting.Compact.Reader; + +class Rendering { - class Rendering - { - public string Name { get; } - public string Format { get; } - public string Rendered { get; } + public string Name { get; } + public string Format { get; } + public string Rendered { get; } - public Rendering(string name, string format, string rendered) - { - Name = name; - Format = format; - Rendered = rendered; - } + public Rendering(string name, string format, string rendered) + { + Name = name; + Format = format; + Rendered = rendered; } } \ No newline at end of file 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 97349cd..0518f21 100644 --- a/src/Serilog.Formatting.Compact.Reader/Serilog.Formatting.Compact.Reader.csproj +++ b/src/Serilog.Formatting.Compact.Reader/Serilog.Formatting.Compact.Reader.csproj @@ -1,29 +1,37 @@ - 3.0.1 - net462;net471 - $(TargetFrameworks);netstandard2.1;netstandard2.0;net5.0;net6.0;net7.0 + 4.0.0 + Serilog Contributors + + net471;net462 + + $(TargetFrameworks);net8.0;net6.0;netstandard2.0 true true - Serilog.Formatting.Compact.Reader ../../asset/Serilog.snk true true - Serilog.Formatting.Compact.Reader serilog;json icon.png Apache-2.0 https://github.com/serilog/serilog-formatting-compact-reader - https://github.com/serilog/serilog-formatting-compact-reader - git README.md - 8 + latest + true + true + true + snupkg + enable + enable - + + diff --git a/src/Serilog.Formatting.Compact.Reader/TextException.cs b/src/Serilog.Formatting.Compact.Reader/TextException.cs index 69cd22f..eea11d8 100644 --- a/src/Serilog.Formatting.Compact.Reader/TextException.cs +++ b/src/Serilog.Formatting.Compact.Reader/TextException.cs @@ -12,23 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; +namespace Serilog.Formatting.Compact.Reader; -namespace Serilog.Formatting.Compact.Reader +class TextException : Exception { - class TextException : Exception - { - readonly string _text; + readonly string _text; - public TextException(string text) - : base("This exception type provides ToString() access to details only.") - { - _text = text; - } + public TextException(string text) + : base("This exception type provides ToString() access to details only.") + { + _text = text; + } - public override string ToString() - { - return _text; - } + public override string ToString() + { + return _text; } -} +} \ No newline at end of file diff --git a/test/Serilog.Formatting.Compact.Reader.Tests/LogEventReaderTests.cs b/test/Serilog.Formatting.Compact.Reader.Tests/LogEventReaderTests.cs index 94c4b55..ea4fb5f 100644 --- a/test/Serilog.Formatting.Compact.Reader.Tests/LogEventReaderTests.cs +++ b/test/Serilog.Formatting.Compact.Reader.Tests/LogEventReaderTests.cs @@ -6,110 +6,109 @@ using System.IO; using Xunit; -namespace Serilog.Formatting.Compact.Reader.Tests +namespace Serilog.Formatting.Compact.Reader.Tests; + +public class LogEventReaderTests { - public class LogEventReaderTests + [Fact] + public void AllEventsAreRead() { - [Fact] - public void AllEventsAreRead() - { - var all = new List(); - - using (var clef = File.OpenText("LogEventReaderTests.clef")) - { - var reader = new LogEventReader(clef); - while (reader.TryRead(out var evt)) - all.Add(evt); - } - - Assert.Equal(6, all.Count); - } + var all = new List(); - [Fact] - public void BasicFieldsAreRead() + using (var clef = File.OpenText("LogEventReaderTests.clef")) { - const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@mt\":\"Hello, {@User}\",\"User\":{\"Name\":\"nblumhardt\",\"Id\":101}}"; - var evt = LogEventReader.ReadFromString(document); - - Assert.Equal(DateTimeOffset.Parse("2016-10-12T04:20:58.0554314Z"), evt.Timestamp); - Assert.Equal(LogEventLevel.Information, evt.Level); - Assert.Equal("Hello, {@User}", evt.MessageTemplate.Text); - - var user = (StructureValue)evt.Properties["User"]; - Assert.Equal("Name", user.Properties[0].Name); - Assert.Equal(new ScalarValue("nblumhardt"), (ScalarValue)user.Properties[0].Value); - Assert.Equal("Id", user.Properties[1].Name); - Assert.Equal(101L, ((ScalarValue)user.Properties[1].Value).Value); + var reader = new LogEventReader(clef); + while (reader.TryRead(out var evt)) + all.Add(evt); } - [Fact] - public void MessagesAreEscapedIntoTemplates() - { - const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@m\":\"Hello, {text}\"}"; - var evt = LogEventReader.ReadFromString(document); + Assert.Equal(6, all.Count); + } - Assert.Equal("Hello, {{text}}", evt.MessageTemplate.Text); - } + [Fact] + public void BasicFieldsAreRead() + { + const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@mt\":\"Hello, {@User}\",\"User\":{\"Name\":\"nblumhardt\",\"Id\":101}}"; + var evt = LogEventReader.ReadFromString(document); + + Assert.Equal(DateTimeOffset.Parse("2016-10-12T04:20:58.0554314Z"), evt.Timestamp); + Assert.Equal(LogEventLevel.Information, evt.Level); + Assert.Equal("Hello, {@User}", evt.MessageTemplate.Text); + + var user = (StructureValue)evt.Properties["User"]; + Assert.Equal("Name", user.Properties[0].Name); + Assert.Equal(new ScalarValue("nblumhardt"), (ScalarValue)user.Properties[0].Value); + Assert.Equal("Id", user.Properties[1].Name); + Assert.Equal(101L, ((ScalarValue)user.Properties[1].Value).Value); + } - [Fact] - 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); + [Fact] + public void MessagesAreEscapedIntoTemplates() + { + const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@m\":\"Hello, {text}\"}"; + var evt = LogEventReader.ReadFromString(document); - Assert.Equal(DateTimeOffset.Parse("2016-10-12T04:20:58.0554314Z"), evt.Timestamp); - } + Assert.Equal("Hello, {{text}}", evt.MessageTemplate.Text); + } - [Fact] - public void RoundTripsTypeTags() - { - const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@m\":\"Hello\",\"User\":{\"$type\":\"TestUser\",\"Name\":\"nblumhardt\"}}"; - var evt = LogEventReader.ReadFromString(document); + [Fact] + 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); - var user = (StructureValue)evt.Properties["User"]; - Assert.Equal("TestUser", user.TypeTag); - } + Assert.Equal(DateTimeOffset.Parse("2016-10-12T04:20:58.0554314Z"), evt.Timestamp); + } - [Fact] - public void PassesThroughUnrecognizedReifiedProperties() - { - const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@m\":\"Hello\",\"@foo\":42}"; - var evt = LogEventReader.ReadFromString(document); + [Fact] + public void RoundTripsTypeTags() + { + const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@m\":\"Hello\",\"User\":{\"$type\":\"TestUser\",\"Name\":\"nblumhardt\"}}"; + var evt = LogEventReader.ReadFromString(document); - var foo = evt.Properties["@foo"]; - Assert.Equal(42L, ((ScalarValue)foo).Value); + var user = (StructureValue)evt.Properties["User"]; + Assert.Equal("TestUser", user.TypeTag); + } - // Ensure we don't just forward everything - Assert.False(evt.Properties.ContainsKey("@m")); - } + [Fact] + public void PassesThroughUnrecognizedReifiedProperties() + { + const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@m\":\"Hello\",\"@foo\":42}"; + var evt = LogEventReader.ReadFromString(document); - [Fact] - public void MissingMessagesAreAcceptedAsEmpty() - { - const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\"}"; - var evt = LogEventReader.ReadFromString(document); + var foo = evt.Properties["@foo"]; + Assert.Equal(42L, ((ScalarValue)foo).Value); - Assert.Empty(evt.MessageTemplate.Tokens); - } + // Ensure we don't just forward everything + Assert.False(evt.Properties.ContainsKey("@m")); + } - [Fact] - public void EventIdIntegersAreAccepted() - { - const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@i\":42,\"@m\":\"Hello\"}"; - var evt = LogEventReader.ReadFromString(document); + [Fact] + public void MissingMessagesAreAcceptedAsEmpty() + { + const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\"}"; + var evt = LogEventReader.ReadFromString(document); + + Assert.Empty(evt.MessageTemplate.Tokens); + } + + [Fact] + public void EventIdIntegersAreAccepted() + { + const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@i\":42,\"@m\":\"Hello\"}"; + var evt = LogEventReader.ReadFromString(document); - Assert.Equal((uint)42, ((ScalarValue)evt.Properties["@i"]).Value); - } + Assert.Equal((uint)42, ((ScalarValue)evt.Properties["@i"]).Value); + } - [Fact] - public void ReadsTraceAndSpanIds() - { - const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@tr\":\"1befc31e94b01d1a473f63a7905f6c9b\",\"@sp\":\"bb1111820570b80e\"}"; - var evt = LogEventReader.ReadFromString(document); + [Fact] + public void ReadsTraceAndSpanIds() + { + const string document = "{\"@t\":\"2016-10-12T04:20:58.0554314Z\",\"@tr\":\"1befc31e94b01d1a473f63a7905f6c9b\",\"@sp\":\"bb1111820570b80e\"}"; + var evt = LogEventReader.ReadFromString(document); - Assert.Equal("1befc31e94b01d1a473f63a7905f6c9b", evt.TraceId.ToString()); - Assert.Equal("bb1111820570b80e", evt.SpanId.ToString()); - } + Assert.Equal("1befc31e94b01d1a473f63a7905f6c9b", evt.TraceId.ToString()); + Assert.Equal("bb1111820570b80e", evt.SpanId.ToString()); } -} +} \ No newline at end of file diff --git a/test/Serilog.Formatting.Compact.Reader.Tests/Properties/launchSettings.json b/test/Serilog.Formatting.Compact.Reader.Tests/Properties/launchSettings.json deleted file mode 100644 index 3ab0635..0000000 --- a/test/Serilog.Formatting.Compact.Reader.Tests/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "test": { - "commandName": "test" - }, - "test-dnxcore50": { - "commandName": "test", - "sdkVersion": "dnx-coreclr-win-x86.1.0.0-rc1-final" - } - } -} \ No newline at end of file diff --git a/test/Serilog.Formatting.Compact.Reader.Tests/PropertyFactoryTests.cs b/test/Serilog.Formatting.Compact.Reader.Tests/PropertyFactoryTests.cs index d411195..040723a 100644 --- a/test/Serilog.Formatting.Compact.Reader.Tests/PropertyFactoryTests.cs +++ b/test/Serilog.Formatting.Compact.Reader.Tests/PropertyFactoryTests.cs @@ -2,27 +2,26 @@ using Serilog.Events; using Xunit; -namespace Serilog.Formatting.Compact.Reader.Tests +namespace Serilog.Formatting.Compact.Reader.Tests; + +public class PropertyFactoryTests { - public class PropertyFactoryTests + [Fact] + public void PropertiesAreConstructed() { - [Fact] - public void PropertiesAreConstructed() - { - const string name = "Test"; - const string value = "Value"; - var p = PropertyFactory.CreateProperty(name, new JValue(value), null); - Assert.Equal(p.Name, name); - var s = Assert.IsType(p.Value); - Assert.Equal(value, s.Value); - } + const string name = "Test"; + const string value = "Value"; + var p = PropertyFactory.CreateProperty(name, new JValue(value), null); + Assert.Equal(name, p.Name); + var s = Assert.IsType(p.Value); + Assert.Equal(value, s.Value); + } - [Fact] - public void InvalidPropertyNamesAreSubstituted() - { - const string name = ""; - var p = PropertyFactory.CreateProperty(name, new JValue((object)null), null); - Assert.NotEqual(p.Name, name); - } + [Fact] + public void InvalidPropertyNamesAreSubstituted() + { + const string name = ""; + var p = PropertyFactory.CreateProperty(name, new JValue((object)null), null); + Assert.NotEqual(name, p.Name); } } \ No newline at end of file diff --git a/test/Serilog.Formatting.Compact.Reader.Tests/Serilog.Formatting.Compact.Reader.Tests.csproj b/test/Serilog.Formatting.Compact.Reader.Tests/Serilog.Formatting.Compact.Reader.Tests.csproj index 9789646..5b72f6d 100644 --- a/test/Serilog.Formatting.Compact.Reader.Tests/Serilog.Formatting.Compact.Reader.Tests.csproj +++ b/test/Serilog.Formatting.Compact.Reader.Tests/Serilog.Formatting.Compact.Reader.Tests.csproj @@ -1,12 +1,11 @@ - net462;net5.0 - Serilog.Formatting.Compact.Reader.Tests + net4.8;net8.0 ../../asset/Serilog.snk true true - Serilog.Formatting.Compact.Reader.Tests + latest @@ -20,13 +19,12 @@ - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - - - +