Skip to content

Commit cd95c61

Browse files
authored
Merge pull request #192 from serilog/dev
3.1.0 Release
2 parents 60bcdd9 + 2a3afbb commit cd95c61

18 files changed

+378
-46
lines changed

CHANGES.md

-12
This file was deleted.

README.md

+69-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# Serilog.Extensions.Logging [![Build status](https://ci.appveyor.com/api/projects/status/865nohxfiq1rnby0/branch/master?svg=true)](https://ci.appveyor.com/project/serilog/serilog-framework-logging/history) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Extensions.Logging.svg?style=flat)](https://www.nuget.org/packages/Serilog.Extensions.Logging/)
22

3-
43
A Serilog provider for [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging), the logging subsystem used by ASP.NET Core.
54

6-
### ASP.NET Core 2.0+ Instructions
5+
### ASP.NET Core Instructions
6+
7+
**ASP.NET Core** applications should prefer [Serilog.AspNetCore](https://github.com/serilog/serilog-aspnetcore) and `UseSerilog()` instead.
78

8-
**ASP.NET Core 2.0** applications should prefer [Serilog.AspNetCore](https://github.com/serilog/serilog-aspnetcore) and `UseSerilog()` instead.
9+
### Non-web .NET Core Instructions
10+
11+
**Non-web .NET Core** applications should prefer [Serilog.Extensions.Hosting](https://github.com/serilog/serilog-extensions-hosting) and `UseSerilog()` instead.
912

1013
### .NET Core 1.0, 1.1 and Default Provider Integration
1114

@@ -77,6 +80,69 @@ That's it! With the level bumped up a little you should see log output like:
7780
[22:14:45.741 DBG] Handled. Status code: 304 File: /css/site.css
7881
```
7982

83+
### Notes on Log Scopes
84+
85+
_Microsoft.Extensions.Logging_ provides the `BeginScope` API, which can be used to add arbitrary properties to log events within a certain region of code. The API comes in two forms:
86+
87+
1. The method: `IDisposable BeginScope<TState>(TState state)`
88+
2. The extension method: `IDisposable BeginScope(this ILogger logger, string messageFormat, params object[] args)`
89+
90+
Using the extension method will add a `Scope` property to your log events. This is most useful for adding simple "scope strings" to your events, as in the following code:
91+
92+
```cs
93+
using (_logger.BeginScope("Transaction")) {
94+
_logger.LogInformation("Beginning...");
95+
_logger.LogInformation("Completed in {DurationMs}ms...", 30);
96+
}
97+
// Example JSON output:
98+
// {"@t":"2020-10-29T19:05:56.4126822Z","@m":"Beginning...","@i":"f6a328e9","SourceContext":"SomeNamespace.SomeService","Scope":["Transaction"]}
99+
// {"@t":"2020-10-29T19:05:56.4176816Z","@m":"Completed in 30ms...","@i":"51812baa","DurationMs":30,"SourceContext":"SomeNamespace.SomeService","Scope":["Transaction"]}
100+
```
101+
102+
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:
103+
104+
```cs
105+
// WRONG! Prefer the dictionary approach below instead
106+
using (_logger.BeginScope("TransactionId: {TransactionId}, ResponseJson: {ResponseJson}", 12345, jsonString)) {
107+
_logger.LogInformation("Completed in {DurationMs}ms...", 30);
108+
}
109+
// Example JSON output:
110+
// {
111+
// "@t":"2020-10-29T19:05:56.4176816Z",
112+
// "@m":"Completed in 30ms...",
113+
// "@i":"51812baa",
114+
// "DurationMs":30,
115+
// "SourceContext":"SomeNamespace.SomeService",
116+
// "TransactionId": 12345,
117+
// "ResponseJson": "{ \"Key1\": \"Value1\", \"Key2\": \"Value2\" }",
118+
// "Scope":["TransactionId: 12345, ResponseJson: { \"Key1\": \"Value1\", \"Key2\": \"Value2\" }"]
119+
// }
120+
```
121+
122+
Not only does this add the unnecessary `Scope` property to your event, but it also duplicates serialized values between `Scope` and the intended properties, as you can see here with `ResponseJson`. If this were "real" JSON like an API response, then a potentially very large block of text would be duplicated within your log event! Moreover, the template string within `BeginScope` is rather arbitrary when all you want to do is add a bag of properties, and you start mixing enriching concerns with formatting concerns.
123+
124+
A far better alternative is to use the `BeginScope<TState>(TState state)` method. If you provide any `IEnumerable<KeyValuePair<string, object>>` to this method, then Serilog will output the key/value pairs as structured properties _without_ the `Scope` property, as in this example:
125+
126+
```cs
127+
var scopeProps = new Dictionary<string, object> {
128+
{ "TransactionId", 12345 },
129+
{ "ResponseJson", jsonString },
130+
};
131+
using (_logger.BeginScope(scopeProps) {
132+
_logger.LogInformation("Transaction completed in {DurationMs}ms...", 30);
133+
}
134+
// Example JSON output:
135+
// {
136+
// "@t":"2020-10-29T19:05:56.4176816Z",
137+
// "@m":"Completed in 30ms...",
138+
// "@i":"51812baa",
139+
// "DurationMs":30,
140+
// "SourceContext":"SomeNamespace.SomeService",
141+
// "TransactionId": 12345,
142+
// "ResponseJson": "{ \"Key1\": \"Value1\", \"Key2\": \"Value2\" }"
143+
// }
144+
```
145+
80146
### Credits
81147

82148
This package evolved from an earlier package _Microsoft.Framework.Logging.Serilog_ [provided by the ASP.NET team](https://github.com/aspnet/Logging/pull/182).

appveyor.yml

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
version: '{build}'
22
skip_tags: true
33
image: Visual Studio 2019
4-
configuration: Release
54
install:
65
- ps: mkdir -Force ".\build\" | Out-Null
7-
#- ps: Invoke-WebRequest "https://raw.githubusercontent.com/dotnet/cli/release/2.0.0/scripts/obtain/dotnet-install.ps1" -OutFile ".\build\installcli.ps1"
8-
#- ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetcli"
9-
#- ps: '& .\build\installcli.ps1 -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath -Version 2.0.0-preview2-006497'
10-
#- ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path"
116
build_script:
127
- ps: ./Build.ps1
138
test: off
@@ -16,7 +11,7 @@ artifacts:
1611
deploy:
1712
- provider: NuGet
1813
api_key:
19-
secure: N59tiJECUYpip6tEn0xvdmDAEiP9SIzyLEFLpwiigm/8WhJvBNs13QxzT1/3/JW/
14+
secure: kYR3BYzJm3wSFbFjPhgTzuDHHcE8ApoNUmHvJvunWZ39nyrqVk6J6srjzYVQ7/gX
2015
skip_symbols: true
2116
on:
2217
branch: /^(master|dev)$/

assets/serilog-extension-nuget.png

5.87 KB
Loading

global.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"sdk": {
3-
"version": "2.2.103"
3+
"allowPrerelease": false,
4+
"version": "3.1.100",
5+
"rollForward": "latestFeature"
46
}
57
}

serilog-extensions-logging.sln.DotSettings

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88
<s:Boolean x:Key="/Default/UserDictionary/Words/=Loggable/@EntryIndexedValue">True</s:Boolean>
99
<s:Boolean x:Key="/Default/UserDictionary/Words/=Nonscalar/@EntryIndexedValue">True</s:Boolean>
1010
<s:Boolean x:Key="/Default/UserDictionary/Words/=Serilog/@EntryIndexedValue">True</s:Boolean>
11-
<s:Boolean x:Key="/Default/UserDictionary/Words/=sobj/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
11+
<s:Boolean x:Key="/Default/UserDictionary/Words/=sobj/@EntryIndexedValue">True</s:Boolean>
12+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Stringification/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) 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+
16+
using System;
17+
using Serilog.Events;
18+
using Serilog.Parsing;
19+
using System.Collections;
20+
21+
namespace Serilog.Extensions.Logging
22+
{
23+
class CachingMessageTemplateParser
24+
{
25+
readonly MessageTemplateParser _innerParser = new MessageTemplateParser();
26+
27+
readonly object _templatesLock = new object();
28+
readonly Hashtable _templates = new Hashtable();
29+
30+
const int MaxCacheItems = 1000;
31+
const int MaxCachedTemplateLength = 1024;
32+
33+
public MessageTemplate Parse(string messageTemplate)
34+
{
35+
if (messageTemplate == null) throw new ArgumentNullException(nameof(messageTemplate));
36+
37+
if (messageTemplate.Length > MaxCachedTemplateLength)
38+
return _innerParser.Parse(messageTemplate);
39+
40+
// ReSharper disable once InconsistentlySynchronizedField
41+
// ignored warning because this is by design
42+
var result = (MessageTemplate)_templates[messageTemplate];
43+
if (result != null)
44+
return result;
45+
46+
result = _innerParser.Parse(messageTemplate);
47+
48+
lock (_templatesLock)
49+
{
50+
// Exceeding MaxCacheItems is *not* the sunny day scenario; all we're doing here is preventing out-of-memory
51+
// conditions when the library is used incorrectly. Correct use (templates, rather than
52+
// direct message strings) should barely, if ever, overflow this cache.
53+
54+
// Changing workloads through the lifecycle of an app instance mean we can gain some ground by
55+
// potentially dropping templates generated only in startup, or only during specific infrequent
56+
// activities.
57+
58+
if (_templates.Count == MaxCacheItems)
59+
_templates.Clear();
60+
61+
_templates[messageTemplate] = result;
62+
}
63+
64+
return result;
65+
}
66+
}
67+
}

src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace Serilog.Extensions.Logging
2626
/// </summary>
2727
public class LoggerProviderCollection : IDisposable
2828
{
29-
volatile ILoggerProvider[] _providers = new ILoggerProvider[0];
29+
volatile ILoggerProvider[] _providers = Array.Empty<ILoggerProvider>();
3030

3131
/// <summary>
3232
/// Add <paramref name="provider"/> to the collection.
@@ -36,16 +36,16 @@ public void AddProvider(ILoggerProvider provider)
3636
{
3737
if (provider == null) throw new ArgumentNullException(nameof(provider));
3838

39-
var existing = _providers;
40-
var added = existing.Concat(new[] {provider}).ToArray();
39+
ILoggerProvider[] existing, added;
4140

42-
#pragma warning disable 420 // ref to a volatile field
43-
while (Interlocked.CompareExchange(ref _providers, added, existing) != existing)
44-
#pragma warning restore 420
41+
do
4542
{
4643
existing = _providers;
4744
added = existing.Concat(new[] { provider }).ToArray();
4845
}
46+
#pragma warning disable 420 // ref to a volatile field
47+
while (Interlocked.CompareExchange(ref _providers, added, existing) != existing);
48+
#pragma warning restore 420
4949
}
5050

5151
/// <summary>

src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs

+15-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using Microsoft.Extensions.Logging;
1617
using Serilog.Core;
1718
using Serilog.Events;
1819

@@ -30,13 +31,26 @@ public LoggerProviderCollectionSink(LoggerProviderCollection providers)
3031
public void Emit(LogEvent logEvent)
3132
{
3233
string categoryName = null;
34+
EventId eventId = default;
3335

3436
if (logEvent.Properties.TryGetValue("SourceContext", out var sourceContextProperty) &&
3537
sourceContextProperty is ScalarValue sourceContextValue &&
3638
sourceContextValue.Value is string sourceContext)
3739
{
3840
categoryName = sourceContext;
3941
}
42+
if (logEvent.Properties.TryGetValue("EventId", out var eventIdPropertyValue) && eventIdPropertyValue is StructureValue structuredEventId)
43+
{
44+
string name = null;
45+
var id = 0;
46+
foreach (var item in structuredEventId.Properties)
47+
{
48+
if (item.Name == "Id" && item.Value is ScalarValue sv && sv.Value is int i) id = i;
49+
if (item.Name == "Name" && item.Value is ScalarValue sv2 && sv2.Value is string s) name = s;
50+
}
51+
52+
eventId = new EventId(id, name);
53+
}
4054

4155
var level = LevelConvert.ToExtensionsLevel(logEvent.Level);
4256
var slv = new SerilogLogValues(logEvent.MessageTemplate, logEvent.Properties);
@@ -47,7 +61,7 @@ sourceContextProperty is ScalarValue sourceContextValue &&
4761

4862
logger.Log(
4963
level,
50-
default,
64+
eventId,
5165
slv,
5266
logEvent.Exception,
5367
(s, e) => s.ToString());

src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs

+20-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
using Serilog.Events;
1010
using FrameworkLogger = Microsoft.Extensions.Logging.ILogger;
1111
using System.Reflection;
12-
using Serilog.Parsing;
12+
using Serilog.Debugging;
1313

1414
namespace Serilog.Extensions.Logging
1515
{
@@ -18,7 +18,7 @@ class SerilogLogger : FrameworkLogger
1818
readonly SerilogLoggerProvider _provider;
1919
readonly ILogger _logger;
2020

21-
static readonly MessageTemplateParser MessageTemplateParser = new MessageTemplateParser();
21+
static readonly CachingMessageTemplateParser MessageTemplateParser = new CachingMessageTemplateParser();
2222

2323
// It's rare to see large event ids, as they are category-specific
2424
static readonly LogEventProperty[] LowEventIdValues = Enumerable.Range(0, 48)
@@ -60,6 +60,18 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
6060
return;
6161
}
6262

63+
try
64+
{
65+
Write(level, eventId, state, exception, formatter);
66+
}
67+
catch (Exception ex)
68+
{
69+
SelfLog.WriteLine($"Failed to write event through {typeof(SerilogLogger).Name}: {ex}");
70+
}
71+
}
72+
73+
void Write<TState>(LogEventLevel level, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
74+
{
6375
var logger = _logger;
6476
string messageTemplate = null;
6577

@@ -78,6 +90,11 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
7890
if (logger.BindProperty(property.Key.Substring(1), property.Value, true, out var destructured))
7991
properties.Add(destructured);
8092
}
93+
else if (property.Key.StartsWith("$"))
94+
{
95+
if (logger.BindProperty(property.Key.Substring(1), property.Value?.ToString(), true, out var stringified))
96+
properties.Add(stringified);
97+
}
8198
else
8299
{
83100
if (logger.BindProperty(property.Key, property.Value, false, out var bound))
@@ -154,4 +171,4 @@ internal static LogEventProperty CreateEventIdProperty(EventId eventId)
154171
return new LogEventProperty("EventId", new StructureValue(properties));
155172
}
156173
}
157-
}
174+
}

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
6969
List<LogEventPropertyValue> scopeItems = null;
7070
for (var scope = CurrentScope; scope != null; scope = scope.Parent)
7171
{
72-
LogEventPropertyValue scopeItem;
73-
scope.EnrichAndCreateScopeItem(logEvent, propertyFactory, out scopeItem);
72+
scope.EnrichAndCreateScopeItem(logEvent, propertyFactory, out LogEventPropertyValue scopeItem);
7473

7574
if (scopeItem != null)
7675
{

src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs

+10-4
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory
5757
return;
5858
}
5959

60-
var stateProperties = _state as IEnumerable<KeyValuePair<string, object>>;
61-
if (stateProperties != null)
60+
if (_state is IEnumerable<KeyValuePair<string, object>> stateProperties)
6261
{
6362
scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items.
6463

@@ -72,14 +71,21 @@ public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory
7271

7372
var key = stateProperty.Key;
7473
var destructureObject = false;
74+
var value = stateProperty.Value;
7575

7676
if (key.StartsWith("@"))
7777
{
7878
key = key.Substring(1);
7979
destructureObject = true;
8080
}
81-
82-
var property = propertyFactory.CreateProperty(key, stateProperty.Value, destructureObject);
81+
82+
if (key.StartsWith("$"))
83+
{
84+
key = key.Substring(1);
85+
value = value?.ToString();
86+
}
87+
88+
var property = propertyFactory.CreateProperty(key, value, destructureObject);
8389
logEvent.AddPropertyIfAbsent(property);
8490
}
8591
}

0 commit comments

Comments
 (0)