ScyberLog 1.2.0

dotnet add package ScyberLog --version 1.2.0
NuGet\Install-Package ScyberLog -Version 1.2.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ScyberLog" Version="1.2.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add ScyberLog --version 1.2.0
#r "nuget: ScyberLog, 1.2.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.2.0 311 1/25/2023
1.1.5 415 5/12/2022
1.1.4 375 5/2/2022
1.1.3 377 4/27/2022
1.1.2 412 4/14/2022
1.1.1 419 4/13/2022
1.1.0 393 4/12/2022
1.0.3 388 3/4/2022
1.0.2 408 2/14/2022
1.0.1 387 2/13/2022
1.0.0 414 2/9/2022