From 322c5f2dc247af05c93a86dfdf0ae4349752238e Mon Sep 17 00:00:00 2001 From: James Duda Date: Wed, 8 Nov 2023 23:44:55 +0000 Subject: [PATCH 01/43] SerilogLoggerScope add support for ValueTuple state. Add SerilogLoggerScopeTests. --- .../Extensions/Logging/SerilogLoggerScope.cs | 17 ++- .../SerilogLoggerScopeTests.cs | 100 ++++++++++++++++++ .../Support/LogEventPropertyFactory.cs | 15 +++ 3 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs create mode 100644 test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs index 9d13190..4d4a2c1 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs @@ -49,11 +49,9 @@ public void Dispose() public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, out LogEventPropertyValue? scopeItem) { - void AddProperty(KeyValuePair stateProperty) + void AddProperty(string key, object? value) { - var key = stateProperty.Key; var destructureObject = false; - var value = stateProperty.Value; if (key.StartsWith("@")) { @@ -86,7 +84,7 @@ void AddProperty(KeyValuePair stateProperty) if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) scopeItem = new ScalarValue(_state.ToString()); else - AddProperty(stateProperty); + AddProperty(stateProperty.Key, stateProperty.Value); } } else if (_state is IEnumerable> stateProperties) @@ -98,9 +96,18 @@ void AddProperty(KeyValuePair stateProperty) if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) scopeItem = new ScalarValue(_state.ToString()); else - AddProperty(stateProperty); + AddProperty(stateProperty.Key, stateProperty.Value); } } + else if (_state is ValueTuple tuple) + { + scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. + + if (tuple.Item1 == SerilogLoggerProvider.OriginalFormatPropertyName && tuple.Item2 is string) + scopeItem = new ScalarValue(_state.ToString()); + else + AddProperty(tuple.Item1, tuple.Item2); + } else { scopeItem = propertyFactory.CreateProperty(NoName, _state).Value; diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs new file mode 100644 index 0000000..7567a1f --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Serilog.Events; +using Serilog.Extensions.Logging.Tests.Support; + +using Xunit; + +namespace Serilog.Extensions.Logging.Tests; +public class SerilogLoggerScopeTests +{ + static (SerilogLoggerProvider, LogEventPropertyFactory, LogEvent) SetUp() + { + var loggerProvider = new SerilogLoggerProvider(); + + var logEventPropertyFactory = new LogEventPropertyFactory(); + + var dateTimeOffset = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero); + var messageTemplate = new MessageTemplate(Enumerable.Empty()); + var properties = Enumerable.Empty(); + var logEvent = new LogEvent(dateTimeOffset, LogEventLevel.Information, null, messageTemplate, properties); + + return (loggerProvider, logEventPropertyFactory, logEvent); + } + + [Fact] + public void EnrichWithDictionaryStringObject() + { + const string propertyName = "Foo"; + const string expectedValue = "Bar"; + + var(loggerProvider, logEventPropertyFactory, logEvent) = SetUp(); + + + var state = new Dictionary() { { propertyName, expectedValue } }; + + var loggerScope = new SerilogLoggerScope(loggerProvider, state); + + loggerScope.EnrichAndCreateScopeItem(logEvent, logEventPropertyFactory, out LogEventPropertyValue? scopeItem); + + Assert.Contains(propertyName, logEvent.Properties); + + var scalarValue = logEvent.Properties[propertyName] as ScalarValue; + Assert.NotNull(scalarValue); + + var actualValue = scalarValue.Value as string; + Assert.NotNull(actualValue); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void EnrichWithIEnumerableKeyValuePairStringObject() + { + const string propertyName = "Foo"; + const string expectedValue = "Bar"; + + var (loggerProvider, logEventPropertyFactory, logEvent) = SetUp(); + + + var state = new KeyValuePair[] { new KeyValuePair(propertyName, expectedValue) }; + + var loggerScope = new SerilogLoggerScope(loggerProvider, state); + + loggerScope.EnrichAndCreateScopeItem(logEvent, logEventPropertyFactory, out LogEventPropertyValue? scopeItem); + + Assert.Contains(propertyName, logEvent.Properties); + + var scalarValue = logEvent.Properties[propertyName] as ScalarValue; + Assert.NotNull(scalarValue); + + var actualValue = scalarValue.Value as string; + Assert.NotNull(actualValue); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void EnrichWithTupleStringObject() + { + const string propertyName = "Foo"; + const string expectedValue = "Bar"; + + var (loggerProvider, logEventPropertyFactory, logEvent) = SetUp(); + + + var state = (propertyName, (object)expectedValue); + + var loggerScope = new SerilogLoggerScope(loggerProvider, state); + + loggerScope.EnrichAndCreateScopeItem(logEvent, logEventPropertyFactory, out LogEventPropertyValue? scopeItem); + + Assert.Contains(propertyName, logEvent.Properties); + + var scalarValue = logEvent.Properties[propertyName] as ScalarValue; + Assert.NotNull(scalarValue); + + var actualValue = scalarValue.Value as string; + Assert.NotNull(actualValue); + Assert.Equal(expectedValue, actualValue); + } +} diff --git a/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs b/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs new file mode 100644 index 0000000..b1116fd --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Extensions.Logging.Tests.Support; +internal class LogEventPropertyFactory : ILogEventPropertyFactory +{ + public LogEventProperty CreateProperty(string name, object? value, bool destructureObjects = false) + { + var scalarValue = new ScalarValue(value); + return new LogEventProperty(name, scalarValue); + } +} From d8637c2ef7d99696ac625ea7abed73c913d6b177 Mon Sep 17 00:00:00 2001 From: James Duda Date: Sun, 12 Nov 2023 22:53:36 +0000 Subject: [PATCH 02/43] Update readme with exaple of BeginScope with a ValueTuple. --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bbed04e..719aa8a 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ using (_logger.BeginScope("Transaction")) { If you simply want to add a "bag" of additional properties to your log events, however, this extension method approach can be overly verbose. For example, to add `TransactionId` and `ResponseJson` properties to your log events, you would have to do something like the following: ```csharp -// WRONG! Prefer the dictionary approach below instead +// WRONG! Prefer the dictionary or value tuple approach below instead using (_logger.BeginScope("TransactionId: {TransactionId}, ResponseJson: {ResponseJson}", 12345, jsonString)) { _logger.LogInformation("Completed in {DurationMs}ms...", 30); } @@ -144,6 +144,23 @@ using (_logger.BeginScope(scopeProps) { // } ``` +Alternatively provide a `ValueTuple` to this method, where `Item1` is the property name and `Item2` is the property value. Note that `T2` _must_ be `object?`. + +```csharp +using (_logger.BeginScope(("TransactionId", (object?)12345)) { + _logger.LogInformation("Transaction completed in {DurationMs}ms...", 30); +} +// Example JSON output: +// { +// "@t":"2020-10-29T19:05:56.4176816Z", +// "@m":"Completed in 30ms...", +// "@i":"51812baa", +// "DurationMs":30, +// "SourceContext":"SomeNamespace.SomeService", +// "TransactionId": 12345 +// } +``` + ### Versioning This package tracks the versioning and target framework support of its [_Microsoft.Extensions.Logging_](https://nuget.org/packages/Microsoft.Extensions.Logging) dependency. From 85172ada1875cb29bfa072e7a97251623ac853b6 Mon Sep 17 00:00:00 2001 From: James Duda Date: Tue, 14 Nov 2023 18:08:29 +0000 Subject: [PATCH 03/43] Changed copyright header from .NET Foundation to Serilog Contributors. --- .../Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs | 2 +- .../Support/LogEventPropertyFactory.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs index 7567a1f..442d737 100644 --- a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright © Serilog Contributors // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Serilog.Events; diff --git a/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs b/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs index b1116fd..12d34af 100644 --- a/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs +++ b/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright © Serilog Contributors // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Serilog.Core; From 14c2965547f3ab8fad3bb10765a067a9c91c98c9 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 15 Nov 2023 08:46:28 +1000 Subject: [PATCH 04/43] Update Serilog.Extensions.Logging.csproj Dev version bump [skip ci] --- .../Serilog.Extensions.Logging.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj index c37866f..a197a45 100644 --- a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj +++ b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj @@ -3,7 +3,7 @@ Low-level Serilog provider for Microsoft.Extensions.Logging - 8.0.0 + 8.0.1 Microsoft;Serilog Contributors From 7a2fa1dbb89c6977f9e50ec6b65d99e5e7377f1a Mon Sep 17 00:00:00 2001 From: jods Date: Tue, 13 Feb 2024 00:43:33 +0100 Subject: [PATCH 05/43] Minor optimisations Fixes #241 --- .../Extensions/Logging/SerilogLogger.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 06efe62..3aa7fb1 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -103,12 +103,12 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state { messageTemplate = value; } - else if (property.Key.StartsWith("@")) + else if (property.Key.StartsWith('@')) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(DestructureDictionary, property.Key), property.Value, true, out var destructured)) properties.Add(destructured); } - else if (property.Key.StartsWith("$")) + else if (property.Key.StartsWith('$')) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(StringifyDictionary, property.Key), property.Value?.ToString(), true, out var stringified)) properties.Add(stringified); @@ -167,10 +167,10 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state static object? AsLoggableValue(TState state, Func? formatter) { - object? stateObj = state; + object? stateObj = null; if (formatter != null) stateObj = formatter(state, null); - return stateObj; + return stateObj ?? state; } internal static LogEventProperty CreateEventIdProperty(EventId eventId) From 76a05eeb420fce30f5417b77e73a048148ec2a69 Mon Sep 17 00:00:00 2001 From: jods Date: Tue, 13 Feb 2024 00:50:38 +0100 Subject: [PATCH 06/43] Revert String.StartsWith, not present in old .net fx --- .../Extensions/Logging/SerilogLogger.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 3aa7fb1..9a82609 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -103,12 +103,12 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state { messageTemplate = value; } - else if (property.Key.StartsWith('@')) + else if (property.Key.StartsWith("@")) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(DestructureDictionary, property.Key), property.Value, true, out var destructured)) properties.Add(destructured); } - else if (property.Key.StartsWith('$')) + else if (property.Key.StartsWith("$")) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(StringifyDictionary, property.Key), property.Value?.ToString(), true, out var stringified)) properties.Add(stringified); From 099a66a57ea6f46e8468f71e974aa9284f4334f1 Mon Sep 17 00:00:00 2001 From: David Hopkins Date: Wed, 21 Feb 2024 17:44:56 +0800 Subject: [PATCH 07/43] Document {SourceContext} for MEL log category --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index bbed04e..f052612 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,19 @@ That's it! With the level bumped up a little you should see log output like: [22:14:45.741 DBG] Handled. Status code: 304 File: /css/site.css ``` +### Including the log category in text-format sink output +All _Microsoft.Extensions.Logging.ILogger_ implementations are created with a specified [_log category_](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line#log-category) string, which is then attached as structured data to each log message created by that `ILogger` instance. Typically, the log category is the fully-qualified name of the class generating the log messages. This convention is implemented by the [`ILogger`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) interface, which is commonly used as an injected dependency in frameworks that use _Microsoft.Extensions.Logging_. + +_Serilog.Extensions.Logging_ captures the `ILogger`'s log category, but it's not included in the default output templates for text-based sinks, such as [Console](https://github.com/serilog/serilog-sinks-console), [File](https://github.com/serilog/serilog-sinks-file) and [Debug](https://github.com/serilog/serilog-sinks-debug). + +To include the log category in the final written messages, add the `{SourceContext}` named hole to a customised `outputTemplate` parameter value when configuring the relevant sink(s). For example: +```csharp + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}") + .WriteTo.File("log.txt", + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}") +``` + ### Notes on Log Scopes _Microsoft.Extensions.Logging_ provides the `BeginScope` API, which can be used to add arbitrary properties to log events within a certain region of code. The API comes in two forms: From 3e5c53c9ff87580cd9e60e70baca6192d7b7cbed Mon Sep 17 00:00:00 2001 From: David Obee Date: Wed, 13 Mar 2024 10:54:59 +0000 Subject: [PATCH 08/43] Add experiement project demonstrating that tags and baggage do not appear (while Span and Trace IDs do) --- .../ActivityProofOfConcept.csproj | 21 ++++++++ samples/ActivityProofOfConcept/Program.cs | 49 +++++++++++++++++++ serilog-extensions-logging.sln | 7 +++ 3 files changed, 77 insertions(+) create mode 100644 samples/ActivityProofOfConcept/ActivityProofOfConcept.csproj create mode 100644 samples/ActivityProofOfConcept/Program.cs diff --git a/samples/ActivityProofOfConcept/ActivityProofOfConcept.csproj b/samples/ActivityProofOfConcept/ActivityProofOfConcept.csproj new file mode 100644 index 0000000..045c31a --- /dev/null +++ b/samples/ActivityProofOfConcept/ActivityProofOfConcept.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + Exe + enable + + + + + + + + + + + + + diff --git a/samples/ActivityProofOfConcept/Program.cs b/samples/ActivityProofOfConcept/Program.cs new file mode 100644 index 0000000..6f8f303 --- /dev/null +++ b/samples/ActivityProofOfConcept/Program.cs @@ -0,0 +1,49 @@ +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Formatting.Json; + +// Configure a JsonFormatter to log out scope to the console +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console(new JsonFormatter()) + .CreateLogger(); + +// Setup Serilog with M.E.L, and configure the appropriate ActivityTrackingOptions +var services = new ServiceCollection(); + +services.AddLogging(l => l + .AddSerilog() + .Configure(options => + { + options.ActivityTrackingOptions = ActivityTrackingOptions.TraceId | ActivityTrackingOptions.Tags | ActivityTrackingOptions.Baggage; + })); + +// Add an ActivityListener (required, otherwise Activities don't actually get created if nothing is listening to them) +ActivitySource.AddActivityListener(new ActivityListener +{ + ShouldListenTo = source => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded +}); + +// Run our test +var activitySource = new ActivitySource("SomeActivitySource"); + +var serviceProvider = services.BuildServiceProvider(); +var logger = serviceProvider.GetRequiredService>(); + +using var activity = activitySource.StartActivity(); + +activity?.SetTag("tag.domain.id", 1234); +activity?.SetBaggage("baggage.environment", "uat"); + +using var scope = logger.BeginScope(new +{ + User = "Hugh Mann", + Time = DateTimeOffset.UtcNow +}); + +logger.LogInformation("Hello world!"); + +serviceProvider.Dispose(); diff --git a/serilog-extensions-logging.sln b/serilog-extensions-logging.sln index 776ae3c..b8313fb 100644 --- a/serilog-extensions-logging.sln +++ b/serilog-extensions-logging.sln @@ -31,6 +31,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{9C21B9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Extensions.Logging.Benchmarks", "test\Serilog.Extensions.Logging.Benchmarks\Serilog.Extensions.Logging.Benchmarks.csproj", "{6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActivityProofOfConcept", "samples\ActivityProofOfConcept\ActivityProofOfConcept.csproj", "{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,6 +55,10 @@ Global {6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}.Release|Any CPU.Build.0 = Release|Any CPU + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -62,6 +68,7 @@ Global {37EADF84-5E41-4224-A194-1E3299DCD0B8} = {E30F638E-BBBE-4AD1-93CE-48CC69CFEFE1} {65357FBC-9BC4-466D-B621-1C3A19BC2A78} = {F2407211-6043-439C-8E06-3641634332E7} {6D5986FF-EECD-4E75-8BC6-A5F78AB549B2} = {E30F638E-BBBE-4AD1-93CE-48CC69CFEFE1} + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2} = {F2407211-6043-439C-8E06-3641634332E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {811E61C5-3871-4633-AFAE-B35B619C8A10} From 239585269d1a8a655ec22830859ca63cf34616f5 Mon Sep 17 00:00:00 2001 From: David Obee Date: Wed, 13 Mar 2024 11:26:21 +0000 Subject: [PATCH 09/43] Implement ISupportExternalScope in SerilogLoggerProvider --- .../Logging/SerilogLoggerProvider.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs index 3fead38..5a603bc 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs @@ -13,7 +13,7 @@ namespace Serilog.Extensions.Logging; /// An that pipes events through Serilog. /// [ProviderAlias("Serilog")] -public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher +public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISupportExternalScope { internal const string OriginalFormatPropertyName = "{OriginalFormat}"; internal const string ScopePropertyName = "Scope"; @@ -21,6 +21,7 @@ public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher // May be null; if it is, Log.Logger will be lazily used readonly ILogger? _logger; readonly Action? _dispose; + private IExternalScopeProvider? _externalScopeProvider; /// /// Construct a . @@ -75,6 +76,19 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) } } + _externalScopeProvider?.ForEachScope((state, accumulatingLogEvent) => + { + var scope = new SerilogLoggerScope(this, state); + + scope.EnrichAndCreateScopeItem(accumulatingLogEvent, propertyFactory, out var scopeItem); + + if (scopeItem != null) + { + scopeItems ??= new List(); + scopeItems.Add(scopeItem); + } + }, logEvent); + if (scopeItems != null) { scopeItems.Reverse(); @@ -82,6 +96,12 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) } } + /// + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + _externalScopeProvider = scopeProvider; + } + readonly AsyncLocal _value = new(); internal SerilogLoggerScope? CurrentScope From e5d72ac4ab4f4ccced3fe6146be26ee23d1c316e Mon Sep 17 00:00:00 2001 From: David Obee Date: Wed, 13 Mar 2024 14:59:04 +0000 Subject: [PATCH 10/43] Add test of external scope provider support --- .../SerilogLoggerTests.cs | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs index 179fd56..84bba82 100644 --- a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs @@ -17,7 +17,7 @@ public class SerilogLoggerTest const string Name = "test"; const string TestMessage = "This is a test"; - static Tuple SetUp(LogLevel logLevel) + static Tuple SetUp(LogLevel logLevel, IExternalScopeProvider? externalScopeProvider = null) { var sink = new SerilogSink(); @@ -29,6 +29,11 @@ static Tuple SetUp(LogLevel logLevel) var provider = new SerilogLoggerProvider(serilogLogger); var logger = (SerilogLogger)provider.CreateLogger(Name); + if (externalScopeProvider is not null) + { + provider.SetScopeProvider(externalScopeProvider); + } + return new Tuple(logger, sink); } @@ -397,6 +402,35 @@ public void NamedScopesAreCaptured() Assert.Equal("Inner", items[1]); } + [Fact] + public void ExternalScopesAreCaptured() + { + var externalScopeProvider = new FakeExternalScopeProvider(); + var (logger, sink) = SetUp(LogLevel.Trace, externalScopeProvider); + + externalScopeProvider.Push(new Dictionary() + { + { "FirstKey", 1 }, + { "SecondKey", 2 } + }); + + var scopeObject = new { ObjectKey = "Some value" }; + externalScopeProvider.Push(scopeObject); + + logger.Log(LogLevel.Information, 0, TestMessage, null!, null!); + + Assert.Single(sink.Writes); + Assert.True(sink.Writes[0].Properties.TryGetValue(SerilogLoggerProvider.ScopePropertyName, out var scopeValue)); + var sequence = Assert.IsType(scopeValue); + + var objectScope = (ScalarValue) sequence.Elements.Single(e => e is ScalarValue); + Assert.Equal(scopeObject.ToString(), (string?)objectScope.Value); + + var dictionaryScope = (DictionaryValue) sequence.Elements.Single(e => e is DictionaryValue); + Assert.Equal(1, ((ScalarValue)dictionaryScope.Elements.Single(pair => pair.Key.Value!.Equals("FirstKey")).Value).Value); + Assert.Equal(2, ((ScalarValue)dictionaryScope.Elements.Single(pair => pair.Key.Value!.Equals("SecondKey")).Value).Value); + } + class FoodScope : IEnumerable> { readonly string _name; @@ -446,6 +480,43 @@ class Person public string? LastName { get; set; } } + class FakeExternalScopeProvider : IExternalScopeProvider + { + private readonly List _scopes = new List(); + + public void ForEachScope(Action callback, TState state) + { + foreach (var scope in _scopes) + { + if (scope.IsDisposed) continue; + callback(scope.Value, state); + } + } + + public IDisposable Push(object? state) + { + var scope = new Scope(state); + _scopes.Add(scope); + return scope; + } + + private class Scope : IDisposable + { + public bool IsDisposed { get; set; } = false; + public object? Value { get; set; } + + public Scope(object? value) + { + Value = value; + } + + public void Dispose() + { + IsDisposed = true; + } + } + } + [Theory] [InlineData(1)] [InlineData(10)] From fe4a9b9b52d1a97024fdb9f6aecc141c67da68e0 Mon Sep 17 00:00:00 2001 From: David Obee Date: Wed, 13 Mar 2024 15:04:08 +0000 Subject: [PATCH 11/43] Rename proof of concept sample app --- .../Program.cs | 0 .../SampleActivityLogging.csproj} | 0 serilog-extensions-logging.sln | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename samples/{ActivityProofOfConcept => SampleActivityLogging}/Program.cs (100%) rename samples/{ActivityProofOfConcept/ActivityProofOfConcept.csproj => SampleActivityLogging/SampleActivityLogging.csproj} (100%) diff --git a/samples/ActivityProofOfConcept/Program.cs b/samples/SampleActivityLogging/Program.cs similarity index 100% rename from samples/ActivityProofOfConcept/Program.cs rename to samples/SampleActivityLogging/Program.cs diff --git a/samples/ActivityProofOfConcept/ActivityProofOfConcept.csproj b/samples/SampleActivityLogging/SampleActivityLogging.csproj similarity index 100% rename from samples/ActivityProofOfConcept/ActivityProofOfConcept.csproj rename to samples/SampleActivityLogging/SampleActivityLogging.csproj diff --git a/serilog-extensions-logging.sln b/serilog-extensions-logging.sln index b8313fb..05bf2d6 100644 --- a/serilog-extensions-logging.sln +++ b/serilog-extensions-logging.sln @@ -31,7 +31,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{9C21B9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Extensions.Logging.Benchmarks", "test\Serilog.Extensions.Logging.Benchmarks\Serilog.Extensions.Logging.Benchmarks.csproj", "{6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActivityProofOfConcept", "samples\ActivityProofOfConcept\ActivityProofOfConcept.csproj", "{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleActivityLogging", "samples\SampleActivityLogging\SampleActivityLogging.csproj", "{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 65eabbf0f930507c2e506e9aece2918162043d5e Mon Sep 17 00:00:00 2001 From: David Obee Date: Wed, 13 Mar 2024 15:33:06 +0000 Subject: [PATCH 12/43] Use all ActivityTrackingOptions in sample app --- samples/SampleActivityLogging/Program.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/samples/SampleActivityLogging/Program.cs b/samples/SampleActivityLogging/Program.cs index 6f8f303..3a1a018 100644 --- a/samples/SampleActivityLogging/Program.cs +++ b/samples/SampleActivityLogging/Program.cs @@ -17,7 +17,14 @@ .AddSerilog() .Configure(options => { - options.ActivityTrackingOptions = ActivityTrackingOptions.TraceId | ActivityTrackingOptions.Tags | ActivityTrackingOptions.Baggage; + options.ActivityTrackingOptions = + ActivityTrackingOptions.SpanId + | ActivityTrackingOptions.TraceId + | ActivityTrackingOptions.ParentId + | ActivityTrackingOptions.TraceState + | ActivityTrackingOptions.TraceFlags + | ActivityTrackingOptions.Tags + | ActivityTrackingOptions.Baggage; })); // Add an ActivityListener (required, otherwise Activities don't actually get created if nothing is listening to them) From 49fa3e984f59af5d383f2cf7bf23dbe2a08c5882 Mon Sep 17 00:00:00 2001 From: David Obee Date: Thu, 14 Mar 2024 10:34:44 +0000 Subject: [PATCH 13/43] Rename sample to SampleWithExternalScope --- .../Program.cs | 0 .../SampleWithExternalScope.csproj} | 0 serilog-extensions-logging.sln | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename samples/{SampleActivityLogging => SampleWithExternalScope}/Program.cs (100%) rename samples/{SampleActivityLogging/SampleActivityLogging.csproj => SampleWithExternalScope/SampleWithExternalScope.csproj} (100%) diff --git a/samples/SampleActivityLogging/Program.cs b/samples/SampleWithExternalScope/Program.cs similarity index 100% rename from samples/SampleActivityLogging/Program.cs rename to samples/SampleWithExternalScope/Program.cs diff --git a/samples/SampleActivityLogging/SampleActivityLogging.csproj b/samples/SampleWithExternalScope/SampleWithExternalScope.csproj similarity index 100% rename from samples/SampleActivityLogging/SampleActivityLogging.csproj rename to samples/SampleWithExternalScope/SampleWithExternalScope.csproj diff --git a/serilog-extensions-logging.sln b/serilog-extensions-logging.sln index 05bf2d6..d0d5cb9 100644 --- a/serilog-extensions-logging.sln +++ b/serilog-extensions-logging.sln @@ -31,7 +31,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{9C21B9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Extensions.Logging.Benchmarks", "test\Serilog.Extensions.Logging.Benchmarks\Serilog.Extensions.Logging.Benchmarks.csproj", "{6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleActivityLogging", "samples\SampleActivityLogging\SampleActivityLogging.csproj", "{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWithExternalScope", "samples\SampleWithExternalScope\SampleWithExternalScope.csproj", "{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 6fd2e729cc00c76d173d665657bda0f89c195dfa Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 15 Mar 2024 09:17:54 +1000 Subject: [PATCH 14/43] Publishing key update --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index ae5bee4..b7e0f44 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,7 @@ artifacts: deploy: - provider: NuGet api_key: - secure: EN9f+XXE3fW+ebL4wxrIbafdtbNvRfddBN8UUixvctYh4qMBHzr1JdnM83QsM1zo + secure: H96ajkMxwIafhF2vrr+UAUS10bFcAL/1wc3iphidRiYi9WoTc2i8shTLtF+75ODb skip_symbols: true on: branch: /^(main|dev)$/ From 62b775b48084b6ff1227617665b898e4cf7ee0ce Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 15 Mar 2024 11:04:16 +1000 Subject: [PATCH 15/43] Move AddProperty() out to avoid a closure allocation --- serilog-extensions-logging.sln.DotSettings | 1 + .../Logging/SerilogLoggerProvider.cs | 2 +- .../Extensions/Logging/SerilogLoggerScope.cs | 44 +++++++++---------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/serilog-extensions-logging.sln.DotSettings b/serilog-extensions-logging.sln.DotSettings index 23a0f99..6fd2e62 100644 --- a/serilog-extensions-logging.sln.DotSettings +++ b/serilog-extensions-logging.sln.DotSettings @@ -1,2 +1,3 @@  + True True \ No newline at end of file diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs index 5a603bc..115a8b0 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs @@ -21,7 +21,7 @@ public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISuppor // May be null; if it is, Log.Logger will be lazily used readonly ILogger? _logger; readonly Action? _dispose; - private IExternalScopeProvider? _externalScopeProvider; + IExternalScopeProvider? _externalScopeProvider; /// /// Construct a . diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs index 4d4a2c1..25d2ae7 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs @@ -49,25 +49,6 @@ public void Dispose() public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, out LogEventPropertyValue? scopeItem) { - void AddProperty(string key, object? value) - { - var destructureObject = false; - - if (key.StartsWith("@")) - { - key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.DestructureDictionary, key); - destructureObject = true; - } - else if (key.StartsWith("$")) - { - key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.StringifyDictionary, key); - value = value?.ToString(); - } - - var property = propertyFactory.CreateProperty(key, value, destructureObject); - logEvent.AddPropertyIfAbsent(property); - } - if (_state == null) { scopeItem = null; @@ -84,7 +65,7 @@ void AddProperty(string key, object? value) if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) scopeItem = new ScalarValue(_state.ToString()); else - AddProperty(stateProperty.Key, stateProperty.Value); + AddProperty(logEvent, propertyFactory, stateProperty.Key, stateProperty.Value); } } else if (_state is IEnumerable> stateProperties) @@ -96,7 +77,7 @@ void AddProperty(string key, object? value) if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) scopeItem = new ScalarValue(_state.ToString()); else - AddProperty(stateProperty.Key, stateProperty.Value); + AddProperty(logEvent, propertyFactory, stateProperty.Key, stateProperty.Value); } } else if (_state is ValueTuple tuple) @@ -106,11 +87,30 @@ void AddProperty(string key, object? value) if (tuple.Item1 == SerilogLoggerProvider.OriginalFormatPropertyName && tuple.Item2 is string) scopeItem = new ScalarValue(_state.ToString()); else - AddProperty(tuple.Item1, tuple.Item2); + AddProperty(logEvent, propertyFactory, tuple.Item1, tuple.Item2); } else { scopeItem = propertyFactory.CreateProperty(NoName, _state).Value; } } + + static void AddProperty(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, string key, object? value) + { + var destructureObject = false; + + if (key.StartsWith("@")) + { + key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.DestructureDictionary, key); + destructureObject = true; + } + else if (key.StartsWith("$")) + { + key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.StringifyDictionary, key); + value = value?.ToString(); + } + + var property = propertyFactory.CreateProperty(key, value, destructureObject); + logEvent.AddPropertyIfAbsent(property); + } } From f8452370e5779fe7a6de115eb3497bf254015d3e Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 15 Mar 2024 11:08:51 +1000 Subject: [PATCH 16/43] It's not necessary to handle {OriginalFormat} when TState is Dictionary or (K,V). --- .../Extensions/Logging/SerilogLoggerScope.cs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs index 25d2ae7..af76248 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs @@ -55,39 +55,35 @@ public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory return; } - // Eliminates boxing of Dictionary.Enumerator for the most common use case if (_state is Dictionary dictionary) { - scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. - + // Separate handling of this case eliminates boxing of Dictionary.Enumerator. + scopeItem = null; foreach (var stateProperty in dictionary) { - if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) - scopeItem = new ScalarValue(_state.ToString()); - else - AddProperty(logEvent, propertyFactory, stateProperty.Key, stateProperty.Value); + AddProperty(logEvent, propertyFactory, stateProperty.Key, stateProperty.Value); } } else if (_state is IEnumerable> stateProperties) { - scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. - + scopeItem = null; foreach (var stateProperty in stateProperties) { - if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) + if (stateProperty is { Key: SerilogLoggerProvider.OriginalFormatPropertyName, Value: string }) + { + // `_state` is most likely `FormattedLogValues` (a MEL internal type). scopeItem = new ScalarValue(_state.ToString()); + } else + { AddProperty(logEvent, propertyFactory, stateProperty.Key, stateProperty.Value); + } } } else if (_state is ValueTuple tuple) { - scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. - - if (tuple.Item1 == SerilogLoggerProvider.OriginalFormatPropertyName && tuple.Item2 is string) - scopeItem = new ScalarValue(_state.ToString()); - else - AddProperty(logEvent, propertyFactory, tuple.Item1, tuple.Item2); + scopeItem = null; + AddProperty(logEvent, propertyFactory, tuple.Item1, tuple.Item2); } else { From 5864a5db432fc0c314499bf9151a0045526e844a Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 15 Mar 2024 11:16:15 +1000 Subject: [PATCH 17/43] Use LogEvent.UnstableAssembleFromParts() to avoid a dictionary alloc --- .../Extensions/Logging/SerilogLogger.cs | 22 +++++++++---------- .../Serilog.Extensions.Logging.csproj | 2 +- .../LogEventBenchmark.cs | 6 ++--- .../SerilogLoggerTests.cs | 7 +++--- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 9a82609..45399bc 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -93,30 +93,30 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state { string? messageTemplate = null; - var properties = new List(); + var properties = new Dictionary(); if (state is IEnumerable> structure) { foreach (var property in structure) { - if (property.Key == SerilogLoggerProvider.OriginalFormatPropertyName && property.Value is string value) + if (property is { Key: SerilogLoggerProvider.OriginalFormatPropertyName, Value: string value }) { messageTemplate = value; } else if (property.Key.StartsWith("@")) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(DestructureDictionary, property.Key), property.Value, true, out var destructured)) - properties.Add(destructured); + properties.Add(destructured.Name, destructured.Value); } else if (property.Key.StartsWith("$")) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(StringifyDictionary, property.Key), property.Value?.ToString(), true, out var stringified)) - properties.Add(stringified); + properties.Add(stringified.Name, stringified.Value); } else { if (_logger.BindProperty(property.Key, property.Value, false, out var bound)) - properties.Add(bound); + properties.Add(bound.Name, bound.Value); } } @@ -127,7 +127,7 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state { messageTemplate = "{" + stateType.Name + ":l}"; if (_logger.BindProperty(stateType.Name, AsLoggableValue(state, formatter), false, out var stateTypeProperty)) - properties.Add(stateTypeProperty); + properties.Add(stateTypeProperty.Name, stateTypeProperty.Value); } } @@ -150,19 +150,19 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state if (propertyName != null) { if (_logger.BindProperty(propertyName, AsLoggableValue(state, formatter!), false, out var property)) - properties.Add(property); + properties.Add(property.Name, property.Value); } } if (eventId.Id != 0 || eventId.Name != null) - properties.Add(CreateEventIdProperty(eventId)); + properties.Add("EventId", CreateEventIdPropertyValue(eventId)); var (traceId, spanId) = Activity.Current is { } activity ? (activity.TraceId, activity.SpanId) : (default(ActivityTraceId), default(ActivitySpanId)); var parsedTemplate = MessageTemplateParser.Parse(messageTemplate ?? ""); - return new LogEvent(DateTimeOffset.Now, level, exception, parsedTemplate, properties, traceId, spanId); + return LogEvent.UnstableAssembleFromParts(DateTimeOffset.Now, level, exception, parsedTemplate, properties, traceId, spanId); } static object? AsLoggableValue(TState state, Func? formatter) @@ -173,7 +173,7 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state return stateObj ?? state; } - internal static LogEventProperty CreateEventIdProperty(EventId eventId) + internal static StructureValue CreateEventIdPropertyValue(EventId eventId) { var properties = new List(2); @@ -191,6 +191,6 @@ internal static LogEventProperty CreateEventIdProperty(EventId eventId) properties.Add(new LogEventProperty("Name", new ScalarValue(eventId.Name))); } - return new LogEventProperty("EventId", new StructureValue(properties)); + return new StructureValue(properties); } } diff --git a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj index a197a45..f63b130 100644 --- a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj +++ b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj @@ -29,7 +29,7 @@ - + diff --git a/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs b/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs index 995d462..4c37ae6 100644 --- a/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs +++ b/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs @@ -25,7 +25,7 @@ namespace Serilog.Extensions.Logging.Benchmarks [MemoryDiagnoser] public class LogEventBenchmark { - private class Person + class Person { public string? Name { get; set; } public int Age { get; set; } @@ -58,8 +58,8 @@ public void LogInformation() [Benchmark] public void LogInformationScoped() { - using (var scope = _melLogger.BeginScope("Hi {@User} from {$Me}", _bob, _alice)) - _melLogger.LogInformation("Hi"); + using var scope = _melLogger.BeginScope("Hi {@User} from {$Me}", _bob, _alice); + _melLogger.LogInformation("Hi"); } } } diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs index 84bba82..296eb62 100644 --- a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs @@ -500,9 +500,9 @@ public IDisposable Push(object? state) return scope; } - private class Scope : IDisposable + class Scope : IDisposable { - public bool IsDisposed { get; set; } = false; + public bool IsDisposed { get; set; } public object? Value { get; set; } public Scope(object? value) @@ -525,8 +525,7 @@ public void Dispose() public void LowAndHighNumberedEventIdsAreMapped(int id) { var orig = new EventId(id, "test"); - var mapped = SerilogLogger.CreateEventIdProperty(orig); - var value = Assert.IsType(mapped.Value); + var value = SerilogLogger.CreateEventIdPropertyValue(orig); Assert.Equal(2, value.Properties.Count); var idValue = value.Properties.Single(p => p.Name == "Id").Value; var scalar = Assert.IsType(idValue); From 1c77935b512918d95abbe90b695e2c2d9bf4352d Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 15 Mar 2024 12:16:08 +1000 Subject: [PATCH 18/43] Update BenchmarkDotNet --- .../Serilog.Extensions.Logging.Benchmarks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj b/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj index 155847e..56b4b50 100644 --- a/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj +++ b/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj @@ -13,7 +13,7 @@ - + From 03fcf95497eb38242980b43c6de3d879be3fee9d Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 15 Mar 2024 13:04:47 +1000 Subject: [PATCH 19/43] "Improve LogEventBenchmark benchmarks": --- .../Extensions/Logging/SerilogLogger.cs | 7 +- .../LogEventBenchmark.cs | 125 ++++++++++++------ 2 files changed, 91 insertions(+), 41 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 45399bc..23b3f1a 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -115,7 +115,10 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state } else { - if (_logger.BindProperty(property.Key, property.Value, false, out var bound)) + // Simple micro-optimization for the most common and reliably scalar values; could go further here. + if (property.Value is null or string or int or long && LogEventProperty.IsValidName(property.Key)) + properties.Add(property.Key, new ScalarValue(property.Value)); + else if (_logger.BindProperty(property.Key, property.Value, false, out var bound)) properties.Add(bound.Name, bound.Value); } } @@ -154,7 +157,7 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state } } - if (eventId.Id != 0 || eventId.Name != null) + if (eventId != default) properties.Add("EventId", CreateEventIdPropertyValue(eventId)); var (traceId, spanId) = Activity.Current is { } activity ? diff --git a/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs b/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs index 4c37ae6..461e918 100644 --- a/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs +++ b/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs @@ -20,46 +20,93 @@ #pragma warning disable xUnit1013 // Public method should be marked as test -namespace Serilog.Extensions.Logging.Benchmarks +namespace Serilog.Extensions.Logging.Benchmarks; + +[MemoryDiagnoser] +public class LogEventBenchmark { - [MemoryDiagnoser] - public class LogEventBenchmark + class Person + { + public string? Name { get; set; } + public int Age { get; set; } + public override string ToString() => "Fixed text"; + } + + readonly IMelLogger _melLogger; + readonly Person _bob, _alice; + readonly ILogger _underlyingLogger; + + public LogEventBenchmark() + { + _underlyingLogger = new LoggerConfiguration().CreateLogger(); + _melLogger = new SerilogLoggerProvider(_underlyingLogger).CreateLogger(GetType().FullName!); + _bob = new Person { Name = "Bob", Age = 42 }; + _alice = new Person { Name = "Alice", Age = 42 }; + } + + [Fact] + public void Benchmark() + { + BenchmarkRunner.Run(); + } + + [Benchmark(Baseline = true)] + public void SerilogOnly() + { + _underlyingLogger.Information("Hello!"); + } + + [Benchmark] + public void SimpleEvent() + { + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void Template() + { + _melLogger.LogInformation("Hello, {Property1}!", 42); + } + + [Benchmark] + public void StringScope() + { + using var scope = _melLogger.BeginScope("Scope1"); + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void TemplateScope() + { + using var scope = _melLogger.BeginScope("Scope1 {Property1}", 42); + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void TupleScope() + { + using var scope = _melLogger.BeginScope(("Property1", 42)); + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void DictionaryScope() + { + // Note that allocations here include the dictionary and boxed int. + using var scope = _melLogger.BeginScope(new Dictionary { ["Property1"] = 42 }); + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void Capturing() + { + _melLogger.LogInformation("Hi {@User} from {$Me}", _bob, _alice); + } + + [Benchmark] + public void CapturingScope() { - class Person - { - public string? Name { get; set; } - public int Age { get; set; } - public override string ToString() => "Fixed text"; - } - - readonly IMelLogger _melLogger; - readonly Person _bob, _alice; - - public LogEventBenchmark() - { - var underlyingLogger = new LoggerConfiguration().CreateLogger(); - _melLogger = new SerilogLoggerProvider(underlyingLogger).CreateLogger(GetType().FullName!); - _bob = new Person { Name = "Bob", Age = 42 }; - _alice = new Person { Name = "Alice", Age = 42 }; - } - - [Fact] - public void Benchmark() - { - BenchmarkRunner.Run(); - } - - [Benchmark] - public void LogInformation() - { - _melLogger.LogInformation("Hi {@User} from {$Me}", _bob, _alice); - } - - [Benchmark] - public void LogInformationScoped() - { - using var scope = _melLogger.BeginScope("Hi {@User} from {$Me}", _bob, _alice); - _melLogger.LogInformation("Hi"); - } + using var scope = _melLogger.BeginScope("Hi {@User} from {$Me}", _bob, _alice); + _melLogger.LogInformation("Hi"); } } From bcc50219b6eed28acf53a1cd1fa26e441199c67d Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 18 Mar 2024 10:08:16 +1000 Subject: [PATCH 20/43] Ordinal string comparisons --- .../Extensions/Logging/SerilogLogger.cs | 4 ++-- .../Extensions/Logging/SerilogLoggerScope.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 23b3f1a..2937b14 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -103,12 +103,12 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state { messageTemplate = value; } - else if (property.Key.StartsWith("@")) + else if (property.Key.StartsWith("@", StringComparison.Ordinal)) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(DestructureDictionary, property.Key), property.Value, true, out var destructured)) properties.Add(destructured.Name, destructured.Value); } - else if (property.Key.StartsWith("$")) + else if (property.Key.StartsWith("$", StringComparison.Ordinal)) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(StringifyDictionary, property.Key), property.Value?.ToString(), true, out var stringified)) properties.Add(stringified.Name, stringified.Value); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs index af76248..4e14446 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs @@ -95,12 +95,12 @@ static void AddProperty(LogEvent logEvent, ILogEventPropertyFactory propertyFact { var destructureObject = false; - if (key.StartsWith("@")) + if (key.StartsWith("@", StringComparison.Ordinal)) { key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.DestructureDictionary, key); destructureObject = true; } - else if (key.StartsWith("$")) + else if (key.StartsWith("$", StringComparison.Ordinal)) { key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.StringifyDictionary, key); value = value?.ToString(); From ccae6ff98f15a88fe1c4b7804c79c997e8b258d5 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 18 Mar 2024 10:12:24 +1000 Subject: [PATCH 21/43] Remove some accidental usages of throwing Dictionary<,>.Add() --- .../Extensions/Logging/SerilogLogger.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 2937b14..218fa5e 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -106,20 +106,20 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state else if (property.Key.StartsWith("@", StringComparison.Ordinal)) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(DestructureDictionary, property.Key), property.Value, true, out var destructured)) - properties.Add(destructured.Name, destructured.Value); + properties[destructured.Name] = destructured.Value; } else if (property.Key.StartsWith("$", StringComparison.Ordinal)) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(StringifyDictionary, property.Key), property.Value?.ToString(), true, out var stringified)) - properties.Add(stringified.Name, stringified.Value); + properties[stringified.Name] = stringified.Value; } else { // Simple micro-optimization for the most common and reliably scalar values; could go further here. if (property.Value is null or string or int or long && LogEventProperty.IsValidName(property.Key)) - properties.Add(property.Key, new ScalarValue(property.Value)); + properties[property.Key] = new ScalarValue(property.Value); else if (_logger.BindProperty(property.Key, property.Value, false, out var bound)) - properties.Add(bound.Name, bound.Value); + properties[bound.Name] = bound.Value; } } @@ -130,7 +130,7 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state { messageTemplate = "{" + stateType.Name + ":l}"; if (_logger.BindProperty(stateType.Name, AsLoggableValue(state, formatter), false, out var stateTypeProperty)) - properties.Add(stateTypeProperty.Name, stateTypeProperty.Value); + properties[stateTypeProperty.Name] = stateTypeProperty.Value; } } @@ -153,18 +153,18 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state if (propertyName != null) { if (_logger.BindProperty(propertyName, AsLoggableValue(state, formatter!), false, out var property)) - properties.Add(property.Name, property.Value); + properties[property.Name] = property.Value; } } if (eventId != default) - properties.Add("EventId", CreateEventIdPropertyValue(eventId)); + properties["EventId"] = CreateEventIdPropertyValue(eventId); var (traceId, spanId) = Activity.Current is { } activity ? (activity.TraceId, activity.SpanId) : (default(ActivityTraceId), default(ActivitySpanId)); - var parsedTemplate = MessageTemplateParser.Parse(messageTemplate ?? ""); + var parsedTemplate = messageTemplate != null ? MessageTemplateParser.Parse(messageTemplate) : MessageTemplate.Empty; return LogEvent.UnstableAssembleFromParts(DateTimeOffset.Now, level, exception, parsedTemplate, properties, traceId, spanId); } From 7bdddecdee0622268a3e390ec363d10c95425ddc Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 18 Mar 2024 10:15:48 +1000 Subject: [PATCH 22/43] Roll back EventId comparison change; the overridden != operator does not compare names --- .../Extensions/Logging/SerilogLogger.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 218fa5e..3734e52 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -157,7 +157,8 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state } } - if (eventId != default) + // The overridden `!=` operator on this type ignores `Name`. + if (eventId.Id != 0 || eventId.Name != null) properties["EventId"] = CreateEventIdPropertyValue(eventId); var (traceId, spanId) = Activity.Current is { } activity ? From adf275fa7b11c339a2b5cfb62c9934052f6b123c Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 18 Mar 2024 11:05:39 +1000 Subject: [PATCH 23/43] Tighter level mapping, though for invalid cases only --- .../Extensions/Logging/LevelConvert.cs | 3 ++- ...structionBenchmark.cs => EventIdCapturingBenchmark.cs} | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) rename test/Serilog.Extensions.Logging.Benchmarks/{LogEventConstructionBenchmark.cs => EventIdCapturingBenchmark.cs} (92%) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/LevelConvert.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/LevelConvert.cs index a77494e..cce77da 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/LevelConvert.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/LevelConvert.cs @@ -36,7 +36,8 @@ public static LogEventLevel ToSerilogLevel(LogLevel logLevel) { return logLevel switch { - LogLevel.None or LogLevel.Critical => LogEventLevel.Fatal, + LogLevel.None => LevelAlias.Off, + LogLevel.Critical => LogEventLevel.Fatal, LogLevel.Error => LogEventLevel.Error, LogLevel.Warning => LogEventLevel.Warning, LogLevel.Information => LogEventLevel.Information, diff --git a/test/Serilog.Extensions.Logging.Benchmarks/LogEventConstructionBenchmark.cs b/test/Serilog.Extensions.Logging.Benchmarks/EventIdCapturingBenchmark.cs similarity index 92% rename from test/Serilog.Extensions.Logging.Benchmarks/LogEventConstructionBenchmark.cs rename to test/Serilog.Extensions.Logging.Benchmarks/EventIdCapturingBenchmark.cs index 415867a..a16f38f 100644 --- a/test/Serilog.Extensions.Logging.Benchmarks/LogEventConstructionBenchmark.cs +++ b/test/Serilog.Extensions.Logging.Benchmarks/EventIdCapturingBenchmark.cs @@ -23,7 +23,7 @@ namespace Serilog.Extensions.Logging.Benchmarks; [MemoryDiagnoser] -public class LogEventConstructionBenchmark +public class EventIdCapturingBenchmark { readonly IMelLogger _melLogger; readonly ILogger _serilogContextualLogger; @@ -31,11 +31,11 @@ public class LogEventConstructionBenchmark const int LowId = 10, HighId = 101; const string Template = "This is an event"; - public LogEventConstructionBenchmark() + public EventIdCapturingBenchmark() { _sink = new CapturingSink(); var underlyingLogger = new LoggerConfiguration().WriteTo.Sink(_sink).CreateLogger(); - _serilogContextualLogger = underlyingLogger.ForContext(); + _serilogContextualLogger = underlyingLogger.ForContext(); _melLogger = new SerilogLoggerProvider(underlyingLogger).CreateLogger(GetType().FullName!); } @@ -68,7 +68,7 @@ public void Verify() [Fact] public void Benchmark() { - BenchmarkRunner.Run(); + BenchmarkRunner.Run(); } [Benchmark(Baseline = true)] From 504fc5ca55bc1dc3860e63bd65eadd3dd15d25af Mon Sep 17 00:00:00 2001 From: Ivan Maximov Date: Mon, 25 Mar 2024 11:15:49 +0300 Subject: [PATCH 24/43] Support for ITuple --- README.md | 72 ++++++++++--------- .../Extensions/Logging/SerilogLoggerScope.cs | 12 ++++ .../Serilog.Extensions.Logging.csproj | 15 ++++ .../SerilogLoggerScopeTests.cs | 5 +- 4 files changed, 70 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 81884fa..ff1278c 100644 --- a/README.md +++ b/README.md @@ -28,41 +28,41 @@ using Serilog; public class Startup { - public Startup(IHostingEnvironment env) - { - Log.Logger = new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); - - // Other startup code + public Startup(IHostingEnvironment env) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + // Other startup code ``` **Finally, for .NET Core 2.0+**, in your `Startup` class's `Configure()` method, remove the existing logger configuration entries and call `AddSerilog()` on the provided `loggingBuilder`. ```csharp - public void ConfigureServices(IServiceCollection services) - { - services.AddLogging(loggingBuilder => - loggingBuilder.AddSerilog(dispose: true)); +public void ConfigureServices(IServiceCollection services) +{ + services.AddLogging(loggingBuilder => + loggingBuilder.AddSerilog(dispose: true)); - // Other services ... - } + // Other services ... +} ``` **For .NET Core 1.0 or 1.1**, in your `Startup` class's `Configure()` method, remove the existing logger configuration entries and call `AddSerilog()` on the provided `loggerFactory`. ``` - public void Configure(IApplicationBuilder app, - IHostingEnvironment env, - ILoggerFactory loggerfactory, - IApplicationLifetime appLifetime) - { - loggerfactory.AddSerilog(); - - // Ensure any buffered events are sent at shutdown - appLifetime.ApplicationStopped.Register(Log.CloseAndFlush); +public void Configure(IApplicationBuilder app, + IHostingEnvironment env, + ILoggerFactory loggerfactory, + IApplicationLifetime appLifetime) +{ + loggerfactory.AddSerilog(); + + // Ensure any buffered events are sent at shutdown + appLifetime.ApplicationStopped.Register(Log.CloseAndFlush); ``` That's it! With the level bumped up a little you should see log output like: @@ -87,10 +87,10 @@ _Serilog.Extensions.Logging_ captures the `ILogger`'s log category, but it's not To include the log category in the final written messages, add the `{SourceContext}` named hole to a customised `outputTemplate` parameter value when configuring the relevant sink(s). For example: ```csharp - .WriteTo.Console( - outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}") - .WriteTo.File("log.txt", - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}") +.WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}") +.WriteTo.File("log.txt", + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}") ``` ### Notes on Log Scopes @@ -103,7 +103,8 @@ _Microsoft.Extensions.Logging_ provides the `BeginScope` API, which can be used Using the extension method will add a `Scope` property to your log events. This is most useful for adding simple "scope strings" to your events, as in the following code: ```csharp -using (_logger.BeginScope("Transaction")) { +using (_logger.BeginScope("Transaction")) +{ _logger.LogInformation("Beginning..."); _logger.LogInformation("Completed in {DurationMs}ms...", 30); } @@ -116,7 +117,8 @@ If you simply want to add a "bag" of additional properties to your log events, h ```csharp // WRONG! Prefer the dictionary or value tuple approach below instead -using (_logger.BeginScope("TransactionId: {TransactionId}, ResponseJson: {ResponseJson}", 12345, jsonString)) { +using (_logger.BeginScope("TransactionId: {TransactionId}, ResponseJson: {ResponseJson}", 12345, jsonString)) +{ _logger.LogInformation("Completed in {DurationMs}ms...", 30); } // Example JSON output: @@ -138,11 +140,13 @@ Moreover, the template string within `BeginScope` is rather arbitrary when all y A far better alternative is to use the `BeginScope(TState state)` method. If you provide any `IEnumerable>` to this method, then Serilog will output the key/value pairs as structured properties _without_ the `Scope` property, as in this example: ```csharp -var scopeProps = new Dictionary { +var scopeProps = new Dictionary +{ { "TransactionId", 12345 }, { "ResponseJson", jsonString }, }; -using (_logger.BeginScope(scopeProps) { +using (_logger.BeginScope(scopeProps) +{ _logger.LogInformation("Transaction completed in {DurationMs}ms...", 30); } // Example JSON output: @@ -157,10 +161,12 @@ using (_logger.BeginScope(scopeProps) { // } ``` -Alternatively provide a `ValueTuple` to this method, where `Item1` is the property name and `Item2` is the property value. Note that `T2` _must_ be `object?`. +Alternatively provide a `ValueTuple` to this method, where `Item1` is the property name and `Item2` is the property value. +Note that `T2` _must_ be `object?` if your target platform is net462 or netstandard2.0. ```csharp -using (_logger.BeginScope(("TransactionId", (object?)12345)) { +using (_logger.BeginScope(("TransactionId", 12345)) +{ _logger.LogInformation("Transaction completed in {DurationMs}ms...", 30); } // Example JSON output: diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs index 4d4a2c1..69d5dc0 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs @@ -99,6 +99,17 @@ void AddProperty(string key, object? value) AddProperty(stateProperty.Key, stateProperty.Value); } } +#if FEATURE_ITUPLE + else if (_state is System.Runtime.CompilerServices.ITuple tuple && tuple.Length == 2 && tuple[0] is string s) + { + scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. + + if (s == SerilogLoggerProvider.OriginalFormatPropertyName && tuple[1] is string) + scopeItem = new ScalarValue(_state.ToString()); + else + AddProperty(s, tuple[1]); + } +#else else if (_state is ValueTuple tuple) { scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. @@ -108,6 +119,7 @@ void AddProperty(string key, object? value) else AddProperty(tuple.Item1, tuple.Item2); } +#endif else { scopeItem = propertyFactory.CreateProperty(NoName, _state).Value; diff --git a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj index a197a45..457818f 100644 --- a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj +++ b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj @@ -35,4 +35,19 @@ + + $(DefineConstants);FEATURE_ITUPLE + + + + $(DefineConstants);FEATURE_ITUPLE + + + + $(DefineConstants);FEATURE_ITUPLE + + + + $(DefineConstants);FEATURE_ITUPLE + diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs index 442d737..1a212c2 100644 --- a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs @@ -81,8 +81,11 @@ public void EnrichWithTupleStringObject() var (loggerProvider, logEventPropertyFactory, logEvent) = SetUp(); - +#if NET48 var state = (propertyName, (object)expectedValue); +#else + var state = (propertyName, expectedValue); +#endif var loggerScope = new SerilogLoggerScope(loggerProvider, state); From 849cb0230f6c3537ddde4ff3ed0d06b42572ad34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Falt=C3=BDnek?= Date: Wed, 22 May 2024 11:56:39 +0200 Subject: [PATCH 25/43] Make the enrichment from scope state static --- .../Extensions/Logging/SerilogLoggerScope.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs index 69d5dc0..baefd6d 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs @@ -47,7 +47,9 @@ public void Dispose() } } - public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, out LogEventPropertyValue? scopeItem) + public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, out LogEventPropertyValue? scopeItem) => EnrichWithStateAndCreateScopeItem(logEvent, propertyFactory, _state, out scopeItem); + + public static void EnrichWithStateAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, object? state, out LogEventPropertyValue? scopeItem) { void AddProperty(string key, object? value) { @@ -68,61 +70,61 @@ void AddProperty(string key, object? value) logEvent.AddPropertyIfAbsent(property); } - if (_state == null) + if (state == null) { scopeItem = null; return; } // Eliminates boxing of Dictionary.Enumerator for the most common use case - if (_state is Dictionary dictionary) + if (state is Dictionary dictionary) { scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. foreach (var stateProperty in dictionary) { if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) - scopeItem = new ScalarValue(_state.ToString()); + scopeItem = new ScalarValue(state.ToString()); else AddProperty(stateProperty.Key, stateProperty.Value); } } - else if (_state is IEnumerable> stateProperties) + else if (state is IEnumerable> stateProperties) { scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. foreach (var stateProperty in stateProperties) { if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) - scopeItem = new ScalarValue(_state.ToString()); + scopeItem = new ScalarValue(state.ToString()); else AddProperty(stateProperty.Key, stateProperty.Value); } } #if FEATURE_ITUPLE - else if (_state is System.Runtime.CompilerServices.ITuple tuple && tuple.Length == 2 && tuple[0] is string s) + else if (state is System.Runtime.CompilerServices.ITuple tuple && tuple.Length == 2 && tuple[0] is string s) { scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. if (s == SerilogLoggerProvider.OriginalFormatPropertyName && tuple[1] is string) - scopeItem = new ScalarValue(_state.ToString()); + scopeItem = new ScalarValue(state.ToString()); else AddProperty(s, tuple[1]); } #else - else if (_state is ValueTuple tuple) + else if (state is ValueTuple tuple) { scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. if (tuple.Item1 == SerilogLoggerProvider.OriginalFormatPropertyName && tuple.Item2 is string) - scopeItem = new ScalarValue(_state.ToString()); + scopeItem = new ScalarValue(state.ToString()); else AddProperty(tuple.Item1, tuple.Item2); } #endif else { - scopeItem = propertyFactory.CreateProperty(NoName, _state).Value; + scopeItem = propertyFactory.CreateProperty(NoName, state).Value; } } } From 8bcc366f0d7b3c0cedfd39dc2b14fd7524933851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Falt=C3=BDnek?= Date: Wed, 22 May 2024 11:56:59 +0200 Subject: [PATCH 26/43] For external scope provider use enrichment with no side effects --- .../Extensions/Logging/SerilogLoggerProvider.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs index 5a603bc..b8563cb 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs @@ -78,9 +78,7 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) _externalScopeProvider?.ForEachScope((state, accumulatingLogEvent) => { - var scope = new SerilogLoggerScope(this, state); - - scope.EnrichAndCreateScopeItem(accumulatingLogEvent, propertyFactory, out var scopeItem); + SerilogLoggerScope.EnrichWithStateAndCreateScopeItem(accumulatingLogEvent, propertyFactory, state, out var scopeItem); if (scopeItem != null) { From 3dd50b200d1be775a452dd9b13c4037505106c7e Mon Sep 17 00:00:00 2001 From: epeshk <8140483+epeshk@users.noreply.github.com> Date: Wed, 17 Jul 2024 19:23:12 +0300 Subject: [PATCH 27/43] Optimization: used StartsWith(char) instead of StartsWith(string) where supported --- .../Extensions/Logging/SerilogLogger.cs | 4 ++-- .../Extensions/Logging/SerilogLoggerScope.cs | 4 ++-- .../Extensions/StringExtensions.cs | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 9a82609..3aa7fb1 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -103,12 +103,12 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state { messageTemplate = value; } - else if (property.Key.StartsWith("@")) + else if (property.Key.StartsWith('@')) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(DestructureDictionary, property.Key), property.Value, true, out var destructured)) properties.Add(destructured); } - else if (property.Key.StartsWith("$")) + else if (property.Key.StartsWith('$')) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(StringifyDictionary, property.Key), property.Value?.ToString(), true, out var stringified)) properties.Add(stringified); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs index baefd6d..ca16691 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs @@ -55,12 +55,12 @@ void AddProperty(string key, object? value) { var destructureObject = false; - if (key.StartsWith("@")) + if (key.StartsWith('@')) { key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.DestructureDictionary, key); destructureObject = true; } - else if (key.StartsWith("$")) + else if (key.StartsWith('$')) { key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.StringifyDictionary, key); value = value?.ToString(); diff --git a/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs b/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs new file mode 100644 index 0000000..0e072b9 --- /dev/null +++ b/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs @@ -0,0 +1,14 @@ +using System.Runtime.CompilerServices; + +namespace Serilog.Extensions; + +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER +static class StringExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool StartsWith(this string str, char value) + { + return str.Length > 0 && str[0] == value; + } +} +#endif From d0dbbb1f45c3bd2bc2ea46d9c3fbadadb39cf951 Mon Sep 17 00:00:00 2001 From: Andrii Babanin Date: Thu, 31 Oct 2024 22:48:28 +0200 Subject: [PATCH 28/43] Extend LogEventBenchmark with ILogger.Log that includes EventId. --- .../LogEventBenchmark.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs b/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs index 995d462..6393348 100644 --- a/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs +++ b/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs @@ -34,6 +34,7 @@ private class Person readonly IMelLogger _melLogger; readonly Person _bob, _alice; + readonly EventId _eventId = new EventId(1, "Test"); public LogEventBenchmark() { @@ -61,5 +62,16 @@ public void LogInformationScoped() using (var scope = _melLogger.BeginScope("Hi {@User} from {$Me}", _bob, _alice)) _melLogger.LogInformation("Hi"); } + + [Benchmark] + public void LogInformation_WithEventId() + { + this._melLogger.Log( + LogLevel.Information, + _eventId, + "Hi {@User} from {$Me}", + _bob, + _alice); + } } } From 0e5fe4c91b00fa191ed2cf2e694592f81a7df7ac Mon Sep 17 00:00:00 2001 From: Andrii Babanin Date: Thu, 31 Oct 2024 23:00:14 +0200 Subject: [PATCH 29/43] Implement EventId to LogEventProperty cache. --- .../Logging/EventIdPropertyCache.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs new file mode 100644 index 0000000..5f4df3f --- /dev/null +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Serilog.Extensions.Logging +{ + using Microsoft.Extensions.Logging; + using Serilog.Events; + + internal sealed class EventIdPropertyCache + { + private readonly object _createLock = new(); + private readonly int _maxCapacity; + private readonly Dictionary _propertyCache; + + private int count; + + public EventIdPropertyCache(int maxCapacity) + { + this._maxCapacity = maxCapacity; + this._propertyCache = new Dictionary(capacity: maxCapacity); + } + + public LogEventProperty GetOrCreateProperty(in EventId eventId) + { + if (_propertyCache.TryGetValue(eventId.Id, out var cachedProperty)) + { + return cachedProperty; + } + + lock (_createLock) + { + return GetOrCreateSynchronized(in eventId); + } + } + + private static LogEventProperty CreateCore(in EventId eventId) + { + var properties = new List(2); + + if (eventId.Id != 0) + { + properties.Add(new LogEventProperty("Id", new ScalarValue(eventId.Id))); + } + + if (eventId.Name != null) + { + properties.Add(new LogEventProperty("Name", new ScalarValue(eventId.Name))); + } + + return new LogEventProperty("EventId", new StructureValue(properties)); + } + + private LogEventProperty GetOrCreateSynchronized(in EventId eventId) + { + // Double check under lock + if (_propertyCache.TryGetValue(eventId.Id, out var cachedProperty)) + { + return cachedProperty; + } + + cachedProperty = CreateCore(in eventId); + + if (count < _maxCapacity) + { + _propertyCache[eventId.Id] = cachedProperty; + count++; + } + + return cachedProperty; + } + } +} From 4eb8b3c4997a2c192979659aaabf4788a53f8bff Mon Sep 17 00:00:00 2001 From: Andrii Babanin Date: Thu, 31 Oct 2024 23:15:41 +0200 Subject: [PATCH 30/43] Use EventIdPropertyCache in SerilogLogger; Create EventIdPropertyCache test class. --- .../Extensions/Logging/SerilogLogger.cs | 33 ++++------------- .../EventIdPropertyCacheTests.cs | 36 +++++++++++++++++++ .../SerilogLoggerTests.cs | 16 --------- 3 files changed, 42 insertions(+), 43 deletions(-) create mode 100644 test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 3aa7fb1..2d1a4dc 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -29,12 +29,12 @@ internal static string GetKeyWithoutFirstSymbol(ConcurrentDictionary new LogEventProperty("Id", new ScalarValue(n))) - .ToArray(); + static readonly CachingMessageTemplateParser MessageTemplateParser = new(); public SerilogLogger( SerilogLoggerProvider provider, @@ -155,7 +155,7 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state } if (eventId.Id != 0 || eventId.Name != null) - properties.Add(CreateEventIdProperty(eventId)); + properties.Add(this._eventPropertyCache.GetOrCreateProperty(in eventId)); var (traceId, spanId) = Activity.Current is { } activity ? (activity.TraceId, activity.SpanId) : @@ -172,25 +172,4 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state stateObj = formatter(state, null); return stateObj ?? state; } - - internal static LogEventProperty CreateEventIdProperty(EventId eventId) - { - var properties = new List(2); - - if (eventId.Id != 0) - { - if (eventId.Id >= 0 && eventId.Id < LowEventIdValues.Length) - // Avoid some allocations - properties.Add(LowEventIdValues[eventId.Id]); - else - properties.Add(new LogEventProperty("Id", new ScalarValue(eventId.Id))); - } - - if (eventId.Name != null) - { - properties.Add(new LogEventProperty("Name", new ScalarValue(eventId.Name))); - } - - return new LogEventProperty("EventId", new StructureValue(properties)); - } } diff --git a/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs new file mode 100644 index 0000000..4295dcb --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Logging; +using Serilog.Events; +using Xunit; + +namespace Serilog.Extensions.Logging.Tests +{ + public class EventIdPropertyCacheTests + { + [Theory] + [InlineData(1)] + [InlineData(10)] + [InlineData(48)] + [InlineData(100)] + public void LowAndHighNumberedEventIdsAreMapped(int id) + { + // Arrange + var cache = new EventIdPropertyCache(48); + + var eventId = new EventId(id, "test"); + + // Act + var mapped = cache.GetOrCreateProperty(eventId); + + // Assert + var value = Assert.IsType(mapped.Value); + Assert.Equal(2, value.Properties.Count); + + var idValue = value.Properties.Single(p => p.Name == "Id").Value; + var scalar = Assert.IsType(idValue); + Assert.Equal(id, scalar.Value); + } + } +} diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs index 84bba82..b23f8f9 100644 --- a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs @@ -517,22 +517,6 @@ public void Dispose() } } - [Theory] - [InlineData(1)] - [InlineData(10)] - [InlineData(48)] - [InlineData(100)] - public void LowAndHighNumberedEventIdsAreMapped(int id) - { - var orig = new EventId(id, "test"); - var mapped = SerilogLogger.CreateEventIdProperty(orig); - var value = Assert.IsType(mapped.Value); - Assert.Equal(2, value.Properties.Count); - var idValue = value.Properties.Single(p => p.Name == "Id").Value; - var scalar = Assert.IsType(idValue); - Assert.Equal(id, scalar.Value); - } - [Fact] public void MismatchedMessageTemplateParameterCountIsHandled() { From 919b919bb3705a1966858a21dcb4719999aac39e Mon Sep 17 00:00:00 2001 From: Andrii Babanin Date: Thu, 31 Oct 2024 23:56:35 +0200 Subject: [PATCH 31/43] Compare both Id and Name for equity. --- .../Logging/EventIdPropertyCache.cs | 71 +++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs index 5f4df3f..df6631d 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs @@ -10,63 +10,94 @@ internal sealed class EventIdPropertyCache { private readonly object _createLock = new(); private readonly int _maxCapacity; - private readonly Dictionary _propertyCache; + private readonly Dictionary _propertyCache; private int count; public EventIdPropertyCache(int maxCapacity) { - this._maxCapacity = maxCapacity; - this._propertyCache = new Dictionary(capacity: maxCapacity); + _maxCapacity = maxCapacity; + _propertyCache = new Dictionary(capacity: maxCapacity); } public LogEventProperty GetOrCreateProperty(in EventId eventId) { - if (_propertyCache.TryGetValue(eventId.Id, out var cachedProperty)) + var eventKey = new EventKey(eventId); + + if (_propertyCache.TryGetValue(eventKey, out var cachedProperty)) { return cachedProperty; } lock (_createLock) { - return GetOrCreateSynchronized(in eventId); + // Double check under lock + if (_propertyCache.TryGetValue(eventKey, out cachedProperty)) + { + return cachedProperty; + } + + cachedProperty = CreateCore(in eventKey); + + if (count < _maxCapacity) + { + _propertyCache[eventKey] = cachedProperty; + count++; + } + + return cachedProperty; } } - private static LogEventProperty CreateCore(in EventId eventId) + private static LogEventProperty CreateCore(in EventKey eventKey) { var properties = new List(2); - if (eventId.Id != 0) + if (eventKey.Id != 0) { - properties.Add(new LogEventProperty("Id", new ScalarValue(eventId.Id))); + properties.Add(new LogEventProperty("Id", new ScalarValue(eventKey.Id))); } - if (eventId.Name != null) + if (eventKey.Name != null) { - properties.Add(new LogEventProperty("Name", new ScalarValue(eventId.Name))); + properties.Add(new LogEventProperty("Name", new ScalarValue(eventKey.Name))); } return new LogEventProperty("EventId", new StructureValue(properties)); } - private LogEventProperty GetOrCreateSynchronized(in EventId eventId) + private readonly struct EventKey : IEquatable { - // Double check under lock - if (_propertyCache.TryGetValue(eventId.Id, out var cachedProperty)) + + public EventKey(EventId eventId) { - return cachedProperty; + Id = eventId.Id; + Name = eventId.Name; } - cachedProperty = CreateCore(in eventId); + public int Id { get; } - if (count < _maxCapacity) + public string? Name { get; } + + /// + public override int GetHashCode() { - _propertyCache[eventId.Id] = cachedProperty; - count++; + unchecked + { + var hashCode = 17; + + hashCode = (hashCode * 397) ^ this.Id; + hashCode = (hashCode * 397) ^ (this.Name?.GetHashCode() ?? 0); + + return hashCode; + } } - - return cachedProperty; + + /// + public bool Equals(EventKey other) => this.Id == other.Id && this.Name == other.Name; + + /// + public override bool Equals(object? obj) => obj is EventKey other && Equals(other); } } } From b68c8847c6873968eb2f37c46744b51018143659 Mon Sep 17 00:00:00 2001 From: Andrii Babanin Date: Fri, 1 Nov 2024 00:00:31 +0200 Subject: [PATCH 32/43] Remove comment. --- .../Extensions/Logging/SerilogLogger.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 2d1a4dc..7370afc 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -29,9 +29,6 @@ internal static string GetKeyWithoutFirstSymbol(ConcurrentDictionary Date: Fri, 1 Nov 2024 11:27:41 +0200 Subject: [PATCH 33/43] Define EventKey as readonly record struct. --- samples/Sample/Program.cs | 39 +++++-------------- .../Logging/EventIdPropertyCache.cs | 22 +---------- 2 files changed, 10 insertions(+), 51 deletions(-) diff --git a/samples/Sample/Program.cs b/samples/Sample/Program.cs index d70b9aa..1abc00b 100644 --- a/samples/Sample/Program.cs +++ b/samples/Sample/Program.cs @@ -32,37 +32,16 @@ var serviceProvider = services.BuildServiceProvider(); var logger = serviceProvider.GetRequiredService>(); -var startTime = DateTimeOffset.UtcNow; -logger.LogInformation(1, "Started at {StartTime} and 0x{Hello:X} is hex of 42", startTime, 42); +var eventId = new EventId(1001, "Test"); -try +for (int i = 0; i < 1_000; i++) { - throw new Exception("Boom!"); + logger.Log( + LogLevel.Information, + eventId, + "Subscription {SubscriptionId} for entity {EntityName} handler for message {MessageId} has been successfully completed.", + "my-subscription-id", + "TestQueue", + 1); } -catch (Exception ex) -{ - logger.LogCritical(ex, "Unexpected critical error starting application"); - logger.Log(LogLevel.Critical, 0, "Unexpected critical error", ex, null!); - // This write should not log anything - logger.Log(LogLevel.Critical, 0, null!, null, null!); - logger.LogError(ex, "Unexpected error"); - logger.LogWarning(ex, "Unexpected warning"); -} - -using (logger.BeginScope("Main")) -{ - logger.LogInformation("Waiting for user input"); - var key = Console.Read(); - logger.LogInformation("User pressed {@KeyInfo}", new { Key = key, KeyChar = (char)key }); -} - -var endTime = DateTimeOffset.UtcNow; -logger.LogInformation(2, "Stopping at {StopTime}", endTime); - -logger.LogInformation("Stopping"); - -logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "RESULT", "START TIME", "END TIME", "DURATION(ms)"); -logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "------", "----- ----", "--- ----", "------------"); -logger.LogInformation("{Result,-10:l}{StartTime,15:mm:s tt}{EndTime,15:mm:s tt}{Duration,15}", "SUCCESS", startTime, endTime, (endTime - startTime).TotalMilliseconds); - serviceProvider.Dispose(); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs index df6631d..989b3b4 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs @@ -66,7 +66,7 @@ private static LogEventProperty CreateCore(in EventKey eventKey) return new LogEventProperty("EventId", new StructureValue(properties)); } - private readonly struct EventKey : IEquatable + private readonly record struct EventKey { public EventKey(EventId eventId) @@ -78,26 +78,6 @@ public EventKey(EventId eventId) public int Id { get; } public string? Name { get; } - - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 17; - - hashCode = (hashCode * 397) ^ this.Id; - hashCode = (hashCode * 397) ^ (this.Name?.GetHashCode() ?? 0); - - return hashCode; - } - } - - /// - public bool Equals(EventKey other) => this.Id == other.Id && this.Name == other.Name; - - /// - public override bool Equals(object? obj) => obj is EventKey other && Equals(other); } } } From 59165d5132ce74b99a2fc423961a09b62a3674a2 Mon Sep 17 00:00:00 2001 From: Andrii Babanin Date: Fri, 1 Nov 2024 11:31:04 +0200 Subject: [PATCH 34/43] Change copyright text. --- .../Extensions/Logging/EventIdPropertyCache.cs | 15 +++++++++++++-- .../EventIdPropertyCacheTests.cs | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs index 989b3b4..ea73024 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs @@ -1,5 +1,16 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. namespace Serilog.Extensions.Logging { diff --git a/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs index 4295dcb..655a54f 100644 --- a/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs @@ -1,5 +1,16 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. using Microsoft.Extensions.Logging; using Serilog.Events; From cb77647a9eb1be56ca783bf140767008b5f3f3d1 Mon Sep 17 00:00:00 2001 From: Andrii Babanin Date: Fri, 1 Nov 2024 13:20:05 +0200 Subject: [PATCH 35/43] Shared static EventIdPropertyCache cache; Use ConcurrentDictionary as storage. --- .../Logging/EventIdPropertyCache.cs | 58 ++++++++----------- .../Extensions/Logging/SerilogLogger.cs | 6 +- .../EventIdPropertyCacheTests.cs | 4 +- 3 files changed, 28 insertions(+), 40 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs index ea73024..02569e8 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs @@ -14,53 +14,46 @@ namespace Serilog.Extensions.Logging { + using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using Serilog.Events; - internal sealed class EventIdPropertyCache + static class EventIdPropertyCache { - private readonly object _createLock = new(); - private readonly int _maxCapacity; - private readonly Dictionary _propertyCache; + const int MaxCachedProperties = 1024; - private int count; + static readonly ConcurrentDictionary s_propertyCache = new(); + static int s_count; - public EventIdPropertyCache(int maxCapacity) - { - _maxCapacity = maxCapacity; - _propertyCache = new Dictionary(capacity: maxCapacity); - } - - public LogEventProperty GetOrCreateProperty(in EventId eventId) + public static LogEventProperty GetOrCreateProperty(in EventId eventId) { var eventKey = new EventKey(eventId); - if (_propertyCache.TryGetValue(eventKey, out var cachedProperty)) - { - return cachedProperty; - } + LogEventProperty? property; - lock (_createLock) + if (s_count >= MaxCachedProperties) { - // Double check under lock - if (_propertyCache.TryGetValue(eventKey, out cachedProperty)) + if (!s_propertyCache.TryGetValue(eventKey, out property)) { - return cachedProperty; + property = CreateCore(in eventKey); } - - cachedProperty = CreateCore(in eventKey); - - if (count < _maxCapacity) - { - _propertyCache[eventKey] = cachedProperty; - count++; - } - - return cachedProperty; } + else + { + property = s_propertyCache.GetOrAdd( + eventKey, + static key => + { + Interlocked.Increment(ref s_count); + + return CreateCore(in key); + }); + } + + return property; } - private static LogEventProperty CreateCore(in EventKey eventKey) + static LogEventProperty CreateCore(in EventKey eventKey) { var properties = new List(2); @@ -77,9 +70,8 @@ private static LogEventProperty CreateCore(in EventKey eventKey) return new LogEventProperty("EventId", new StructureValue(properties)); } - private readonly record struct EventKey + readonly record struct EventKey { - public EventKey(EventId eventId) { Id = eventId.Id; diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 7370afc..7505ebc 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -29,9 +29,7 @@ internal static string GetKeyWithoutFirstSymbol(ConcurrentDictionary(LogEventLevel level, EventId eventId, TState state } if (eventId.Id != 0 || eventId.Name != null) - properties.Add(this._eventPropertyCache.GetOrCreateProperty(in eventId)); + properties.Add(EventIdPropertyCache.GetOrCreateProperty(in eventId)); var (traceId, spanId) = Activity.Current is { } activity ? (activity.TraceId, activity.SpanId) : diff --git a/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs index 655a54f..47c7d40 100644 --- a/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs @@ -28,12 +28,10 @@ public class EventIdPropertyCacheTests public void LowAndHighNumberedEventIdsAreMapped(int id) { // Arrange - var cache = new EventIdPropertyCache(48); - var eventId = new EventId(id, "test"); // Act - var mapped = cache.GetOrCreateProperty(eventId); + var mapped = EventIdPropertyCache.GetOrCreateProperty(eventId); // Assert var value = Assert.IsType(mapped.Value); From 29b94f3017be03441279e50c63176db0dd0dfd2b Mon Sep 17 00:00:00 2001 From: Andrii Babanin Date: Fri, 1 Nov 2024 13:23:36 +0200 Subject: [PATCH 36/43] Revert sample code changes. --- samples/Sample/Program.cs | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/samples/Sample/Program.cs b/samples/Sample/Program.cs index 1abc00b..d70b9aa 100644 --- a/samples/Sample/Program.cs +++ b/samples/Sample/Program.cs @@ -32,16 +32,37 @@ var serviceProvider = services.BuildServiceProvider(); var logger = serviceProvider.GetRequiredService>(); -var eventId = new EventId(1001, "Test"); +var startTime = DateTimeOffset.UtcNow; +logger.LogInformation(1, "Started at {StartTime} and 0x{Hello:X} is hex of 42", startTime, 42); -for (int i = 0; i < 1_000; i++) +try { - logger.Log( - LogLevel.Information, - eventId, - "Subscription {SubscriptionId} for entity {EntityName} handler for message {MessageId} has been successfully completed.", - "my-subscription-id", - "TestQueue", - 1); + throw new Exception("Boom!"); } +catch (Exception ex) +{ + logger.LogCritical(ex, "Unexpected critical error starting application"); + logger.Log(LogLevel.Critical, 0, "Unexpected critical error", ex, null!); + // This write should not log anything + logger.Log(LogLevel.Critical, 0, null!, null, null!); + logger.LogError(ex, "Unexpected error"); + logger.LogWarning(ex, "Unexpected warning"); +} + +using (logger.BeginScope("Main")) +{ + logger.LogInformation("Waiting for user input"); + var key = Console.Read(); + logger.LogInformation("User pressed {@KeyInfo}", new { Key = key, KeyChar = (char)key }); +} + +var endTime = DateTimeOffset.UtcNow; +logger.LogInformation(2, "Stopping at {StopTime}", endTime); + +logger.LogInformation("Stopping"); + +logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "RESULT", "START TIME", "END TIME", "DURATION(ms)"); +logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "------", "----- ----", "--- ----", "------------"); +logger.LogInformation("{Result,-10:l}{StartTime,15:mm:s tt}{EndTime,15:mm:s tt}{Duration,15}", "SUCCESS", startTime, endTime, (endTime - startTime).TotalMilliseconds); + serviceProvider.Dispose(); From 4f3305ae36a6a97e11afbb3955af6a50b06bbfae Mon Sep 17 00:00:00 2001 From: Andreas Gehrke Date: Fri, 15 Nov 2024 12:58:49 +0100 Subject: [PATCH 37/43] Add functional tests of AddSerilog() extension method. --- .../SerilogLoggingBuilderExtensionsTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs new file mode 100644 index 0000000..8ae14ed --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog.Extensions.Logging.Tests.Support; +using Xunit; + +namespace Serilog.Extensions.Logging.Tests; + +public class SerilogLoggingBuilderExtensionsTests +{ + [Fact] + public void AddSerilog_must_register_a_ILoggerProvider() + { + var services = new ServiceCollection() + .AddLogging(builder => { builder.AddSerilog(); }) + .BuildServiceProvider(); + + var loggerProviders = services.GetServices(); + Assert.Contains(loggerProviders, provider => provider is SerilogLoggerProvider); + } + + [Fact] + public void AddSerilog_must_register_a_ILoggerProvider_that_forwards_logs_to_static_Serilog_Logger() + { + var sink = new SerilogSink(); + Log.Logger = new LoggerConfiguration() + .WriteTo.Sink(sink) + .CreateLogger(); + + var services = new ServiceCollection() + .AddLogging(builder => { builder.AddSerilog(); }) + .BuildServiceProvider(); + + var logger = services.GetRequiredService>(); + logger.LogInformation("Hello, world!"); + + Assert.Single(sink.Writes); + } + + [Fact] + public void AddSerilog_must_register_a_ILoggerProvider_that_forwards_logs_to_provided_logger() + { + var sink = new SerilogSink(); + var serilogLogger = new LoggerConfiguration() + .WriteTo.Sink(sink) + .CreateLogger(); + + var services = new ServiceCollection() + .AddLogging(builder => { builder.AddSerilog(logger: serilogLogger); }) + .BuildServiceProvider(); + + var logger = services.GetRequiredService>(); + logger.LogInformation("Hello, world!"); + + Assert.Single(sink.Writes); + } +} From d5eb130f2b294c6fc9db13292114cd2b208c2be2 Mon Sep 17 00:00:00 2001 From: Andreas Gehrke Date: Fri, 15 Nov 2024 13:05:59 +0100 Subject: [PATCH 38/43] Make SerilogLoggerProvider implement IAsyncDisposable on .NET 6 or later. --- .../Logging/SerilogLoggerProvider.cs | 31 ++++++++ .../DisposeTests.cs | 72 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 test/Serilog.Extensions.Logging.Tests/DisposeTests.cs diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs index b8563cb..e72985a 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Serilog.Core; using Serilog.Events; @@ -14,6 +15,9 @@ namespace Serilog.Extensions.Logging; /// [ProviderAlias("Serilog")] public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISupportExternalScope +#if NET6_0_OR_GREATER + , IAsyncDisposable +#endif { internal const string OriginalFormatPropertyName = "{OriginalFormat}"; internal const string ScopePropertyName = "Scope"; @@ -21,6 +25,9 @@ public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISuppor // May be null; if it is, Log.Logger will be lazily used readonly ILogger? _logger; readonly Action? _dispose; +#if NET6_0_OR_GREATER + readonly Func? _disposeAsync; +#endif private IExternalScopeProvider? _externalScopeProvider; /// @@ -36,9 +43,25 @@ public SerilogLoggerProvider(ILogger? logger = null, bool dispose = false) if (dispose) { if (logger != null) + { _dispose = () => (logger as IDisposable)?.Dispose(); +#if NET6_0_OR_GREATER + _disposeAsync = () => + { + // Dispose via IAsyncDisposable if possible, otherwise fall back to IDisposable + if (logger is IAsyncDisposable asyncDisposable) return asyncDisposable.DisposeAsync(); + else (logger as IDisposable)?.Dispose(); + return default; + }; +#endif + } else + { _dispose = Log.CloseAndFlush; +#if NET6_0_OR_GREATER + _disposeAsync = Log.CloseAndFlushAsync; +#endif + } } } @@ -113,4 +136,12 @@ public void Dispose() { _dispose?.Invoke(); } + +#if NET6_0_OR_GREATER + /// + public ValueTask DisposeAsync() + { + return _disposeAsync?.Invoke() ?? default; + } +#endif } diff --git a/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs b/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs new file mode 100644 index 0000000..ab3cef8 --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog.Core; +using Serilog.Events; +using Xunit; + +namespace Serilog.Extensions.Logging.Tests; + +public class DisposeTests +{ + private readonly DisposableSink _sink; + private readonly Logger _serilogLogger; + + public DisposeTests() + { + _sink = new DisposableSink(); + _serilogLogger = new LoggerConfiguration() + .WriteTo.Sink(_sink) + .CreateLogger(); + } + + [Fact] + public void AddSerilog_must_dispose_the_provider_when_dispose_is_true() + { + var services = new ServiceCollection() + .AddLogging(builder => builder.AddSerilog(logger: _serilogLogger, dispose: true)) + .BuildServiceProvider(); + + // Get a logger so that we ensure SerilogLoggerProvider is created + var logger = services.GetRequiredService>(); + logger.LogInformation("Hello, world!"); + + services.Dispose(); + Assert.True(_sink.DisposeCalled); + Assert.False(_sink.DisposeAsyncCalled); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task AddSerilog_must_async_dispose_the_provider_when_dispose_is_true() + { + var services = new ServiceCollection() + .AddLogging(builder => builder.AddSerilog(logger: _serilogLogger, dispose: true)) + .BuildServiceProvider(); + + // Get a logger so that we ensure SerilogLoggerProvider is created + var logger = services.GetRequiredService>(); + logger.LogInformation("Hello, world!"); + + await services.DisposeAsync(); + Assert.False(_sink.DisposeCalled); + Assert.True(_sink.DisposeAsyncCalled); + } +#endif + + private sealed class DisposableSink : ILogEventSink, IDisposable, IAsyncDisposable + { + public bool DisposeAsyncCalled { get; private set; } + public bool DisposeCalled { get; private set; } + + public void Dispose() => DisposeCalled = true; + public ValueTask DisposeAsync() + { + DisposeAsyncCalled = true; + return default; + } + + public void Emit(LogEvent logEvent) + { + } + } +} From 2bc0ea5728da84b44c38104d92a84174932bbab3 Mon Sep 17 00:00:00 2001 From: Andrii Babanin Date: Fri, 15 Nov 2024 17:24:27 +0200 Subject: [PATCH 39/43] Make EventIdPropertyCache non-static; Create unit tests. --- .../Logging/EventIdPropertyCache.cs | 107 ++++++++-------- .../Extensions/Logging/SerilogLogger.cs | 5 +- .../EventIdPropertyCacheTests.cs | 115 ++++++++++++++---- 3 files changed, 154 insertions(+), 73 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs index 02569e8..d2159f3 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs @@ -12,75 +12,86 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Serilog.Extensions.Logging +namespace Serilog.Extensions.Logging; + +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Serilog.Events; + +class EventIdPropertyCache { - using System.Collections.Concurrent; - using Microsoft.Extensions.Logging; - using Serilog.Events; + readonly int _maxCachedProperties; + readonly ConcurrentDictionary _propertyCache = new(); - static class EventIdPropertyCache - { - const int MaxCachedProperties = 1024; + int _count; - static readonly ConcurrentDictionary s_propertyCache = new(); - static int s_count; + public EventIdPropertyCache(int maxCachedProperties = 1024) + { + _maxCachedProperties = maxCachedProperties; + } - public static LogEventProperty GetOrCreateProperty(in EventId eventId) - { - var eventKey = new EventKey(eventId); + public LogEventProperty GetOrCreateProperty(in EventId eventId) + { + var eventKey = new EventKey(eventId); - LogEventProperty? property; + LogEventProperty? property; - if (s_count >= MaxCachedProperties) + if (_count >= _maxCachedProperties) + { + if (!_propertyCache.TryGetValue(eventKey, out property)) { - if (!s_propertyCache.TryGetValue(eventKey, out property)) - { - property = CreateCore(in eventKey); - } + property = CreateProperty(in eventKey); } - else + } + else + { + if (!_propertyCache.TryGetValue(eventKey, out property)) { - property = s_propertyCache.GetOrAdd( - eventKey, - static key => - { - Interlocked.Increment(ref s_count); - - return CreateCore(in key); - }); + // GetOrAdd is moved to a separate method to prevent closure allocation + property = GetOrAddCore(in eventKey); } + } - return property; + return property; + } + + static LogEventProperty CreateProperty(in EventKey eventKey) + { + var properties = new List(2); + + if (eventKey.Id != 0) + { + properties.Add(new LogEventProperty("Id", new ScalarValue(eventKey.Id))); } - static LogEventProperty CreateCore(in EventKey eventKey) + if (eventKey.Name != null) { - var properties = new List(2); + properties.Add(new LogEventProperty("Name", new ScalarValue(eventKey.Name))); + } - if (eventKey.Id != 0) - { - properties.Add(new LogEventProperty("Id", new ScalarValue(eventKey.Id))); - } + return new LogEventProperty("EventId", new StructureValue(properties)); + } - if (eventKey.Name != null) + LogEventProperty GetOrAddCore(in EventKey eventKey) => + _propertyCache.GetOrAdd( + eventKey, + key => { - properties.Add(new LogEventProperty("Name", new ScalarValue(eventKey.Name))); - } + Interlocked.Increment(ref _count); - return new LogEventProperty("EventId", new StructureValue(properties)); - } + return CreateProperty(in key); + }); - readonly record struct EventKey + readonly record struct EventKey + { + public EventKey(EventId eventId) { - public EventKey(EventId eventId) - { - Id = eventId.Id; - Name = eventId.Name; - } + Id = eventId.Id; + Name = eventId.Name; + } - public int Id { get; } + public int Id { get; } - public string? Name { get; } - } + public string? Name { get; } } } diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 7505ebc..a21d631 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -28,8 +28,9 @@ internal static string GetKeyWithoutFirstSymbol(ConcurrentDictionary(LogEventLevel level, EventId eventId, TState state } if (eventId.Id != 0 || eventId.Name != null) - properties.Add(EventIdPropertyCache.GetOrCreateProperty(in eventId)); + properties.Add(_eventIdPropertyCache.GetOrCreateProperty(in eventId)); var (traceId, spanId) = Activity.Current is { } activity ? (activity.TraceId, activity.SpanId) : diff --git a/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs index 47c7d40..9949cbf 100644 --- a/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs @@ -16,30 +16,99 @@ using Serilog.Events; using Xunit; -namespace Serilog.Extensions.Logging.Tests +namespace Serilog.Extensions.Logging.Tests; + +public class EventIdPropertyCacheTests { - public class EventIdPropertyCacheTests + [Fact] + public void CreatesPropertyWithCorrectIdAndName() + { + // Arrange + const int id = 101; + const string name = "TestEvent"; + var eventId = new EventId(id, name); + + var cache = new EventIdPropertyCache(); + + // Act + var eventProperty = cache.GetOrCreateProperty(eventId); + + // Assert + var value = Assert.IsType(eventProperty.Value); + + Assert.Equal(2, value.Properties.Count); + + var idValue = value.Properties.Single(property => property.Name == "Id").Value; + var nameValue = value.Properties.Single(property => property.Name == "Name").Value; + + var scalarId = Assert.IsType(idValue); + var scalarName = Assert.IsType(nameValue); + + Assert.Equal(id, scalarId.Value); + Assert.Equal(name, scalarName.Value); + } + + [Fact] + public void EventsWithDSameKeysHaveSameReferences() + { + // Arrange + var cache = new EventIdPropertyCache(); + + // Act + var property1 = cache.GetOrCreateProperty(new EventId(1, "Name1")); + var property2 = cache.GetOrCreateProperty(new EventId(1, "Name1")); + + // Assert + Assert.Same(property1, property2); + } + + [Theory] + [InlineData(1, "SomeName", 1, "AnotherName")] + [InlineData(1, "SomeName", 2, "SomeName")] + [InlineData(1, "SomeName", 2, "AnotherName")] + public void EventsWithDifferentKeysHaveDifferentReferences(int firstId, string firstName, int secondId, string secondName) + { + // Arrange + var cache = new EventIdPropertyCache(); + + // Act + var property1 = cache.GetOrCreateProperty(new EventId(firstId, firstName)); + var property2 = cache.GetOrCreateProperty(new EventId(secondId, secondName)); + + // Assert + Assert.NotSame(property1, property2); + } + + + [Fact] + public void WhenLimitIsNotOverSameEventsHaveSameReferences() { - [Theory] - [InlineData(1)] - [InlineData(10)] - [InlineData(48)] - [InlineData(100)] - public void LowAndHighNumberedEventIdsAreMapped(int id) - { - // Arrange - var eventId = new EventId(id, "test"); - - // Act - var mapped = EventIdPropertyCache.GetOrCreateProperty(eventId); - - // Assert - var value = Assert.IsType(mapped.Value); - Assert.Equal(2, value.Properties.Count); - - var idValue = value.Properties.Single(p => p.Name == "Id").Value; - var scalar = Assert.IsType(idValue); - Assert.Equal(id, scalar.Value); - } + // Arrange + var eventId = new EventId(101, "test"); + var cache = new EventIdPropertyCache(); + + // Act + var property1 = cache.GetOrCreateProperty(eventId); + var property2 = cache.GetOrCreateProperty(eventId); + + // Assert + Assert.Same(property1, property2); + } + + [Fact] + public void WhenLimitIsOverSameEventsHaveDifferentReferences() + { + // Arrange + var cache = new EventIdPropertyCache(maxCachedProperties: 1); + cache.GetOrCreateProperty(new EventId(1, "InitialEvent")); + + var eventId = new EventId(101, "DifferentEvent"); + + // Act + var property1 = cache.GetOrCreateProperty(eventId); + var property2 = cache.GetOrCreateProperty(eventId); + + // Assert + Assert.NotSame(property1, property2); } } From 03a235afe2de3f56615ee28ce4a7ba6c91dce281 Mon Sep 17 00:00:00 2001 From: Andreas Gehrke Date: Sat, 16 Nov 2024 08:01:46 +0100 Subject: [PATCH 40/43] Address naming --- test/Serilog.Extensions.Logging.Tests/DisposeTests.cs | 4 ++-- .../SerilogLoggingBuilderExtensionsTests.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs b/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs index ab3cef8..0e7448e 100644 --- a/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs @@ -20,7 +20,7 @@ public DisposeTests() } [Fact] - public void AddSerilog_must_dispose_the_provider_when_dispose_is_true() + public void DisposesProviderWhenDisposeIsTrue() { var services = new ServiceCollection() .AddLogging(builder => builder.AddSerilog(logger: _serilogLogger, dispose: true)) @@ -37,7 +37,7 @@ public void AddSerilog_must_dispose_the_provider_when_dispose_is_true() #if NET8_0_OR_GREATER [Fact] - public async Task AddSerilog_must_async_dispose_the_provider_when_dispose_is_true() + public async Task DisposesProviderAsyncWhenDisposeIsTrue() { var services = new ServiceCollection() .AddLogging(builder => builder.AddSerilog(logger: _serilogLogger, dispose: true)) diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs index 8ae14ed..e30d017 100644 --- a/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs @@ -8,7 +8,7 @@ namespace Serilog.Extensions.Logging.Tests; public class SerilogLoggingBuilderExtensionsTests { [Fact] - public void AddSerilog_must_register_a_ILoggerProvider() + public void AddSerilogMustRegisterAnILoggerProvider() { var services = new ServiceCollection() .AddLogging(builder => { builder.AddSerilog(); }) @@ -19,7 +19,7 @@ public void AddSerilog_must_register_a_ILoggerProvider() } [Fact] - public void AddSerilog_must_register_a_ILoggerProvider_that_forwards_logs_to_static_Serilog_Logger() + public void AddSerilogMustRegisterAnILoggerProviderThatForwardsLogsToStaticSerilogLogger() { var sink = new SerilogSink(); Log.Logger = new LoggerConfiguration() @@ -37,7 +37,7 @@ public void AddSerilog_must_register_a_ILoggerProvider_that_forwards_logs_to_sta } [Fact] - public void AddSerilog_must_register_a_ILoggerProvider_that_forwards_logs_to_provided_logger() + public void AddSerilogMustRegisterAnILoggerProviderThatForwardsLogsToProvidedLogger() { var sink = new SerilogSink(); var serilogLogger = new LoggerConfiguration() From ac351f2f098e504ae8d354ed03d5edde7ef1036d Mon Sep 17 00:00:00 2001 From: Andreas Gehrke Date: Sat, 16 Nov 2024 08:02:29 +0100 Subject: [PATCH 41/43] Define FEATURE_ASYNCDISPOSABLE and use --- .../Extensions/Logging/SerilogLoggerProvider.cs | 10 +++++----- .../Serilog.Extensions.Logging.csproj | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs index e72985a..d80e4e9 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs @@ -15,7 +15,7 @@ namespace Serilog.Extensions.Logging; /// [ProviderAlias("Serilog")] public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISupportExternalScope -#if NET6_0_OR_GREATER +#if FEATURE_ASYNCDISPOSABLE , IAsyncDisposable #endif { @@ -25,7 +25,7 @@ public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISuppor // May be null; if it is, Log.Logger will be lazily used readonly ILogger? _logger; readonly Action? _dispose; -#if NET6_0_OR_GREATER +#if FEATURE_ASYNCDISPOSABLE readonly Func? _disposeAsync; #endif private IExternalScopeProvider? _externalScopeProvider; @@ -45,7 +45,7 @@ public SerilogLoggerProvider(ILogger? logger = null, bool dispose = false) if (logger != null) { _dispose = () => (logger as IDisposable)?.Dispose(); -#if NET6_0_OR_GREATER +#if FEATURE_ASYNCDISPOSABLE _disposeAsync = () => { // Dispose via IAsyncDisposable if possible, otherwise fall back to IDisposable @@ -58,7 +58,7 @@ public SerilogLoggerProvider(ILogger? logger = null, bool dispose = false) else { _dispose = Log.CloseAndFlush; -#if NET6_0_OR_GREATER +#if FEATURE_ASYNCDISPOSABLE _disposeAsync = Log.CloseAndFlushAsync; #endif } @@ -137,7 +137,7 @@ public void Dispose() _dispose?.Invoke(); } -#if NET6_0_OR_GREATER +#if FEATURE_ASYNCDISPOSABLE /// public ValueTask DisposeAsync() { diff --git a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj index 457818f..d1a4cef 100644 --- a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj +++ b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj @@ -36,18 +36,18 @@ - $(DefineConstants);FEATURE_ITUPLE + $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE - $(DefineConstants);FEATURE_ITUPLE + $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE - $(DefineConstants);FEATURE_ITUPLE + $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE - $(DefineConstants);FEATURE_ITUPLE + $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE From a35b35b4d17748433653cc1147d3db87a89182f3 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 25 Nov 2024 06:12:41 +1000 Subject: [PATCH 42/43] Update to .NET 9, Actions build, revise targets, seal public types not designed for inheritance --- .DS_Store | Bin 0 -> 6148 bytes .github/.DS_Store | Bin 0 -> 6148 bytes .github/workflows/ci.yml | 41 ++++++++ Build.ps1 | 93 +++++++++++------- Directory.Build.props | 21 +++- Directory.Build.targets | 3 - Directory.Version.props | 6 ++ Setup.ps1 | 9 -- appveyor.yml | 26 ----- build.sh | 12 --- global.json | 4 +- samples/.DS_Store | Bin 0 -> 6148 bytes samples/Sample/Program.cs | 24 +---- samples/Sample/Properties/launchSettings.json | 3 - samples/Sample/Sample.csproj | 10 +- samples/SampleWithExternalScope/Program.cs | 7 +- .../SampleWithExternalScope.csproj | 11 +-- samples/SampleWithMelProviders/Program.cs | 69 +++++++++++++ .../SampleWithMelProviders.csproj | 18 ++++ serilog-extensions-logging.sln | 12 ++- .../Logging/CachingMessageTemplateParser.cs | 3 +- .../Logging/EventIdPropertyCache.cs | 4 +- .../Logging/LoggerProviderCollection.cs | 6 +- .../Logging/LoggerProviderCollectionSink.cs | 2 +- .../Extensions/Logging/SerilogLogValues.cs | 5 +- .../Extensions/Logging/SerilogLogger.cs | 2 +- .../Logging/SerilogLoggerFactory.cs | 2 +- .../Logging/SerilogLoggerProvider.cs | 23 +++-- .../Extensions/Logging/SerilogLoggerScope.cs | 2 +- .../Extensions/StringExtensions.cs | 4 +- .../Properties/AssemblyInfo.cs | 2 - .../Serilog.Extensions.Logging.csproj | 25 ++--- .../SerilogLoggingBuilderExtensions.cs | 2 +- ...rilog.Extensions.Logging.Benchmarks.csproj | 12 +-- .../Serilog.Extensions.Logging.Tests.csproj | 12 +-- 35 files changed, 278 insertions(+), 197 deletions(-) create mode 100644 .DS_Store create mode 100644 .github/.DS_Store create mode 100644 .github/workflows/ci.yml delete mode 100644 Directory.Build.targets create mode 100644 Directory.Version.props delete mode 100644 Setup.ps1 delete mode 100644 appveyor.yml delete mode 100644 build.sh create mode 100644 samples/.DS_Store delete mode 100644 samples/Sample/Properties/launchSettings.json create mode 100644 samples/SampleWithMelProviders/Program.cs create mode 100644 samples/SampleWithMelProviders/SampleWithMelProviders.csproj diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..76be7823983e9eaa2fd2d8e3fbfe2318c68f95b6 GIT binary patch literal 6148 zcmeHK%}(1u5S|Sw*$5%!08)-C^%_Bl1XaYvg!I-EMsR3QuyK@FwY^sC5TrDc&v5O- z@C1E?K2K+VQjr8CE>ta=k!HW$*_mDM=VZqN0IV|!4ghKZz)=;Js@OauYF_D@l&obF zk?A!?FabXr9=JDh&6^DWA_MB(RbUT>5JC=5-d~(PdE6C`Sa!R9Bz^SYKYHK4wC{eN ztgjQx;1yo!Nc~z?cFVwn7*dE}0GU6{QoGxupGIMpH=Dm$X`}pMv%)KUi(k1TIdyX{ zpJW|x{8f$4rHuSV-}5iR!L(o7Ih9H7g~?#71Hxc{F1^bz3FNdRCrOa$Sl3MODzEly z_1Ub|K5B^8QFGo9vv#Z55Qm@I^Ldr;e%wDk>wk;lMBW>&82$^iY+GEw19mE~bgEl- zoJ2Ai;>;py;Y~;k5Cg=(TQgwq9lrI}8`1j205S02F`(`TfvV84SQyk-2R6C{KrEwK z3-(nlp<;wZ$6{d+XHb|4MKqzzmKe;0qg_}($6{g7gafn12eUgfTcI$$JFYLZJ21x} zwZs51u*|@Q8P-((|NL_OzZ^s(Vt^QUR}8Rn*X?%jNH({w9Zuz1fqIRqMEMm4|Ab&i ir(*O~skn}+1?@t05FLw!LA0RIhk&Mm8e-tDGVmKa5o%rl literal 0 HcmV?d00001 diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..36b372c72cba58418798b30534145e40b446efdb GIT binary patch literal 6148 zcmeHKJ5EC}5S)b+K{P2TeTCe>ioyxF0D>q@p7bOlMEfevm7_8HDTp3)NfXUV>#^56 zwmik#w*YMOxmyD(086?fzI~XR@4L_Jt|CUH^Nc+PjQGNFcbsMa9dPag86!S$_|1PG z-o|n8ZJ8910#ZN7!Kn+ zdI@6l0I?TNiHy)Jsl=pOwHTIk##`m}!YMK7uxdW6Zno-BENN(xAUn^M3Q+n4QzPpaBFdz{zWMt`My&WY~Ec~Ce+IVMIq=EBSI dQzT_x^Evl>;glG3#)D4O&w%S9lLFUP;0L2O71{s* literal 0 HcmV?d00001 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..acd3bc6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +# If this file is renamed, the incrementing run attempt number will be reset. + +name: CI + +on: + push: + branches: [ "dev", "main" ] + pull_request: + branches: [ "dev", "main" ] + +env: + CI_BUILD_NUMBER_BASE: ${{ github.run_number }} + CI_TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} + +jobs: + build: + + # The build must run on Windows so that .NET Framework targets can be built and tested. + runs-on: windows-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Setup + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Compute build number + shell: bash + run: | + echo "CI_BUILD_NUMBER=$(($CI_BUILD_NUMBER_BASE+2300))" >> $GITHUB_ENV + - name: Build and Publish + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: pwsh + run: | + ./Build.ps1 diff --git a/Build.ps1 b/Build.ps1 index 431f4e2..e798284 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,58 +1,79 @@ -echo "build: Build started" +Write-Output "build: Tool versions follow" -$env:Path = "$pwd/.dotnetcli;$env:Path" +dotnet --version +dotnet --list-sdks + +Write-Output "build: Build started" Push-Location $PSScriptRoot +try { + if(Test-Path .\artifacts) { + Write-Output "build: Cleaning ./artifacts" + Remove-Item ./artifacts -Force -Recurse + } -if(Test-Path .\artifacts) { - echo "build: Cleaning .\artifacts" - Remove-Item .\artifacts -Force -Recurse -} + & dotnet restore --no-cache -& dotnet restore --no-cache + $dbp = [Xml] (Get-Content .\Directory.Version.props) + $versionPrefix = $dbp.Project.PropertyGroup.VersionPrefix -$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; -$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; -$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"] + Write-Output "build: Package version prefix is $versionPrefix" -echo "build: Version suffix is $suffix" + $branch = @{ $true = $env:CI_TARGET_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$NULL -ne $env:CI_TARGET_BRANCH]; + $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:CI_BUILD_NUMBER, 10); $false = "local" }[$NULL -ne $env:CI_BUILD_NUMBER]; + $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)) -replace '([^a-zA-Z0-9\-]*)', '')-$revision"}[$branch -eq "main" -and $revision -ne "local"] + $commitHash = $(git rev-parse --short HEAD) + $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] -foreach ($src in ls src/*) { - Push-Location $src + Write-Output "build: Package version suffix is $suffix" + Write-Output "build: Build version suffix is $buildSuffix" - echo "build: Packaging project in $src" + & 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 - } else { - & dotnet pack -c Release --include-source -o ..\..\artifacts - } + foreach ($src in Get-ChildItem src/*) { + Push-Location $src - if($LASTEXITCODE -ne 0) { throw "build failed" } + Write-Output "build: Packaging project in $src" - Pop-Location -} + if ($suffix) { + & dotnet pack -c Release --no-build --no-restore -o ../../artifacts --version-suffix=$suffix + } else { + & dotnet pack -c Release --no-build --no-restore -o ../../artifacts + } + if($LASTEXITCODE -ne 0) { throw "Packaging failed" } + + Pop-Location + } -foreach ($test in ls test/*.PerformanceTests) { - Push-Location $test + foreach ($test in Get-ChildItem test/*.Tests) { + Push-Location $test - echo "build: Building performance test project in $test" + Write-Output "build: Testing project in $test" - & dotnet build -c Release - if($LASTEXITCODE -ne 0) { throw "test failed" } + & dotnet test -c Release --no-build --no-restore + if($LASTEXITCODE -ne 0) { throw "Testing failed" } - Pop-Location -} + Pop-Location + } -foreach ($test in ls test/*.Tests) { - Push-Location $test + if ($env:NUGET_API_KEY) { + # GitHub Actions will only supply this to branch builds and not PRs. We publish + # builds from any branch this action targets (i.e. main and dev). - echo "build: Testing project in $test" + Write-Output "build: Publishing NuGet packages" - & dotnet test -c Release - if($LASTEXITCODE -ne 0) { throw "test failed" } + foreach ($nupkg in Get-ChildItem artifacts/*.nupkg) { + & dotnet nuget push -k $env:NUGET_API_KEY -s https://api.nuget.org/v3/index.json "$nupkg" + if($LASTEXITCODE -ne 0) { throw "Publishing failed" } + } + if (!($suffix)) { + Write-Output "build: Creating release for version $versionPrefix" + + iex "gh release create v$versionPrefix --title v$versionPrefix --generate-notes $(get-item ./artifacts/*.nupkg) $(get-item ./artifacts/*.snupkg)" + } + } +} finally { Pop-Location } - -Pop-Location diff --git a/Directory.Build.props b/Directory.Build.props index 2b620bc..c114992 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,12 +1,25 @@ - + + latest True - true + + true $(MSBuildThisFileDirectory)assets/Serilog.snk - true + false enable + enable + true + true + true + true + snupkg - + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets deleted file mode 100644 index faf2349..0000000 --- a/Directory.Build.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/Directory.Version.props b/Directory.Version.props new file mode 100644 index 0000000..3077571 --- /dev/null +++ b/Directory.Version.props @@ -0,0 +1,6 @@ + + + + 9.0.0 + + diff --git a/Setup.ps1 b/Setup.ps1 deleted file mode 100644 index aa90b5b..0000000 --- a/Setup.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -$ErrorActionPreference = "Stop" - -$RequiredDotnetVersion = $(cat ./global.json | convertfrom-json).sdk.version - -New-Item -ItemType Directory -Force "./build/" | Out-Null - -Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./build/installcli.ps1" -& ./build/installcli.ps1 -InstallDir "$pwd/.dotnetcli" -NoPath -Version $RequiredDotnetVersion -if ($LASTEXITCODE) { throw ".NET install failed" } diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index b7e0f44..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: '{build}' -skip_tags: true -image: Visual Studio 2022 -install: -- pwsh: ./Setup.ps1 -- pwsh: mkdir -Force ".\build\" | Out-Null -build_script: -- pwsh: ./Build.ps1 -test: off -artifacts: -- path: artifacts/Serilog.*.nupkg -deploy: -- provider: NuGet - api_key: - secure: H96ajkMxwIafhF2vrr+UAUS10bFcAL/1wc3iphidRiYi9WoTc2i8shTLtF+75ODb - 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/build.sh b/build.sh deleted file mode 100644 index 6d1ff38..0000000 --- a/build.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -dotnet --info -dotnet restore - -for path in src/**/*.csproj; do - dotnet build -f netstandard2.0 -c Release ${path} - -done - -for path in test/*.Tests/*.csproj; do - dotnet test -f netcoreapp2.0 -c Release ${path} -done \ No newline at end of file diff --git a/global.json b/global.json index 5ce8495..db8627a 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,7 @@ { "sdk": { - "version": "8.0.100" + "version": "9.0.100", + "allowPrerelease": false, + "rollForward": "latestFeature" } } diff --git a/samples/.DS_Store b/samples/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2312eceb69f3e5a2f0c07bef13341e9b1475a763 GIT binary patch literal 6148 zcmeHKJxc>Y5PhR50ye3F<^Bc#!4hIA1X84pdV<2qg+xFat29E8?WX*mPfgWLFUpBAsWnc)PXqtO{RIx&9)RF||=;6D`j0^tN1e*gdg literal 0 HcmV?d00001 diff --git a/samples/Sample/Program.cs b/samples/Sample/Program.cs index d70b9aa..b369510 100644 --- a/samples/Sample/Program.cs +++ b/samples/Sample/Program.cs @@ -3,33 +3,16 @@ using Serilog; using Serilog.Extensions.Logging; -// Creating a `LoggerProviderCollection` lets Serilog optionally write -// events through other dynamically-added MEL ILoggerProviders. -var providers = new LoggerProviderCollection(); - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() .WriteTo.Console() - .WriteTo.Providers(providers) .CreateLogger(); var services = new ServiceCollection(); -services.AddSingleton(providers); -services.AddSingleton(sc => -{ - var providerCollection = sc.GetService(); - var factory = new SerilogLoggerFactory(null, true, providerCollection); - - foreach (var provider in sc.GetServices()) - factory.AddProvider(provider); - - return factory; -}); - -services.AddLogging(l => l.AddConsole()); +services.AddLogging(); +services.AddSingleton(new SerilogLoggerFactory()); -var serviceProvider = services.BuildServiceProvider(); +using var serviceProvider = services.BuildServiceProvider(); var logger = serviceProvider.GetRequiredService>(); var startTime = DateTimeOffset.UtcNow; @@ -65,4 +48,3 @@ logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "------", "----- ----", "--- ----", "------------"); logger.LogInformation("{Result,-10:l}{StartTime,15:mm:s tt}{EndTime,15:mm:s tt}{Duration,15}", "SUCCESS", startTime, endTime, (endTime - startTime).TotalMilliseconds); -serviceProvider.Dispose(); diff --git a/samples/Sample/Properties/launchSettings.json b/samples/Sample/Properties/launchSettings.json deleted file mode 100644 index 43e0c99..0000000 --- a/samples/Sample/Properties/launchSettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "profiles": {} -} \ No newline at end of file diff --git a/samples/Sample/Sample.csproj b/samples/Sample/Sample.csproj index 951b742..793b13a 100644 --- a/samples/Sample/Sample.csproj +++ b/samples/Sample/Sample.csproj @@ -1,10 +1,8 @@  - net8.0 - Sample + net9.0 Exe - enable @@ -12,9 +10,9 @@ - - - + + + diff --git a/samples/SampleWithExternalScope/Program.cs b/samples/SampleWithExternalScope/Program.cs index 3a1a018..2b0aad7 100644 --- a/samples/SampleWithExternalScope/Program.cs +++ b/samples/SampleWithExternalScope/Program.cs @@ -30,14 +30,14 @@ // Add an ActivityListener (required, otherwise Activities don't actually get created if nothing is listening to them) ActivitySource.AddActivityListener(new ActivityListener { - ShouldListenTo = source => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded }); // Run our test var activitySource = new ActivitySource("SomeActivitySource"); -var serviceProvider = services.BuildServiceProvider(); +using var serviceProvider = services.BuildServiceProvider(); var logger = serviceProvider.GetRequiredService>(); using var activity = activitySource.StartActivity(); @@ -53,4 +53,3 @@ logger.LogInformation("Hello world!"); -serviceProvider.Dispose(); diff --git a/samples/SampleWithExternalScope/SampleWithExternalScope.csproj b/samples/SampleWithExternalScope/SampleWithExternalScope.csproj index 045c31a..84cb0ac 100644 --- a/samples/SampleWithExternalScope/SampleWithExternalScope.csproj +++ b/samples/SampleWithExternalScope/SampleWithExternalScope.csproj @@ -2,10 +2,7 @@ Exe - net8.0 - enable - Exe - enable + net9.0 @@ -13,9 +10,9 @@ - - - + + + diff --git a/samples/SampleWithMelProviders/Program.cs b/samples/SampleWithMelProviders/Program.cs new file mode 100644 index 0000000..c8b88b1 --- /dev/null +++ b/samples/SampleWithMelProviders/Program.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Extensions.Logging; + +// Creating a `LoggerProviderCollection` lets Serilog optionally write +// events through other dynamically-added MEL ILoggerProviders. +var providers = new LoggerProviderCollection(); + +// The sample sets up Serilog's console sink here: +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .WriteTo.Providers(providers) + .CreateLogger(); + +var services = new ServiceCollection(); + +services.AddSingleton(providers); +services.AddSingleton(sc => +{ + var providerCollection = sc.GetService(); + var factory = new SerilogLoggerFactory(null, true, providerCollection); + + foreach (var provider in sc.GetServices()) + factory.AddProvider(provider); + + return factory; +}); + +// ..and MEL's console provider here: +services.AddLogging(l => l.AddConsole()); + +using var serviceProvider = services.BuildServiceProvider(); +var logger = serviceProvider.GetRequiredService>(); + +var startTime = DateTimeOffset.UtcNow; +logger.LogInformation(1, "Started at {StartTime} and 0x{Hello:X} is hex of 42", startTime, 42); + +try +{ + throw new Exception("Boom!"); +} +catch (Exception ex) +{ + logger.LogCritical(ex, "Unexpected critical error starting application"); + logger.Log(LogLevel.Critical, 0, "Unexpected critical error", ex, null!); + // This write should not log anything + logger.Log(LogLevel.Critical, 0, null!, null, null!); + logger.LogError(ex, "Unexpected error"); + logger.LogWarning(ex, "Unexpected warning"); +} + +using (logger.BeginScope("Main")) +{ + logger.LogInformation("Waiting for user input"); + var key = Console.Read(); + logger.LogInformation("User pressed {@KeyInfo}", new { Key = key, KeyChar = (char)key }); +} + +var endTime = DateTimeOffset.UtcNow; +logger.LogInformation(2, "Stopping at {StopTime}", endTime); + +logger.LogInformation("Stopping"); + +logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "RESULT", "START TIME", "END TIME", "DURATION(ms)"); +logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "------", "----- ----", "--- ----", "------------"); +logger.LogInformation("{Result,-10:l}{StartTime,15:mm:s tt}{EndTime,15:mm:s tt}{Duration,15}", "SUCCESS", startTime, endTime, (endTime - startTime).TotalMilliseconds); + diff --git a/samples/SampleWithMelProviders/SampleWithMelProviders.csproj b/samples/SampleWithMelProviders/SampleWithMelProviders.csproj new file mode 100644 index 0000000..793b13a --- /dev/null +++ b/samples/SampleWithMelProviders/SampleWithMelProviders.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + Exe + + + + + + + + + + + + + diff --git a/serilog-extensions-logging.sln b/serilog-extensions-logging.sln index d0d5cb9..baca24a 100644 --- a/serilog-extensions-logging.sln +++ b/serilog-extensions-logging.sln @@ -18,21 +18,20 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{9C21B9DF-AEDD-4AA6-BEA4-912DEF3E5B8E}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 Directory.Build.props = Directory.Build.props - Directory.Build.targets = Directory.Build.targets README.md = README.md assets\Serilog.snk = assets\Serilog.snk - build.sh = build.sh - Setup.ps1 = Setup.ps1 global.json = global.json + Directory.Version.props = Directory.Version.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Extensions.Logging.Benchmarks", "test\Serilog.Extensions.Logging.Benchmarks\Serilog.Extensions.Logging.Benchmarks.csproj", "{6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWithExternalScope", "samples\SampleWithExternalScope\SampleWithExternalScope.csproj", "{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWithMelProviders", "samples\SampleWithMelProviders\SampleWithMelProviders.csproj", "{B1454759-126F-4F33-84EE-C8E19541DF79}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +58,10 @@ Global {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Release|Any CPU.Build.0 = Release|Any CPU + {B1454759-126F-4F33-84EE-C8E19541DF79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1454759-126F-4F33-84EE-C8E19541DF79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1454759-126F-4F33-84EE-C8E19541DF79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1454759-126F-4F33-84EE-C8E19541DF79}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -69,6 +72,7 @@ Global {65357FBC-9BC4-466D-B621-1C3A19BC2A78} = {F2407211-6043-439C-8E06-3641634332E7} {6D5986FF-EECD-4E75-8BC6-A5F78AB549B2} = {E30F638E-BBBE-4AD1-93CE-48CC69CFEFE1} {653092A8-CBAD-40AA-A4CE-F8B19D6492C2} = {F2407211-6043-439C-8E06-3641634332E7} + {B1454759-126F-4F33-84EE-C8E19541DF79} = {F2407211-6043-439C-8E06-3641634332E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {811E61C5-3871-4633-AFAE-B35B619C8A10} diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/CachingMessageTemplateParser.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/CachingMessageTemplateParser.cs index 8966ef7..01b87f6 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/CachingMessageTemplateParser.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/CachingMessageTemplateParser.cs @@ -12,14 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. - using Serilog.Events; using Serilog.Parsing; using System.Collections; namespace Serilog.Extensions.Logging; -class CachingMessageTemplateParser +sealed class CachingMessageTemplateParser { readonly MessageTemplateParser _innerParser = new(); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs index d2159f3..f7ce438 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs @@ -16,9 +16,9 @@ namespace Serilog.Extensions.Logging; using System.Collections.Concurrent; using Microsoft.Extensions.Logging; -using Serilog.Events; +using Events; -class EventIdPropertyCache +sealed class EventIdPropertyCache { readonly int _maxCachedProperties; readonly ConcurrentDictionary _propertyCache = new(); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs index b944040..8799d7b 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs @@ -20,9 +20,9 @@ namespace Serilog.Extensions.Logging; /// /// A dynamically-modifiable collection of s. /// -public class LoggerProviderCollection : IDisposable +public sealed class LoggerProviderCollection : IDisposable { - volatile ILoggerProvider[] _providers = Array.Empty(); + volatile ILoggerProvider[] _providers = []; /// /// Add to the collection. @@ -37,7 +37,7 @@ public void AddProvider(ILoggerProvider provider) do { existing = _providers; - added = existing.Concat(new[] { provider }).ToArray(); + added = [..existing, provider]; } #pragma warning disable 420 // ref to a volatile field while (Interlocked.CompareExchange(ref _providers, added, existing) != existing); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs index 0febb52..5609905 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs @@ -18,7 +18,7 @@ namespace Serilog.Extensions.Logging; -class LoggerProviderCollectionSink : ILogEventSink, IDisposable +sealed class LoggerProviderCollectionSink : ILogEventSink, IDisposable { readonly LoggerProviderCollection _providers; diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs index 20253f8..287608b 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs @@ -44,10 +44,7 @@ public SerilogLogValues(MessageTemplate messageTemplate, IReadOnlyDictionary("{OriginalFormat}", _messageTemplate.Text); } - public KeyValuePair this[int index] - { - get => _values[index]; - } + public KeyValuePair this[int index] => _values[index]; public int Count => _properties.Count + 1; diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index a21d631..05f6bdd 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -12,7 +12,7 @@ namespace Serilog.Extensions.Logging; -class SerilogLogger : FrameworkLogger +sealed class SerilogLogger : FrameworkLogger { internal static readonly ConcurrentDictionary DestructureDictionary = new(); internal static readonly ConcurrentDictionary StringifyDictionary = new(); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs index b85dbcb..12ece4c 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs @@ -20,7 +20,7 @@ namespace Serilog.Extensions.Logging; /// /// A complete Serilog-backed implementation of the .NET Core logging infrastructure. /// -public class SerilogLoggerFactory : ILoggerFactory +public sealed class SerilogLoggerFactory : ILoggerFactory { readonly LoggerProviderCollection? _providerCollection; readonly SerilogLoggerProvider _provider; diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs index d80e4e9..d6b18d1 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Serilog.Core; using Serilog.Events; @@ -14,7 +13,7 @@ namespace Serilog.Extensions.Logging; /// An that pipes events through Serilog. /// [ProviderAlias("Serilog")] -public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISupportExternalScope +public sealed class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISupportExternalScope #if FEATURE_ASYNCDISPOSABLE , IAsyncDisposable #endif @@ -28,7 +27,7 @@ public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISuppor #if FEATURE_ASYNCDISPOSABLE readonly Func? _disposeAsync; #endif - private IExternalScopeProvider? _externalScopeProvider; + IExternalScopeProvider? _externalScopeProvider; /// /// Construct a . @@ -38,7 +37,7 @@ public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISuppor public SerilogLoggerProvider(ILogger? logger = null, bool dispose = false) { if (logger != null) - _logger = logger.ForContext(new[] { this }); + _logger = logger.ForContext([this]); if (dispose) { @@ -46,12 +45,16 @@ public SerilogLoggerProvider(ILogger? logger = null, bool dispose = false) { _dispose = () => (logger as IDisposable)?.Dispose(); #if FEATURE_ASYNCDISPOSABLE - _disposeAsync = () => + _disposeAsync = async () => { - // Dispose via IAsyncDisposable if possible, otherwise fall back to IDisposable - if (logger is IAsyncDisposable asyncDisposable) return asyncDisposable.DisposeAsync(); - else (logger as IDisposable)?.Dispose(); - return default; + if (logger is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + (logger as IDisposable)?.Dispose(); + } }; #endif } @@ -94,7 +97,7 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) if (scopeItem != null) { - scopeItems ??= new List(); + scopeItems ??= []; scopeItems.Add(scopeItem); } } diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs index ca16691..8e71133 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs @@ -6,7 +6,7 @@ namespace Serilog.Extensions.Logging; -class SerilogLoggerScope : IDisposable +sealed class SerilogLoggerScope : IDisposable { const string NoName = "None"; diff --git a/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs b/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs index 0e072b9..ac772fd 100644 --- a/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs +++ b/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs @@ -1,8 +1,8 @@ -using System.Runtime.CompilerServices; +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER +using System.Runtime.CompilerServices; namespace Serilog.Extensions; -#if !NET6_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER static class StringExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Serilog.Extensions.Logging/Properties/AssemblyInfo.cs b/src/Serilog.Extensions.Logging/Properties/AssemblyInfo.cs index 50afb45..468e64f 100644 --- a/src/Serilog.Extensions.Logging/Properties/AssemblyInfo.cs +++ b/src/Serilog.Extensions.Logging/Properties/AssemblyInfo.cs @@ -6,8 +6,6 @@ using System.Reflection; using System.Runtime.CompilerServices; -[assembly: AssemblyVersion("7.0.0.0")] - [assembly: InternalsVisibleTo("Serilog.Extensions.Logging.Tests, PublicKey=" + "0024000004800000940000000602000000240000525341310004000001000100fb8d13fd344a1c" + "6fe0fe83ef33c1080bf30690765bc6eb0df26ebfdf8f21670c64265b30db09f73a0dea5b3db4c9" + diff --git a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj index d1a4cef..e1a3a12 100644 --- a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj +++ b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj @@ -2,24 +2,16 @@ Low-level Serilog provider for Microsoft.Extensions.Logging - - 8.0.1 Microsoft;Serilog Contributors - net462;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 + net462;netstandard2.0;netstandard2.1;net8.0;net9.0 true serilog;Microsoft.Extensions.Logging serilog-extension-nuget.png https://github.com/serilog/serilog-extensions-logging Apache-2.0 - false Serilog - git - embedded - true - true - True README.md NU5118 @@ -28,26 +20,21 @@ - - + - + - $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE - - - - $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE + $(DefineConstants);FEATURE_ITUPLE - + $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE - + $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE diff --git a/src/Serilog.Extensions.Logging/SerilogLoggingBuilderExtensions.cs b/src/Serilog.Extensions.Logging/SerilogLoggingBuilderExtensions.cs index 1757de0..e7fe105 100644 --- a/src/Serilog.Extensions.Logging/SerilogLoggingBuilderExtensions.cs +++ b/src/Serilog.Extensions.Logging/SerilogLoggingBuilderExtensions.cs @@ -38,7 +38,7 @@ public static ILoggingBuilder AddSerilog(this ILoggingBuilder builder, ILogger? if (dispose) { - builder.Services.AddSingleton(services => new SerilogLoggerProvider(logger, true)); + builder.Services.AddSingleton(_ => new SerilogLoggerProvider(logger, true)); } else { diff --git a/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj b/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj index 155847e..34aa874 100644 --- a/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj +++ b/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj @@ -1,8 +1,8 @@ - net8.0 - enable + net8.0;net9.0 + false @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj b/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj index 7c1a2f8..aace2e4 100644 --- a/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj +++ b/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj @@ -1,8 +1,8 @@ - net8.0;net48 - enable + net8.0;net9.0;net48 + false @@ -14,11 +14,11 @@ - - - + + + - + From 44d0ec940c488a84e310c6c5e553c66ebb9b1926 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 25 Nov 2024 06:29:32 +1000 Subject: [PATCH 43/43] Ignore .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes .github/.DS_Store | Bin 6148 -> 0 bytes .gitignore | 5 ++++- samples/.DS_Store | Bin 6148 -> 0 bytes 4 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 .DS_Store delete mode 100644 .github/.DS_Store delete mode 100644 samples/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 76be7823983e9eaa2fd2d8e3fbfe2318c68f95b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}(1u5S|Sw*$5%!08)-C^%_Bl1XaYvg!I-EMsR3QuyK@FwY^sC5TrDc&v5O- z@C1E?K2K+VQjr8CE>ta=k!HW$*_mDM=VZqN0IV|!4ghKZz)=;Js@OauYF_D@l&obF zk?A!?FabXr9=JDh&6^DWA_MB(RbUT>5JC=5-d~(PdE6C`Sa!R9Bz^SYKYHK4wC{eN ztgjQx;1yo!Nc~z?cFVwn7*dE}0GU6{QoGxupGIMpH=Dm$X`}pMv%)KUi(k1TIdyX{ zpJW|x{8f$4rHuSV-}5iR!L(o7Ih9H7g~?#71Hxc{F1^bz3FNdRCrOa$Sl3MODzEly z_1Ub|K5B^8QFGo9vv#Z55Qm@I^Ldr;e%wDk>wk;lMBW>&82$^iY+GEw19mE~bgEl- zoJ2Ai;>;py;Y~;k5Cg=(TQgwq9lrI}8`1j205S02F`(`TfvV84SQyk-2R6C{KrEwK z3-(nlp<;wZ$6{d+XHb|4MKqzzmKe;0qg_}($6{g7gafn12eUgfTcI$$JFYLZJ21x} zwZs51u*|@Q8P-((|NL_OzZ^s(Vt^QUR}8Rn*X?%jNH({w9Zuz1fqIRqMEMm4|Ab&i ir(*O~skn}+1?@t05FLw!LA0RIhk&Mm8e-tDGVmKa5o%rl diff --git a/.github/.DS_Store b/.github/.DS_Store deleted file mode 100644 index 36b372c72cba58418798b30534145e40b446efdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5EC}5S)b+K{P2TeTCe>ioyxF0D>q@p7bOlMEfevm7_8HDTp3)NfXUV>#^56 zwmik#w*YMOxmyD(086?fzI~XR@4L_Jt|CUH^Nc+PjQGNFcbsMa9dPag86!S$_|1PG z-o|n8ZJ8910#ZN7!Kn+ zdI@6l0I?TNiHy)Jsl=pOwHTIk##`m}!YMK7uxdW6Zno-BENN(xAUn^M3Q+n4QzPpaBFdz{zWMt`My&WY~Ec~Ce+IVMIq=EBSI dQzT_x^Evl>;glG3#)D4O&w%S9lLFUP;0L2O71{s* diff --git a/.gitignore b/.gitignore index 7b17e06..febeee0 100644 --- a/.gitignore +++ b/.gitignore @@ -200,4 +200,7 @@ project.lock.json # JetBrains Rider .idea -.vscode \ No newline at end of file +.vscode + +.DS_Store + diff --git a/samples/.DS_Store b/samples/.DS_Store deleted file mode 100644 index 2312eceb69f3e5a2f0c07bef13341e9b1475a763..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJxc>Y5PhR50ye3F<^Bc#!4hIA1X84pdV<2qg+xFat29E8?WX*mPfgWLFUpBAsWnc)PXqtO{RIx&9)RF||=;6D`j0^tN1e*gdg