Skip to content

Commit ac4f4cd

Browse files
committed
Diagnostic context infra for downstream middleware
1 parent 1f401ac commit ac4f4cd

12 files changed

+253
-21
lines changed

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "2.1.300-*"
3+
"version": "2.2.105"
44
}
55
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
22
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
3+
<s:Boolean x:Key="/Default/UserDictionary/Words/=benaadams/@EntryIndexedValue">True</s:Boolean>
4+
<s:Boolean x:Key="/Default/UserDictionary/Words/=destructure/@EntryIndexedValue">True</s:Boolean>
35
<s:Boolean x:Key="/Default/UserDictionary/Words/=Serilog/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Threading;
3+
4+
namespace Serilog.Extensions.Hosting
5+
{
6+
class AmbientDiagnosticContextCollector : IDisposable
7+
{
8+
static readonly AsyncLocal<AmbientDiagnosticContextCollector> AmbientCollector =
9+
new AsyncLocal<AmbientDiagnosticContextCollector>();
10+
11+
// The indirection here ensures that completing collection cleans up the collector in all
12+
// execution contexts. Via @benaadams' addition to `HttpContextAccessor` :-)
13+
DiagnosticContextCollector _collector;
14+
15+
public static DiagnosticContextCollector Current => AmbientCollector.Value?._collector;
16+
17+
public static DiagnosticContextCollector Begin()
18+
{
19+
var value = new AmbientDiagnosticContextCollector();
20+
value._collector = new DiagnosticContextCollector(value);
21+
AmbientCollector.Value = value;
22+
return value._collector;
23+
}
24+
25+
public void Dispose()
26+
{
27+
_collector = null;
28+
if (AmbientCollector.Value == this)
29+
AmbientCollector.Value = null;
30+
}
31+
}
32+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2019 Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
17+
namespace Serilog.Extensions.Hosting
18+
{
19+
/// <summary>
20+
/// Implements an ambient/async-local diagnostic context. Consumers should
21+
/// use <see cref="IDiagnosticContext"/> in preference to this concrete type.
22+
/// </summary>
23+
public class DiagnosticContext : IDiagnosticContext
24+
{
25+
readonly ILogger _log;
26+
27+
/// <summary>
28+
/// Construct a <see cref="DiagnosticContext"/>.
29+
/// </summary>
30+
/// <param name="log">A logger for binding properties in the context, or <c>null</c> to use <see cref="Log.Logger"/>.</param>
31+
public DiagnosticContext(ILogger log)
32+
{
33+
_log = log;
34+
}
35+
36+
/// <summary>
37+
/// Start collecting properties to associate with the current async context. This will replace
38+
/// the active collector, if any.
39+
/// </summary>
40+
/// <returns>A collector that will receive events added in the current async context.</returns>
41+
public DiagnosticContextCollector BeginCollection()
42+
{
43+
return AmbientDiagnosticContextCollector.Begin();
44+
}
45+
46+
/// <inheritdoc cref="IDiagnosticContext.Add"/>
47+
public void Add(string propertyName, object value, bool destructureObjects = false)
48+
{
49+
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
50+
51+
var collector = AmbientDiagnosticContextCollector.Current;
52+
if (collector != null &&
53+
(_log ?? Log.Logger).BindProperty(propertyName, value, destructureObjects, out var property))
54+
{
55+
collector.Add(property);
56+
}
57+
}
58+
}
59+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Serilog.Events;
4+
5+
namespace Serilog.Extensions.Hosting
6+
{
7+
/// <summary>
8+
/// A container that receives properties added to a diagnostic context.
9+
/// </summary>
10+
public sealed class DiagnosticContextCollector : IDisposable
11+
{
12+
readonly AmbientDiagnosticContextCollector _ambientCollector;
13+
List<LogEventProperty> _properties = new List<LogEventProperty>();
14+
15+
internal DiagnosticContextCollector(AmbientDiagnosticContextCollector ambientCollector)
16+
{
17+
_ambientCollector = ambientCollector ?? throw new ArgumentNullException(nameof(ambientCollector));
18+
}
19+
20+
/// <summary>
21+
/// Add the property to the context.
22+
/// </summary>
23+
/// <param name="property">The property to add.</param>
24+
public void Add(LogEventProperty property)
25+
{
26+
if (property == null) throw new ArgumentNullException(nameof(property));
27+
_properties?.Add(property);
28+
}
29+
30+
/// <summary>
31+
/// Complete the context and retrieve the properties added to it, if any. This will
32+
/// stop collection and remove the collector from the original execution context and
33+
/// any of its children.
34+
/// </summary>
35+
/// <param name="properties">The collected properties, or null if no collection is active.</param>
36+
/// <returns>True if properties could be collected.</returns>
37+
public bool TryComplete(out List<LogEventProperty> properties)
38+
{
39+
properties = _properties;
40+
_properties = null;
41+
Dispose();
42+
return properties != null;
43+
}
44+
45+
/// <inheritdoc/>
46+
public void Dispose()
47+
{
48+
_ambientCollector.Dispose();
49+
}
50+
}
51+
}

src/Serilog.Extensions.Hosting/Extensions/Hosting/SerilogLoggerFactory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.ComponentModel;
1617
using Microsoft.Extensions.Logging;
1718
using Serilog.Debugging;
1819
using Serilog.Extensions.Logging;
@@ -26,6 +27,7 @@ namespace Serilog.Hosting
2627
/// Implements <see cref="ILoggerFactory"/> so that we can inject Serilog Logger.
2728
/// </summary>
2829
[Obsolete("Replaced with Serilog.Extensions.Logging.SerilogLoggerFactory")]
30+
[EditorBrowsable(EditorBrowsableState.Never)]
2931
public class SerilogLoggerFactory : ILoggerFactory
3032
{
3133
readonly SerilogLoggerProvider _provider;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2019 Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace Serilog
16+
{
17+
/// <summary>
18+
/// Collects diagnostic information for packaging into wide events.
19+
/// </summary>
20+
public interface IDiagnosticContext
21+
{
22+
/// <summary>
23+
/// Add the specified property to the request's diagnostic payload.
24+
/// </summary>
25+
/// <param name="propertyName">The name of the property. Must be non-empty.</param>
26+
/// <param name="value">The property value.</param>
27+
/// <param name="destructureObjects">If true, the value will be serialized as structured
28+
/// data if possible; if false, the object will be recorded as a scalar or simple array.</param>
29+
void Add(string propertyName, object value, bool destructureObjects = false);
30+
}
31+
}

src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ public static IHostBuilder UseSerilog(
6363
{
6464
collection.AddSingleton<ILoggerFactory>(services => new SerilogLoggerFactory(logger, dispose));
6565
}
66+
67+
if (logger != null)
68+
{
69+
// This won't (and shouldn't) take ownership of the logger.
70+
collection.AddSingleton(logger);
71+
}
6672
});
6773

6874
return builder;
@@ -103,6 +109,9 @@ public static IHostBuilder UseSerilog(
103109
configureLogger(context, loggerConfiguration);
104110
var logger = loggerConfiguration.CreateLogger();
105111

112+
// This won't (and shouldn't) take ownership of the logger.
113+
collection.AddSingleton(logger);
114+
106115
ILogger registeredLogger = null;
107116
if (preserveStaticLogger)
108117
{
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Serilog.Extensions.Hosting.Tests.Support;
4+
using Xunit;
5+
6+
namespace Serilog.Extensions.Hosting.Tests
7+
{
8+
public class DiagnosticContextTests
9+
{
10+
[Fact]
11+
public void AddIsSafeWhenNoContextIsActive()
12+
{
13+
var dc = new DiagnosticContext(Some.Logger());
14+
dc.Add(Some.String("name"), Some.Int32());
15+
}
16+
17+
[Fact]
18+
public async Task PropertiesAreCollectedInAnActiveContext()
19+
{
20+
var dc = new DiagnosticContext(Some.Logger());
21+
22+
var collector = dc.BeginCollection();
23+
dc.Add(Some.String("name"), Some.Int32());
24+
await Task.Delay(TimeSpan.FromMilliseconds(10));
25+
dc.Add(Some.String("name"), Some.Int32());
26+
27+
Assert.True(collector.TryComplete(out var properties));
28+
Assert.Equal(2, properties.Count);
29+
30+
Assert.False(collector.TryComplete(out _));
31+
32+
collector.Dispose();
33+
34+
dc.Add(Some.String("name"), Some.Int32());
35+
Assert.False(collector.TryComplete(out _));
36+
}
37+
}
38+
}

test/Serilog.Extensions.Hosting.Tests/Serilog.Extensions.Hosting.Tests.csproj

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netcoreapp2.1</TargetFrameworks>
4+
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
55
<AssemblyName>Serilog.Extensions.Hosting.Tests</AssemblyName>
66
<AssemblyOriginatorKeyFile>../../assets/Serilog.snk</AssemblyOriginatorKeyFile>
77
<SignAssembly>true</SignAssembly>
@@ -14,9 +14,12 @@
1414
</ItemGroup>
1515

1616
<ItemGroup>
17-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
18-
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
19-
<PackageReference Include="xunit" Version="2.2.0" />
17+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.1.1" />
18+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
19+
<PrivateAssets>all</PrivateAssets>
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
</PackageReference>
22+
<PackageReference Include="xunit" Version="2.4.1" />
2023
</ItemGroup>
2124

2225
</Project>

0 commit comments

Comments
 (0)