Macross.Logging.Abstractions is a .NET Standard 2.0+ library for flattening .NET Core ILogger messages to JSON. It also adds extension methods to make some common use cases easier.
The .NET Core logging framework is pretty wide-open. Developers can add whatever
they want to scopes and log whatever they want as states. The framework really
leaves it up to
ILoggerProvider
authors to sort it out, providing little guidance. Log message flattening in
Macross.Logging.Abstractions
(via LoggerJsonMessage.FromLoggerData
) follows
thses rules:
-
When
state
isIEnumerable<KeyValuePair<string, object>>
treat data as top-level properties on the JSON and call the formatter to build the content.This is the most common logging scenario. A log message written like this...
Logger.LogInformation("Log message {userId} {contactId}.", 0, 1);
...will omit JSON like this...
{ ... "Content": "Log message 0 1.", "userId": 0, "contactId": 1 ... }
A slightly more interesting example, with a complex object...
Logger.LogInformation("Log message {user}.", new { userId = 0, userName = "Mike" });
...will omit JSON like this...
{ ... "Content": "Log message { userId = 0, userName = Mike }.", "user": { "userId": 0, "userName": "Mike" } ... }
Properties are there but content ends up with the object as part of the string. That probably isn't really what the author intended. See the
Write
extension below for some other options. -
When
state
is NOTIEnumerable<KeyValuePair<string, object>>
call the formatter to build the content.This is an uncommon scenario and you have to work hard to get yourself into this state because the helper extensions (like
LogInformation
) take care of doing it nicely for us. Basically you have to callLog
method directly onILogger
and supply your own formatter. Some of the ASP.NET Core pipeline does this, for some reason.Logger.Log(LogLevel.Information, 0, new { userId = 0, userName = "Mike" }, null, (s, e) => $"Message {s.userId} {s.userName}");
...will omit JSON like this...
{ ... "Content": "Message 0 Mike" ... }
In this case we end up with just the content written out.
-
When
Scope
is attached to a message, loop through eachstate
and apply these rules:-
If
state
is a string, add it to theScope
JSON enumerable:using IDisposable Scope = Logger.BeginScope("Value"); Logger.LogInformation("Hello world.");
...will omit JSON like this...
{ ... "Content": "Hello world.", "Scope": [ "Value" ] ... }
-
If
state
isIEnumerable<KeyValuePair<string, object>>
treat data as top-level properties on the JSON. If a formatter is supplied, call it to build content and add toScope
enumerable..using IDisposable Scope = Logger.BeginScope("OrderId {OrderId} CustomerId {CustomerId}", 1, 2); Logger.LogInformation("Hello world.");
...will omit JSON like this...
{ ... "Content": "Hello world.", "Scope": [ "OrderId 1 CustomerId 2" ], "OrderId": 1, "CustomerId": 2 ... }
-
If
state
is avalue
type, add it to theScope
JSON enumerable:using IDisposable Scope = Logger.BeginScope(1000); Logger.LogInformation("Hello world.");
...will omit JSON like this...
{ ... "Content": "Hello world.", "Scope": [ 1000 ] ... }
-
If
state
is anobject
, useType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
to read the properties and then add them as top-level properties on the JSON.using IDisposable Scope = Logger.BeginScope( new { ProductId = 3, AddressId = 4 }); Logger.LogInformation("Hello world.");
...will omit JSON like this...
{ ... "Content": "Hello world.", "ProductId": 3, "AddressId": 4 ... }
-
-
Exceptions, LogLevel, ThreadId, TimestampUtc, CategoryName, & GroupName are straight-forward, they will be added off the root when present.
The BeginGroup
extension adds a special LoggerGroup
class into the ILogger
scope.
using IDisposable Group = Logger.BeginGroup("GroupName");
...is the same as...
using IDisposable Scope = Logger.BeginScope(new LoggerGroup("GroupName"));
Most logging frameworks won't do anything special with LoggerGroup
, treating
it as any other object
added to scope. These providers are
LoggerGroup
-aware:
- Macross.Logging.Files can use
LoggerGroup
to group related messages in an application into specific log files. - Macross.Windows.Debugging can use
LoggerGroup
to display messages in an application grouped together in its UI.
A bunch of "Write" helper methods (Write
, WriteTrace
, WriteDebug
,
WriteInfo
, WriteWarning
, WriteError
, and WriteCritical
) are available on
ILogger
. These methods pass data through the logging pipeline without needing
to be part of a string message.
Consider this log message:
Logger.LogInformation("Address processed.{Data}", new { AddressId = 1, CustomerId = 2 });
That will omit JSON like this:
{
...
"Content": "Address processed.{ AddressId = 1, CustomerId = 2 }",
"Data": {
"AddressId": 1,
"CustomerId": 2
}
...
}
The developer really just wanted to push AddressId
& CustomerId
into the log
but the only way to really do that was add data into the string message, which
is a bit cumbersome.
Same message using WriteInfo
:
Logger.WriteInfo(new { AddressId = 1, CustomerId = 2 }, "Address processed.");
Will omit JSON like this:
{
...
"Content": "Address processed.",
"AddressId": 1,
"CustomerId": 2
...
}
This is supported by
Macross.Logging.Files,
Macross.Logging.StandardOutput,
and Macross.Windows.Debugging. Other
log frameworks will be a mixed bag. Frameworks that just "ToString()" the
formatter will ignore the extra data. Formatters that loop over all the
properties on state
should pick up the data as {Data}
property and write it
into their logs as they would any other state
property.