Skip to content

Commit 3b56cb4

Browse files
authored
Merge pull request #246 from david-obee/support-external-scopes
Implement `ISupportExternalScope` in `SerilogLoggerProvider`
2 parents 7dc58c3 + 49fa3e9 commit 3b56cb4

File tree

5 files changed

+177
-2
lines changed

5 files changed

+177
-2
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Diagnostics;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using Serilog;
5+
using Serilog.Formatting.Json;
6+
7+
// Configure a JsonFormatter to log out scope to the console
8+
Log.Logger = new LoggerConfiguration()
9+
.MinimumLevel.Debug()
10+
.WriteTo.Console(new JsonFormatter())
11+
.CreateLogger();
12+
13+
// Setup Serilog with M.E.L, and configure the appropriate ActivityTrackingOptions
14+
var services = new ServiceCollection();
15+
16+
services.AddLogging(l => l
17+
.AddSerilog()
18+
.Configure(options =>
19+
{
20+
options.ActivityTrackingOptions =
21+
ActivityTrackingOptions.SpanId
22+
| ActivityTrackingOptions.TraceId
23+
| ActivityTrackingOptions.ParentId
24+
| ActivityTrackingOptions.TraceState
25+
| ActivityTrackingOptions.TraceFlags
26+
| ActivityTrackingOptions.Tags
27+
| ActivityTrackingOptions.Baggage;
28+
}));
29+
30+
// Add an ActivityListener (required, otherwise Activities don't actually get created if nothing is listening to them)
31+
ActivitySource.AddActivityListener(new ActivityListener
32+
{
33+
ShouldListenTo = source => true,
34+
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllDataAndRecorded
35+
});
36+
37+
// Run our test
38+
var activitySource = new ActivitySource("SomeActivitySource");
39+
40+
var serviceProvider = services.BuildServiceProvider();
41+
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
42+
43+
using var activity = activitySource.StartActivity();
44+
45+
activity?.SetTag("tag.domain.id", 1234);
46+
activity?.SetBaggage("baggage.environment", "uat");
47+
48+
using var scope = logger.BeginScope(new
49+
{
50+
User = "Hugh Mann",
51+
Time = DateTimeOffset.UtcNow
52+
});
53+
54+
logger.LogInformation("Hello world!");
55+
56+
serviceProvider.Dispose();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<OutputType>Exe</OutputType>
8+
<Nullable>enable</Nullable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\..\src\Serilog.Extensions.Logging\Serilog.Extensions.Logging.csproj" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
17+
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
18+
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
19+
</ItemGroup>
20+
21+
</Project>

serilog-extensions-logging.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{9C21B9
3131
EndProject
3232
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}"
3333
EndProject
34+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWithExternalScope", "samples\SampleWithExternalScope\SampleWithExternalScope.csproj", "{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}"
35+
EndProject
3436
Global
3537
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3638
Debug|Any CPU = Debug|Any CPU
@@ -53,6 +55,10 @@ Global
5355
{6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
5456
{6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
5557
{6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}.Release|Any CPU.Build.0 = Release|Any CPU
58+
{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59+
{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
60+
{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
61+
{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Release|Any CPU.Build.0 = Release|Any CPU
5662
EndGlobalSection
5763
GlobalSection(SolutionProperties) = preSolution
5864
HideSolutionNode = FALSE
@@ -62,6 +68,7 @@ Global
6268
{37EADF84-5E41-4224-A194-1E3299DCD0B8} = {E30F638E-BBBE-4AD1-93CE-48CC69CFEFE1}
6369
{65357FBC-9BC4-466D-B621-1C3A19BC2A78} = {F2407211-6043-439C-8E06-3641634332E7}
6470
{6D5986FF-EECD-4E75-8BC6-A5F78AB549B2} = {E30F638E-BBBE-4AD1-93CE-48CC69CFEFE1}
71+
{653092A8-CBAD-40AA-A4CE-F8B19D6492C2} = {F2407211-6043-439C-8E06-3641634332E7}
6572
EndGlobalSection
6673
GlobalSection(ExtensibilityGlobals) = postSolution
6774
SolutionGuid = {811E61C5-3871-4633-AFAE-B35B619C8A10}

src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ namespace Serilog.Extensions.Logging;
1313
/// An <see cref="ILoggerProvider"/> that pipes events through Serilog.
1414
/// </summary>
1515
[ProviderAlias("Serilog")]
16-
public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher
16+
public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISupportExternalScope
1717
{
1818
internal const string OriginalFormatPropertyName = "{OriginalFormat}";
1919
internal const string ScopePropertyName = "Scope";
2020

2121
// May be null; if it is, Log.Logger will be lazily used
2222
readonly ILogger? _logger;
2323
readonly Action? _dispose;
24+
private IExternalScopeProvider? _externalScopeProvider;
2425

2526
/// <summary>
2627
/// Construct a <see cref="SerilogLoggerProvider"/>.
@@ -75,13 +76,32 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
7576
}
7677
}
7778

79+
_externalScopeProvider?.ForEachScope((state, accumulatingLogEvent) =>
80+
{
81+
var scope = new SerilogLoggerScope(this, state);
82+
83+
scope.EnrichAndCreateScopeItem(accumulatingLogEvent, propertyFactory, out var scopeItem);
84+
85+
if (scopeItem != null)
86+
{
87+
scopeItems ??= new List<LogEventPropertyValue>();
88+
scopeItems.Add(scopeItem);
89+
}
90+
}, logEvent);
91+
7892
if (scopeItems != null)
7993
{
8094
scopeItems.Reverse();
8195
logEvent.AddPropertyIfAbsent(new LogEventProperty(ScopePropertyName, new SequenceValue(scopeItems)));
8296
}
8397
}
8498

99+
/// <inheritdoc />
100+
public void SetScopeProvider(IExternalScopeProvider scopeProvider)
101+
{
102+
_externalScopeProvider = scopeProvider;
103+
}
104+
85105
readonly AsyncLocal<SerilogLoggerScope?> _value = new();
86106

87107
internal SerilogLoggerScope? CurrentScope

test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class SerilogLoggerTest
1717
const string Name = "test";
1818
const string TestMessage = "This is a test";
1919

20-
static Tuple<SerilogLogger, SerilogSink> SetUp(LogLevel logLevel)
20+
static Tuple<SerilogLogger, SerilogSink> SetUp(LogLevel logLevel, IExternalScopeProvider? externalScopeProvider = null)
2121
{
2222
var sink = new SerilogSink();
2323

@@ -29,6 +29,11 @@ static Tuple<SerilogLogger, SerilogSink> SetUp(LogLevel logLevel)
2929
var provider = new SerilogLoggerProvider(serilogLogger);
3030
var logger = (SerilogLogger)provider.CreateLogger(Name);
3131

32+
if (externalScopeProvider is not null)
33+
{
34+
provider.SetScopeProvider(externalScopeProvider);
35+
}
36+
3237
return new Tuple<SerilogLogger, SerilogSink>(logger, sink);
3338
}
3439

@@ -397,6 +402,35 @@ public void NamedScopesAreCaptured()
397402
Assert.Equal("Inner", items[1]);
398403
}
399404

405+
[Fact]
406+
public void ExternalScopesAreCaptured()
407+
{
408+
var externalScopeProvider = new FakeExternalScopeProvider();
409+
var (logger, sink) = SetUp(LogLevel.Trace, externalScopeProvider);
410+
411+
externalScopeProvider.Push(new Dictionary<string, int>()
412+
{
413+
{ "FirstKey", 1 },
414+
{ "SecondKey", 2 }
415+
});
416+
417+
var scopeObject = new { ObjectKey = "Some value" };
418+
externalScopeProvider.Push(scopeObject);
419+
420+
logger.Log(LogLevel.Information, 0, TestMessage, null!, null!);
421+
422+
Assert.Single(sink.Writes);
423+
Assert.True(sink.Writes[0].Properties.TryGetValue(SerilogLoggerProvider.ScopePropertyName, out var scopeValue));
424+
var sequence = Assert.IsType<SequenceValue>(scopeValue);
425+
426+
var objectScope = (ScalarValue) sequence.Elements.Single(e => e is ScalarValue);
427+
Assert.Equal(scopeObject.ToString(), (string?)objectScope.Value);
428+
429+
var dictionaryScope = (DictionaryValue) sequence.Elements.Single(e => e is DictionaryValue);
430+
Assert.Equal(1, ((ScalarValue)dictionaryScope.Elements.Single(pair => pair.Key.Value!.Equals("FirstKey")).Value).Value);
431+
Assert.Equal(2, ((ScalarValue)dictionaryScope.Elements.Single(pair => pair.Key.Value!.Equals("SecondKey")).Value).Value);
432+
}
433+
400434
class FoodScope : IEnumerable<KeyValuePair<string, object>>
401435
{
402436
readonly string _name;
@@ -446,6 +480,43 @@ class Person
446480
public string? LastName { get; set; }
447481
}
448482

483+
class FakeExternalScopeProvider : IExternalScopeProvider
484+
{
485+
private readonly List<Scope> _scopes = new List<Scope>();
486+
487+
public void ForEachScope<TState>(Action<object?, TState> callback, TState state)
488+
{
489+
foreach (var scope in _scopes)
490+
{
491+
if (scope.IsDisposed) continue;
492+
callback(scope.Value, state);
493+
}
494+
}
495+
496+
public IDisposable Push(object? state)
497+
{
498+
var scope = new Scope(state);
499+
_scopes.Add(scope);
500+
return scope;
501+
}
502+
503+
private class Scope : IDisposable
504+
{
505+
public bool IsDisposed { get; set; } = false;
506+
public object? Value { get; set; }
507+
508+
public Scope(object? value)
509+
{
510+
Value = value;
511+
}
512+
513+
public void Dispose()
514+
{
515+
IsDisposed = true;
516+
}
517+
}
518+
}
519+
449520
[Theory]
450521
[InlineData(1)]
451522
[InlineData(10)]

0 commit comments

Comments
 (0)