Skip to content

Commit 204fe13

Browse files
authored
Merge pull request #10 from nblumhardt/threadsafe-context
Make DiagnosticContextCollector thread-safe
2 parents 3b88ae0 + 55bb2a5 commit 204fe13

File tree

6 files changed

+79
-30
lines changed

6 files changed

+79
-30
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Serilog.Extensions.Hosting [![Build status](https://ci.appveyor.com/api/projects/status/ue4s7htjwj88fulh?svg=true)](https://ci.appveyor.com/project/serilog/serilog-extensions-hosting) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Extensions.Hosting.svg?style=flat)](https://www.nuget.org/packages/Serilog.Extensions.Hosting/)
22

3-
Serilog logging for Microsoft.Extensions.Hosting . This package routes Microsoft.Extensions.Hosting log messages through Serilog, so you can get information about the framework's internal operations logged to the same Serilog sinks as your application events.
3+
Serilog logging for _Microsoft.Extensions.Hosting_. This package routes framework log messages through Serilog, so you can get information about the framework's internal operations written to the same Serilog sinks as your application events.
44

55
### Instructions
66

src/Serilog.Extensions.Hosting/Extensions/Hosting/AmbientDiagnosticContextCollector.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1-
using System;
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;
216
using System.Threading;
317

418
namespace Serilog.Extensions.Hosting

src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContext.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Threading;
1617

1718
namespace Serilog.Extensions.Hosting
1819
{
1920
/// <summary>
20-
/// Implements an ambient/async-local diagnostic context. Consumers should
21-
/// use <see cref="IDiagnosticContext"/> in preference to this concrete type.
21+
/// Implements an ambient diagnostic context using <see cref="AsyncLocal{T}"/>.
2222
/// </summary>
23-
public class DiagnosticContext : IDiagnosticContext
23+
/// <remarks>Consumers should use <see cref="IDiagnosticContext"/> to set context properties.</remarks>
24+
public sealed class DiagnosticContext : IDiagnosticContext
2425
{
2526
readonly ILogger _logger;
2627

@@ -34,25 +35,25 @@ public DiagnosticContext(ILogger logger)
3435
}
3536

3637
/// <summary>
37-
/// Start collecting properties to associate with the current async context. This will replace
38+
/// Start collecting properties to associate with the current diagnostic context. This will replace
3839
/// the active collector, if any.
3940
/// </summary>
40-
/// <returns>A collector that will receive events added in the current async context.</returns>
41+
/// <returns>A collector that will receive properties added in the current diagnostic context.</returns>
4142
public DiagnosticContextCollector BeginCollection()
4243
{
4344
return AmbientDiagnosticContextCollector.Begin();
4445
}
4546

46-
/// <inheritdoc cref="IDiagnosticContext.Add"/>
47-
public void Add(string propertyName, object value, bool destructureObjects = false)
47+
/// <inheritdoc cref="IDiagnosticContext.Set"/>
48+
public void Set(string propertyName, object value, bool destructureObjects = false)
4849
{
4950
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
5051

5152
var collector = AmbientDiagnosticContextCollector.Current;
5253
if (collector != null &&
5354
(_logger ?? Log.Logger).BindProperty(propertyName, value, destructureObjects, out var property))
5455
{
55-
collector.Add(property);
56+
collector.AddOrUpdate(property);
5657
}
5758
}
5859
}

src/Serilog.Extensions.Hosting/Extensions/Hosting/DiagnosticContextCollector.cs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,33 @@ namespace Serilog.Extensions.Hosting
99
/// </summary>
1010
public sealed class DiagnosticContextCollector : IDisposable
1111
{
12-
readonly AmbientDiagnosticContextCollector _ambientCollector;
13-
List<LogEventProperty> _properties = new List<LogEventProperty>();
12+
readonly IDisposable _chainedDisposable;
13+
readonly object _propertiesLock = new object();
14+
Dictionary<string, LogEventProperty> _properties = new Dictionary<string, LogEventProperty>();
1415

15-
internal DiagnosticContextCollector(AmbientDiagnosticContextCollector ambientCollector)
16+
/// <summary>
17+
/// Construct a <see cref="DiagnosticContextCollector"/>.
18+
/// </summary>
19+
/// <param name="chainedDisposable">An object that will be disposed to signal completion/disposal of
20+
/// the collector.</param>
21+
public DiagnosticContextCollector(IDisposable chainedDisposable)
1622
{
17-
_ambientCollector = ambientCollector ?? throw new ArgumentNullException(nameof(ambientCollector));
23+
_chainedDisposable = chainedDisposable ?? throw new ArgumentNullException(nameof(chainedDisposable));
1824
}
1925

2026
/// <summary>
2127
/// Add the property to the context.
2228
/// </summary>
2329
/// <param name="property">The property to add.</param>
24-
public void Add(LogEventProperty property)
30+
public void AddOrUpdate(LogEventProperty property)
2531
{
2632
if (property == null) throw new ArgumentNullException(nameof(property));
27-
_properties?.Add(property);
33+
34+
lock (_propertiesLock)
35+
{
36+
if (_properties == null) return;
37+
_properties[property.Name] = property;
38+
}
2839
}
2940

3041
/// <summary>
@@ -34,18 +45,21 @@ public void Add(LogEventProperty property)
3445
/// </summary>
3546
/// <param name="properties">The collected properties, or null if no collection is active.</param>
3647
/// <returns>True if properties could be collected.</returns>
37-
public bool TryComplete(out List<LogEventProperty> properties)
48+
public bool TryComplete(out IEnumerable<LogEventProperty> properties)
3849
{
39-
properties = _properties;
40-
_properties = null;
41-
Dispose();
42-
return properties != null;
50+
lock (_propertiesLock)
51+
{
52+
properties = _properties?.Values;
53+
_properties = null;
54+
Dispose();
55+
return properties != null;
56+
}
4357
}
4458

4559
/// <inheritdoc/>
4660
public void Dispose()
4761
{
48-
_ambientCollector.Dispose();
62+
_chainedDisposable.Dispose();
4963
}
5064
}
5165
}

src/Serilog.Extensions.Hosting/IDiagnosticContext.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ namespace Serilog
2020
public interface IDiagnosticContext
2121
{
2222
/// <summary>
23-
/// Add the specified property to the request's diagnostic payload.
23+
/// Set the specified property on the current diagnostic context. The property will be collected
24+
/// and attached to the event emitted at the completion of the context.
2425
/// </summary>
2526
/// <param name="propertyName">The name of the property. Must be non-empty.</param>
2627
/// <param name="value">The property value.</param>
2728
/// <param name="destructureObjects">If true, the value will be serialized as structured
2829
/// 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+
void Set(string propertyName, object value, bool destructureObjects = false);
3031
}
3132
}
Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
using System;
2+
using System.Linq;
23
using System.Threading.Tasks;
4+
using Serilog.Events;
35
using Serilog.Extensions.Hosting.Tests.Support;
46
using Xunit;
57

8+
// ReSharper disable PossibleNullReferenceException
9+
610
namespace Serilog.Extensions.Hosting.Tests
711
{
812
public class DiagnosticContextTests
913
{
1014
[Fact]
11-
public void AddIsSafeWhenNoContextIsActive()
15+
public void SetIsSafeWhenNoContextIsActive()
1216
{
1317
var dc = new DiagnosticContext(Some.Logger());
14-
dc.Add(Some.String("name"), Some.Int32());
18+
dc.Set(Some.String("name"), Some.Int32());
1519
}
1620

1721
[Fact]
@@ -20,19 +24,34 @@ public async Task PropertiesAreCollectedInAnActiveContext()
2024
var dc = new DiagnosticContext(Some.Logger());
2125

2226
var collector = dc.BeginCollection();
23-
dc.Add(Some.String("name"), Some.Int32());
27+
dc.Set(Some.String("first"), Some.Int32());
2428
await Task.Delay(TimeSpan.FromMilliseconds(10));
25-
dc.Add(Some.String("name"), Some.Int32());
29+
dc.Set(Some.String("second"), Some.Int32());
2630

2731
Assert.True(collector.TryComplete(out var properties));
28-
Assert.Equal(2, properties.Count);
32+
Assert.Equal(2, properties.Count());
2933

3034
Assert.False(collector.TryComplete(out _));
3135

3236
collector.Dispose();
3337

34-
dc.Add(Some.String("name"), Some.Int32());
38+
dc.Set(Some.String("third"), Some.Int32());
3539
Assert.False(collector.TryComplete(out _));
3640
}
41+
42+
[Fact]
43+
public void ExistingPropertiesCanBeUpdated()
44+
{
45+
var dc = new DiagnosticContext(Some.Logger());
46+
47+
var collector = dc.BeginCollection();
48+
dc.Set("name", 10);
49+
dc.Set("name", 20);
50+
51+
Assert.True(collector.TryComplete(out var properties));
52+
var prop = Assert.Single(properties);
53+
var scalar = Assert.IsType<ScalarValue>(prop.Value);
54+
Assert.Equal(20, scalar.Value);
55+
}
3756
}
3857
}

0 commit comments

Comments
 (0)