Skip to content
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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# 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/)
# 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/)


A Serilog provider for [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging), the logging subsystem used by ASP.NET Core.
Expand Down
20 changes: 19 additions & 1 deletion samples/WebSample/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,40 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using Microsoft.Extensions.Logging;

namespace WebSample.Controllers
{
public class HomeController : Controller
{
ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}

public IActionResult Index()
{
Log.Information("Hello from the Index!");
_logger.LogInformation("Before");

using (_logger.BeginScope("Some name"))
using (_logger.BeginScope(42))
using (_logger.BeginScope("Formatted {WithValue}", 12345))
using (_logger.BeginScope(new Dictionary<string, object> { ["ViaDictionary"] = 100 }))
{
_logger.LogInformation("Hello from the Index!");
}

_logger.LogInformation("After");
return View();
}

public IActionResult About()
{
ViewData["Message"] = "Your application description page.";

// Directly through Serilog
Log.Information("This is a handler for {Path}", Request.Path);

return View();
Expand Down
3 changes: 2 additions & 1 deletion samples/WebSample/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
],
"WriteTo": [
"Trace",
"LiterateConsole"
"LiterateConsole",
{"Name": "Seq", "Args": {"serverUrl": "http://localhost:5341" }}
]
}
}
3 changes: 2 additions & 1 deletion samples/WebSample/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"Serilog.Extensions.Logging": { "target": "project" },
"Serilog.Settings.Configuration": "2.1.0-dev-00028",
"Serilog.Sinks.Trace": "2.0.0",
"Serilog.Sinks.Literate": "2.0.0"
"Serilog.Sinks.Literate": "2.0.0",
"Serilog.Sinks.Seq": "3.1.1"
},

"tools": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Serilog.Core;
using Serilog.Events;
using FrameworkLogger = Microsoft.Extensions.Logging.ILogger;
using System.Collections.Generic;
using Serilog.Context;

namespace Serilog.Extensions.Logging
{
Expand All @@ -21,6 +23,7 @@ namespace Serilog.Extensions.Logging
public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher
{
internal const string OriginalFormatPropertyName = "{OriginalFormat}";
internal const string ScopePropertyName = "Scope";

// May be null; if it is, Log.Logger will be lazily used
readonly ILogger _logger;
Expand Down Expand Up @@ -54,15 +57,36 @@ public FrameworkLogger CreateLogger(string name)
/// <inheritdoc />
public IDisposable BeginScope<T>(T state)
{
return new SerilogLoggerScope(this, state);
if (CurrentScope != null)
return new SerilogLoggerScope(this, state);

// The outermost scope pushes and pops the Serilog `LogContext` - once
// this enricher is on the stack, the `CurrentScope` property takes care
// of the rest of the `BeginScope()` stack.
var popSerilogContext = LogContext.PushProperties(this);
return new SerilogLoggerScope(this, state, popSerilogContext);
}

/// <inheritdoc />
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
List<LogEventPropertyValue> scopeItems = null;
for (var scope = CurrentScope; scope != null; scope = scope.Parent)
{
scope.Enrich(logEvent, propertyFactory);
LogEventPropertyValue scopeItem;
scope.EnrichAndCreateScopeItem(logEvent, propertyFactory, out scopeItem);

if (scopeItem != null)
{
scopeItems = scopeItems ?? new List<LogEventPropertyValue>();
scopeItems.Add(scopeItem);
}
}

if (scopeItems != null)
{
scopeItems.Reverse();
logEvent.AddPropertyIfAbsent(new LogEventProperty(ScopePropertyName, new SequenceValue(scopeItems)));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,30 @@

using System;
using System.Collections.Generic;
using Serilog.Context;
using Serilog.Core;
using Serilog.Events;

namespace Serilog.Extensions.Logging
{
class SerilogLoggerScope : IDisposable, ILogEventEnricher
class SerilogLoggerScope : IDisposable
{
const string NoName = "None";

readonly SerilogLoggerProvider _provider;
readonly object _state;
readonly IDisposable _popSerilogContext;
readonly IDisposable _chainedDisposable;

// An optimization only, no problem if there are data races on this.
bool _disposed;

public SerilogLoggerScope(SerilogLoggerProvider provider, object state)
public SerilogLoggerScope(SerilogLoggerProvider provider, object state, IDisposable chainedDisposable = null)
{
_provider = provider;
_state = state;

Parent = _provider.CurrentScope;
_provider.CurrentScope = this;
_popSerilogContext = LogContext.PushProperties(this);
_chainedDisposable = chainedDisposable;
}

public SerilogLoggerScope Parent { get; }
Expand All @@ -44,24 +45,48 @@ public void Dispose()
_provider.CurrentScope = Parent;
}

_popSerilogContext.Dispose();
_chainedDisposable?.Dispose();
}
}

public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, out LogEventPropertyValue scopeItem)
{
if (_state == null)
{
scopeItem = null;
return;
}

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

foreach (var stateProperty in stateProperties)
{
if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string)
{
scopeItem = new ScalarValue(_state.ToString());
continue;
}

var key = stateProperty.Key;
var destructureObject = false;

var property = propertyFactory.CreateProperty(stateProperty.Key, stateProperty.Value);
if (key.StartsWith("@"))
{
key = key.Substring(1);
destructureObject = true;
}

var property = propertyFactory.CreateProperty(key, stateProperty.Value, destructureObject);
logEvent.AddPropertyIfAbsent(property);
}
}
else
{
scopeItem = propertyFactory.CreateProperty(NoName, _state).Value;
}
}
}
}
8 changes: 7 additions & 1 deletion src/Serilog.Extensions.Logging/project.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.2.0-*",
"version": "1.3.0-*",
"description": "Serilog provider for Microsoft.Extensions.Logging",
"authors": [ "Microsoft", "Serilog Contributors" ],
"packOptions": {
Expand All @@ -21,6 +21,12 @@
"net4.5": {
"dependencies": { "System.Runtime": "4.0.0" }
},
"net4.6": {
"dependencies": { "System.Runtime": "4.0.20" },
"buildOptions": {
"define": [ "ASYNCLOCAL" ]
}
},
"netstandard1.3": {
"buildOptions": {
"define": ["ASYNCLOCAL"]
Expand Down
106 changes: 106 additions & 0 deletions test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,106 @@ public void WhenDisposeIsTrueProvidedLoggerIsDisposed()
Assert.True(logger.IsDisposed);
}

[Fact]
public void BeginScopeDestructuresObjectsWhenDestructurerIsUsedInMessageTemplate()
{
var t = SetUp(LogLevel.Trace);
var logger = t.Item1;
var sink = t.Item2;

using (logger.BeginScope("{@Person}", new Person { FirstName = "John", LastName = "Smith" }))
{
logger.Log(LogLevel.Information, 0, TestMessage, null, null);
}

Assert.Equal(1, sink.Writes.Count);
Assert.True(sink.Writes[0].Properties.ContainsKey("Person"));

var person = (StructureValue)sink.Writes[0].Properties["Person"];
var firstName = (ScalarValue)person.Properties.Single(p => p.Name == "FirstName").Value;
var lastName = (ScalarValue)person.Properties.Single(p => p.Name == "LastName").Value;
Assert.Equal("John", firstName.Value);
Assert.Equal("Smith", lastName.Value);
}

[Fact]
public void BeginScopeDestructuresObjectsWhenDestructurerIsUsedInDictionary()
{
var t = SetUp(LogLevel.Trace);
var logger = t.Item1;
var sink = t.Item2;

using (logger.BeginScope(new Dictionary<string, object> {{ "@Person", new Person { FirstName = "John", LastName = "Smith" }}}))
{
logger.Log(LogLevel.Information, 0, TestMessage, null, null);
}

Assert.Equal(1, sink.Writes.Count);
Assert.True(sink.Writes[0].Properties.ContainsKey("Person"));

var person = (StructureValue)sink.Writes[0].Properties["Person"];
var firstName = (ScalarValue)person.Properties.Single(p => p.Name == "FirstName").Value;
var lastName = (ScalarValue)person.Properties.Single(p => p.Name == "LastName").Value;
Assert.Equal("John", firstName.Value);
Assert.Equal("Smith", lastName.Value);
}

[Fact]
public void BeginScopeDoesNotModifyKeyWhenDestructurerIsNotUsedInMessageTemplate()
{
var t = SetUp(LogLevel.Trace);
var logger = t.Item1;
var sink = t.Item2;

using (logger.BeginScope("{FirstName}", "John"))
{
logger.Log(LogLevel.Information, 0, TestMessage, null, null);
}

Assert.Equal(1, sink.Writes.Count);
Assert.True(sink.Writes[0].Properties.ContainsKey("FirstName"));
}

[Fact]
public void BeginScopeDoesNotModifyKeyWhenDestructurerIsNotUsedInDictionary()
{
var t = SetUp(LogLevel.Trace);
var logger = t.Item1;
var sink = t.Item2;

using (logger.BeginScope(new Dictionary<string, object> { { "FirstName", "John"}}))
{
logger.Log(LogLevel.Information, 0, TestMessage, null, null);
}

Assert.Equal(1, sink.Writes.Count);
Assert.True(sink.Writes[0].Properties.ContainsKey("FirstName"));
}

[Fact]
public void NamedScopesAreCaptured()
{
var t = SetUp(LogLevel.Trace);
var logger = t.Item1;
var sink = t.Item2;

using (logger.BeginScope("Outer"))
using (logger.BeginScope("Inner"))
{
logger.Log(LogLevel.Information, 0, TestMessage, null, null);
}

Assert.Equal(1, sink.Writes.Count);

LogEventPropertyValue scopeValue;
Assert.True(sink.Writes[0].Properties.TryGetValue(SerilogLoggerProvider.ScopePropertyName, out scopeValue));

var items = (scopeValue as SequenceValue)?.Elements.Select(e => ((ScalarValue)e).Value).Cast<string>().ToArray();
Assert.Equal(2, items.Length);
Assert.Equal("Outer", items[0]);
Assert.Equal("Inner", items[1]);
}

private class FoodScope : IEnumerable<KeyValuePair<string, object>>
{
readonly string _name;
Expand Down Expand Up @@ -327,5 +427,11 @@ IEnumerator IEnumerable.GetEnumerator()
return GetEnumerator();
}
}

private class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
}