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. 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..442d737 --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs @@ -0,0 +1,100 @@ +// Copyright © Serilog Contributors +// 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..12d34af --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs @@ -0,0 +1,15 @@ +// Copyright © Serilog Contributors +// 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); + } +}