ScyberLog 1.2.0
dotnet add package ScyberLog --version 1.2.0
NuGet\Install-Package ScyberLog -Version 1.2.0
<PackageReference Include="ScyberLog" Version="1.2.0" />
paket add ScyberLog --version 1.2.0
#r "nuget: ScyberLog, 1.2.0"
// Install ScyberLog as a Cake Addin
#addin nuget:?package=ScyberLog&version=1.2.0
// Install ScyberLog as a Cake Tool
#tool nuget:?package=ScyberLog&version=1.2.0
ScyberLog
ScyberLog is a low-nonsense logging framework designed to meet a set of specific requirements:
- Simple configuration
- Rolling file logger that supports JSON formatted output
- Integration with .NET core logging extensions without excluding parameters which don't appear in the message template.
- Support for log scopes
This framework isn't intended to compete with the other big logging frameworks out there in terms of performance and configurability. Instead it aims to fill a gap in the available options by providing something simpler for small projects. If you need something to drop in a pet project that doesn't require you to read a novel of documentation, download a bunch of 3rd party appenders and sinks (each with their own usage docs), and hand roll formatters just to get your logs into a json formatted file, then this the framework for you. And since it only relies on the built in logging interfaces in .NET Core, you can always swap it out without changing your codebase once you outgrow it.
Output
By default, ScyberLog writes log entries to both the console and a file. A log message like this:
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
will produce a console line like this:
[19:17:36:7031] [INFO] LoggerName - Worker running at: 01/26/2022 19:17:36 -06:00
File entries will be writen out as unformatted json by default, with one entry per line:
{"timeStamp":"2022-01-26T20:36:30.3726942-06:00","logger":"LoggerName","level":"DEBUG","message":"Worker running at: 01/26/2022 20:36:30 -06:00","state":{"data":{"time":"2022-01-26T20:36:30.3725537-06:00"}},"scopes":[{"scope":1},{"scope":2}]}
{"timeStamp":"2022-01-26T20:36:30.3733226-06:00","logger":"LoggerName","level":"INFO","message":"Worker running at: 01/26/2022 20:36:30 -06:00","state":{"data":{"time":"2022-01-26T20:36:30.3732356-06:00"},"values":[{"extraData":"HelloWorld"}]},"scopes":[{"scope":1},{"scope":2}]}
{"timeStamp":"2022-01-26T20:36:30.3737131-06:00","logger":"LoggerName","level":"WARN","message":"Worker running at: 01/26/2022 20:36:30 -06:00","state":{"data":{"time":"2022-01-26T20:36:30.3736427-06:00"}}}
{"timeStamp":"2022-01-26T20:36:30.3744115-06:00","logger":"LoggerName","level":"ERROR","message":"An error occurred during execution at 01/26/2022 20:36:30 -06:00","state":{"data":{"time":"2022-01-26T20:36:30.3743389-06:00"}},"exception":{"message":"Exceptional!","data":{},"hResult":-2146233088}}
{"timeStamp":"2022-01-26T20:36:30.3747746-06:00","logger":"LoggerName","level":"CRIT","message":"Worker running at: 01/26/2022 20:36:30 -06:00","state":{"data":{"time":"2022-01-26T20:36:30.3747104-06:00"}}}
You can configure the logger to format the json, if you prefer:
{
"timeStamp": "2022-01-26T20:36:30.3733226-06:00",
"logger": "LoggerName",
"level": "INFO",
"message": "Worker running at: 01/26/2022 20:36:30 -06:00",
"state": {
"data": {
"time": "2022-01-26T20:36:30.3732356-06:00"
},
"values": [
{
"extraData": "HelloWorld"
}
]
},
"scopes": [
{
"scope": 1
},
{
"scope": 2
}
]
}
Usage
First, use the included extension to add the logger to the logging builder in the host builder; this will be very slightly different depending on your environment.
Worker Service:
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.ConfigureLogging((HostBuilderContext hostingContext, ILoggingBuilder loggingBuilder) =>
{
//Clear out the console provider automatically added by the hosted service
loggingBuilder.ClearProviders();
loggingBuilder.AddScyberLog();
})
.Build();
ASP.NET 6.0:
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureLogging((HostBuilderContext hostingContext, ILoggingBuilder loggingBuilder) =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddScyberLog();
});
Then inject the logger wherever you need it:
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
_logger.LogInformation("{Controller} initialized", nameof(WeatherForecastController));
}
There are some example apps in the solution.
Unused Log Message Parameters
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now, new { ExtraData = "HelloWorld"});
If you log a message like the above in another framework, you'll get a compiler warning about the number of parameters supplied to the logging extention methods. This is because by default, .NET throws away parameters that aren't interpolated into the message when the message is written. ScyberLog takes the stance that this information shouldn't be lost, and behaves differently. If you intend to take advantage of this feature, know that if you later switch to another framework, this information will be lost. Avoid it if you want to preserve the ability to switch logging frameworks in the future without a change in behavior.
Including the following line in your project will remove the compiler warnings.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Usage", "CA2017:Number of parameters supplied in the logging message template do not match the number of named placeholders", Justification = "ScyberLog captures unused parameters")]
Configuration
You can configure the logger by either using adding a ScyberLog
node to your appsettings.json file and passing your IConfiguration
instance to the AddScyberLog
method, by using the optional configuration action parameter in the AddScyberLog
extension, or by using the .Configure<ScyberLogConfiguration>()
extension on your service collection.
Using appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.Extensions.Hosting.Internal.Host" : "Information"
}
},
"ScyberLog": {
"EnableConsole": true,
"EnableFile": true,
"FileNameTemplate": "Log\\{0:yyyy-MM-dd}.log",
"FileFormatter": "Json"
}
}
builder.Host.ConfigureLogging((HostBuilderContext hostingContext, ILoggingBuilder loggingBuilder) =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddScyberLog(hostingContext.Configuration);
});
Using configuration action parameter:
builder.Host.ConfigureLogging((HostBuilderContext hostingContext, ILoggingBuilder loggingBuilder) =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddScyberLog(config =>
{
config.FileFormatter = "console";
config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
});
Using services.Configure
:
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
services.Configure<ScyberLogConfiguration>(config =>
{
config.FileFormatter = "console";
config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
})
.ConfigureLogging((HostBuilderContext hostingContext, ILoggingBuilder loggingBuilder) =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddScyberLog();
})
.Build();
NOTE Ironically, not all properties of the JsonSerializerOptions
are serializable from JSON, so if you aren't happy with the defaults you'll need to configure them in code. The default configuration is below; note that as of this writing the built in serializer fails to serialize System.Exception
and hence the library has a custom converter.
public JsonSerializerOptions JsonSerializerOptions { get; set; } = new JsonSerializerOptions()
{
UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
}.With(x => x.Converters.Add(new JsonExceptionConverter<Exception>()));
For more information see Options Pattern in .Net
Rolling Files
Scyberlog "supports" datetime based file rolling, mainly by interpolating the date into the filename when writing to the log. The default file name is "Log\\{0:yyyy-MM-dd}.log"
. The Filename template is a standard format string, with the 0th parameter being the current datetime and the 1st parameter being the logger name. Here is the implementation:
var path = string.Format(this.FileTemplate, DateTime.Now, state.Logger);
Custom formatters and sinks
Loggers in ScyberLog are a composition of a message formatter and some number of mesage sinks. If you need to modify the format of any log messages or write to another datasource you can implement a new ILogFormatter or ILogSink respectively and register them with the service collection. Both of these interfaces implement the IKeyedItem interface, which you can use to specify a string key with which you can reference your sinks/formatters in the configuration:
services.AddTransient<ILogSink, MySink>();
services.AddTransient<ILogFormatter, MyFormatter>();
services.Configure<ScyberLogConfiguration>(config =>
{
config.EnableFile = false;
config.EnableConsole = false;
config.AdditionalLoggers.Add(
new LoggerSetup()
{
Formatter = "my_formatter",
Sinks = new { "my_sink", "file" }
}
);
});
ScyberLog has two buit in formatters, "text"
and "json"
and three built-in sinks,"console"
, "colored_console"
and "file"
.
Custom Json Converters
As noted above, ScyberLog relies on System.Text.Json
to serialize to JSON, with all the caveats that implies. In particular, not all types are serializable out of the box, and you may need to provide a custom JsonConverter to properly log these types. System.Exception
and System.Net.IPAddress
are two common examples, though ScyberLog includes a custom converter for Exception
by default. ScyberLog makes a best effort to inform you when serialization errors occur.
Performance
ScyberLog probably isn't fast. I haven't measured it. I can't recommend you use it for anything performance critical.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 is compatible. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net5.0
- Microsoft.Extensions.Configuration.Abstractions (>= 6.0.0)
- Microsoft.Extensions.Logging (>= 6.0.0)
- Microsoft.Extensions.Logging.Configuration (>= 6.0.0)
- System.Text.Json (>= 6.0.0)
-
net6.0
- Microsoft.Extensions.Configuration.Abstractions (>= 6.0.0)
- Microsoft.Extensions.Logging (>= 6.0.0)
- Microsoft.Extensions.Logging.Configuration (>= 6.0.0)
- System.Text.Json (>= 6.0.0)
-
net7.0
- Microsoft.Extensions.Configuration.Abstractions (>= 6.0.0)
- Microsoft.Extensions.Logging (>= 6.0.0)
- Microsoft.Extensions.Logging.Configuration (>= 6.0.0)
- System.Text.Json (>= 6.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.