Skip to content

Support ValueTuple<string, object?> as state of scope. #232

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -144,6 +144,23 @@ using (_logger.BeginScope(scopeProps) {
// }
```

Alternatively provide a `ValueTuple<string, object?>` 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,9 @@ public void Dispose()

public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, out LogEventPropertyValue? scopeItem)
{
void AddProperty(KeyValuePair<string, object> stateProperty)
void AddProperty(string key, object? value)
{
var key = stateProperty.Key;
var destructureObject = false;
var value = stateProperty.Value;

if (key.StartsWith("@"))
{
Expand Down Expand Up @@ -86,7 +84,7 @@ void AddProperty(KeyValuePair<string, object> 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<KeyValuePair<string, object>> stateProperties)
Expand All @@ -98,9 +96,18 @@ void AddProperty(KeyValuePair<string, object> 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<string, object?> 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;
Expand Down
100 changes: 100 additions & 0 deletions test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs
Original file line number Diff line number Diff line change
@@ -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<Parsing.MessageTemplateToken>());
var properties = Enumerable.Empty<LogEventProperty>();
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<string, object?>() { { 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<string, object?>[] { new KeyValuePair<string, object?>(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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}