top of page

ZLogger — Zero Allocation Logger for .NET Core and Unity

The standard output is especially important for containerization. For example, Datadog Logs and Stackdriver Logging collect logs directly from the standard output of the container. Writing to standard output is also recommended in The Twelve-Factor App — XI. Logs section. As such, dealing with over-decorated logs for the local environment and standard output in a slow Console.WriteLine is too old-fashioned an idea, and unfortunately, until now, no library has taken that into account in C#. There is a lot of waste in the old logging process.

The standard String.Format causes boxing to take all the values as objects, and also creates a new UTF16 String. Encode this String in UTF8, will be the flow to write to the final stream, but for ZLogger ZString format the string directly into the buffer area using ZString as UTF8 write, and streamed together in the ConsoleStream. There is no primitive boxing and it is written asynchronously and at once, so there is no load on the caller or the application as a whole.

In addition to ZString, I have also made libraries for performance, such as MessagePack for C#, Utf8Json, and more recently LitJWT and Ulid. I hope that ZLogger will make you think again about the performance of loggers.


The logger setup follows ConfigureLogging with Generic Host. Therefore, you can also set up filtering, etc., using Microsoft.Extensions.Logging.

using ZLogger;
Host .CreateDefaultBuilder()    
    .ConfigureLogging(logging=>    
    {
        // optional(MS.E.Logging):clear default providers.
        logging.ClearProviders();
        
        // optional(MS.E.Logging): default is Info, you can use this or 
        AddFilter to filtering log.
        logging.SetMinimumLevel(LogLevel.Debug);
        
        // Add Console Logging.
        logging.AddZLoggerConsole();
        
        // Add File Logging.
        logging.AddZLoggerFile("fileName.log");
        
        // Add Rolling File Logging.
        logging.AddZLoggerRollingFile((dt, x) =>$"logs/{
            dt.ToLocalTime():yyyy-MM-dd}_{x:000}.log", 
            x=>x.ToLocalTime().Date, 1024);
        
        // Enable Structured Logging
        logging.AddZLoggerConsole(options=>        
        {
            options.EnableStructuredLogging=true;        
         });    
 })

public class MyClass
{
    readonly ILogger<MyClass> logger;
    
    // get logger from DI.
    public class MyClass(ILogger<MyClass> logger)    
    {        
        this.logger =logger;    
    }
    public void Foo()    
    {
        // log text.
        logger.ZLogDebug("foo{0} bar{1}", 10, 20);
   
         // log text with structure in Structured Logging.
         logger.ZLogDebugWithPayload(new { Foo=10, Bar=20 }, 
             "foo{0} bar{1}", 10, 20);    
     }
 }

Basically, the logger is injected by DI (Please refer to [ReadMe#Global LoggerFactory if you want to use it like LogManager.GetLogger). The basic flow is to use ZLogDebug and ZLogInformation with Z at the beginning instead of LogDebug and LogInformation.

The standard Log method has a method definition of (string format, object[] args), so you can’t avoid the boxing. For this reason, I have defined a large number of overloads that we have prepared in our own generics.

// ZLog, ZLogTrace, ZLogDebug, ZLogInformation, ZLogWarning, ZLogError, ZLogCritical and *WithPayload.
public static void ZLogDebug(
    this ILogger logger, 
    string format)
public static void ZLogDebug(
    this ILoggerlogger, 
    EventId eventId, 
    string format)
public static void ZLogDebug(
    this ILogger logger, 
    Exception? exception, 
    string format)
public static void ZLogDebug(
    this ILogger logger, 
    EventI deventId, 
    Exception? exception, 
    string format)
public static void ZLogDebug<T1>(
    this ILogger logger, 
    string format, 
    T1 arg1)
public static void ZLogDebug<T1>(
    this ILogger logger, 
    EventId eventId, 
    string format, 
    T1 arg1)
public static void ZLogDebug<T1>(
    this ILogger logger, 
    Exception? exception, 
    string format, 
    T1 arg1)
public static void ZLogDebug<T1>(
    this ILogger logger, 
    EventId eventId, 
    Exception? exception, 
    string format, 
    T1 arg1)

// T1~T16public static void ZLogDebug<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(
    this ILogger logger, 
    EventId eventId, 
    Exception? exception, 
    string format, 
    T1 arg1, 
    T2 arg2, 
    T3 arg3, 
    T4 arg4, 
    T5 arg5, 
    T6 arg6, 
    T7 arg7, 
    T8 arg8, 
    T9 arg9, 
    T10 arg10, 
    T11 arg11, 
    T12 arg12, 
    T13 arg13, 
    T14 arg14, 
    T15 arg15, 
    T16 arg16)

There is a very high chance of mistakes (i.e., accidentally using the Log method when you intend to use the ZLog method) due to the mixture of the standard Log method and the ZLog method, which is described in Microsoft.CodeAnalysis.BannedApiAnalyzers to treat the standard Log method as a warning/error in your code.

By configuring these BannedApiAnalyzers, you can ensure that ZLogger is used at high performance.



Structured Logging

Log management cloud services, such as Datadog Logs and Stackdriver Logging, allow flexible filtering, searching, and querying with expressions, but logs must be properly parsed in order to take full advantage of their capabilities. ZLogger can switch between text message output and JSON output by enabling EnableStructuredLogging. In addition, it is possible to switch between multiple log providers, so it is possible to set up text messages for console output and JSON for file output.

logging.AddZLoggerConsole(options=>
{
    options.EnableStructuredLogging=true;
});

// In default, output JSON with log information(categoryName, level, timestamp, exception), message and payload(if exists).

// {"CategoryName":"ConsoleApp.Program","LogLevel":"Information","EventId":0,"EventIdName":null,"Timestamp":"2020-04-07T11:53:22.3867872+00:00","Exception":null,"Message":"Registered User: Id = 10, UserName = Mike","Payload":null}
logger.ZLogInformation(
    "Registered User: Id = {0}, 
    UserName = {1}", 
    id, 
    userName);

// {"CategoryName":"ConsoleApp.Program","LogLevel":"Information","EventId":0,"EventIdName":null,"Timestamp":"2020-04-07T11:53:22.3867872+00:00","Exception":null,"Message":"Registered User: Id = 10, UserName = Mike", "Payload":{"Id":10,"Name":"Mike"}}
logger.ZLogInformationWithPayload(new UserRegisteredLog 
{ 
    Id=id, Name=userName }, 
    "Registered User: Id = {0}, 
    UserName = {1}", 
    id, 
    userName);

Also in the StructuredLogging, write directly to UTF8 binary by System.Text.Json’s Utf8JsonWriter, and transparent passing of buffers using IBufferWriter, so thoroughly prevented any allocations related to JSON processing and performance degradation due to the generation of superfluous copies.

Implements Microsoft.Extensions.Logging directly

Since most .NET Core applications are currently built on top of .NET Generic Host (even console applications! For example, ConsoleAppFramework, which is developing Cysharp(my company), can easily run CLI applications on .NET Generic Hosts), read the configuration, DI and logger by Microsoft.Extensions framework.

Many loggers have separate frameworks and integration in the form of a bridge to “Microsoft.Extensions.Logging”. This means that the log output will go through two frameworks, lengthening the pipeline and leading to performance degradation.

“Microsoft.Extensions.Logging” is a good enough framework for log levels, filtering and registration of multiple destinations. Therefore, we minimize the overhead by not creating our own logging framework and implementing it as directly on top of “Microsoft.Extensions.Logging” as possible.

“Microsoft.Extensions.Logging” output providers are currently only providing minimal or Azure-dependent output, so it was difficult to make it practical on its own. Apart from performance, ZLogger has enough providers such as Console, File, RollingFile, and Stream to make “Microsoft.Extensions.Logging” alone practical.

Also, since all initialization/termination processes are integrated into Generic Host, the logger configuration is very simple.

async TaskMain()
{
    await Host.CreateDefaultBuilder()        
        .ConfigureLogging(logging=>        
        {
            logging.ClearProviders();
            logging.AddZLoggerConsole();        
         })        
        .RunAsync();
}

Unity

It is not that Unity has only Debug.Log (inside is Debug.unityLogger -> DebugLogHandler -> Internal_Log), but it is very poor, the implementation is only DebugLogHandler, and Debug.unityLogger is irreplaceable, so it almost doesn’t function as a logging framework.

Using ZLogger in Unity offers advantages over the standard, such as “multiple log providers including file output,” “standard log levels and filtering,” and “categorization per logger. File output is not very useful for mobile applications, but it would be nice if it existed in PC applications such as VR.

Also, categorization with loggers is very useful for filtering in log consoles that are more powerful than the norm, such as EditorConsolePro (e.g. categorization of [UI], [Battle], [Network], etc.).


ZLogger is designed to deliver maximum performance under standard conditions with no extra settings. Give this new logging library a try, redesigned from a modern perspective.



Source: Medium


The Tech Platform

0 comments
bottom of page