ZLogger Documentation

repository·master·Indexed 23 days ago

https://github.com/cysharp/zlogger

A high-performance, zero-allocation text and structured logging library for .NET and Unity built on top of Microsoft.Extensions.Logging. It leverages C# 10 String Interpolation and .NET 8's IUtf8SpanFormattable to output directly in UTF8, avoiding string encoding and boxing overhead. Features include multiple providers (Console, File, RollingFile, InMemory, Stream), custom LogProcessor implementation, and flexible plain-text or JSON formatting.

Tokens
18K
Snippets
40
Records
61
Agent score
83%

What's inside ZLogger

  1. Overview of ZLogger

    master

    ZLogger is a zero-allocation text and structured logger for .NET and Unity. It is built on top of Microsoft.Extensions.Logging, allowing it to integrate seamlessly with frameworks like ASP.NET Core and Generic Host while eliminating the overhead typically associated with bridging different logging systems.

    Key features include:

    • High Performance: Leverages C# 10 String Interpolation and .NET 8's IUtf8SpanFormattable to output directly in UTF8, avoiding the costs of UTF16-to-UTF8 encoding and value boxing.
    • Structured Logging: Integrates with System.Text.Json's Utf8JsonWriter for efficient structured log output.
    • Cloud-Native Optimized: Designed for high-performance console output suitable for cloud log management.
    • Compatibility: Delivers peak performance on .NET 8+, but maintains consistent performance on .NET 6 and .NET Standard 2.0 via a custom fallback implementation.
    • Standard Features: Supports LogLevel configuration from JSON, category filtering, and logging scopes.
  2. Understand the purpose of Microsoft.Extensions.DependencyInjection.Abstractions

    master

    This package provides the low-level interfaces and abstractions for the Dependency Injection (DI) design pattern. It enables Inversion of Control (IoC) by defining how services are registered, described, and retrieved.

    Note: This package contains only the abstractions (interfaces and descriptors). To actually use dependency injection, you must use it alongside a concrete implementation, such as Microsoft.Extensions.DependencyInjection.

  3. Generate hash codes with System.IO.Hashing

    master

    The System.IO.Hashing namespace provides high-performance implementations of various hash code algorithms. These are designed for fast content identification, object comparison, and detecting content alterations.

    Warning: These hash functions are not suitable for security-critical applications (e.g., handling passwords or verifying untrusted content). For security purposes, use the System.Security.Cryptography namespace instead.

    using System.IO.Hashing;
    
    byte[] data = new byte[] { 1, 2, 3, 4 };
    byte[] hash = XxHash3.Hash(data);
  4. Use high-performance ZLog* methods

    master

    While standard .Log* methods work, ZLogger provides unique .ZLog* methods (e.g., .ZLogInformation, .ZLogDebug) that use .NET InterpolatedStringHandler to process logs at high performance while remaining in UTF8. These methods support both plain-text and structured logging using string interpolation.

    Structured Logging Syntax

    • Automatic Capture: Variables in interpolation are captured as structured properties.
    • Explicit Naming: Use :@name to specify a custom key for a property.
    • JSON Serialization: Use :json to log an object as a JSON string.
    • Custom Formats: Use :@name:format to combine custom names with format strings (e.g., date formatting).
  5. Supported Logging Destinations in ZLogger

    master

    ZLogger provides several built-in providers for different output requirements:

    • Console: Optimized for cloud-native environments, supporting both text and structured logs.
    • File: Direct writing to files in UTF8 format.
    • RollingFile: File logging with rotation capabilities.
    • InMemory: For testing or in-memory buffering.
    • Stream: Writing logs to any provided Stream.
    • AsyncBatchingProcessor: Used for sending logs over protocols like HTTP by batching entries to reduce I/O overhead.
  6. Use IChangeToken to respond to changes

    master

    The IChangeToken interface represents a token that notifies when a change occurs. It is commonly used to trigger actions or invalidate caches when underlying data (like configuration files) is modified. You can use RegisterChangeCallback to attach a callback that executes when the change occurs.

    // Create a change token for the configuration
    IChangeToken changeToken = configuration.GetReloadToken();
    
    // Attach a change callback
    IDisposable changeTokenRegistration = changeToken.RegisterChangeCallback(state =>
    {
        Console.WriteLine("Configuration changed!");
        IConfigurationRoot root = (IConfigurationRoot)state;
        var someValue = root["SomeKey"];
        Console.WriteLine($"New value of SomeKey: {someValue}");
    }, configuration);
    
    // Clean up the registration when no longer needed
    changeTokenRegistration.Dispose();
  7. Understand the LogInfo structure

    master

    The LogInfo struct provides metadata about when and how a log entry was written. This is useful for advanced formatting or custom providers.

    Available Fields:

    • LogCategory Category: Category name (holds JsonEncodedText and utf8 byte sequence).
    • Timestamp Timestamp: The log timestamp.
    • LogLevel LogLevel: Microsoft.Extensions.Logging.LogLevel.
    • EventId EventId: Microsoft.Extensions.Logging.EventId.
    • Exception? Exception: The exception passed during logging.
    • LogScopeState? ScopeState: Properties from ILogger.BeginScope (requires IncludeScopes = true).
    • ThreadInfo ThreadInfo: Thread information (requires CaptureThreadInfo = true).
    • object? Context: Additional context.
    • string? MemberName: Caller member name.
    • string? FilePath: Caller file path.
    • int LineNumber: Caller line number.
  8. Use ZLoggerMessage Source Generator for high performance

    master

    ZLogger includes a source generator for compile-time logging, similar to .NET 6's logging source generation. This provides the highest performance by avoiding runtime parsing of log templates.

    Use the [ZLoggerMessage] attribute on partial methods within a partial class.

    public static partial class MyLogger
    {
        [ZLoggerMessage(LogLevel.Information, "Bar: {x} {y}")]
        public static partial void Bar(this ILogger<Foo> logger, int x, int y);
    }
  9. Implement a custom ILogger provider

    master

    To create a custom logging destination, implement the ILogger interface. The Log<TState> method is the core where you define how messages are processed (e.g., writing to a specific console, a database, or a custom file format). You can use IsEnabled(LogLevel) to filter logs before processing them to improve performance.

    using Microsoft.Extensions.Logging;
    
    public sealed class ColorConsoleLogger : ILogger
    {
        private readonly string _name;
        private readonly Func<ColorConsoleLoggerConfiguration> _getCurrentConfig;
    
        public ColorConsoleLogger(
            string name,
            Func<ColorConsoleLoggerConfiguration> getCurrentConfig) =>
            (_name, _getCurrentConfig) = (name, getCurrentConfig);
    
        public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!;
    
        public bool IsEnabled(LogLevel logLevel) =>
            _getCurrentConfig().LogLevelToColorMap.ContainsKey(logLevel);
    
        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception? exception,
            Func<TState, Exception?, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }
    
            ColorConsoleLoggerConfiguration config = _getCurrentConfig();
            if (config.EventId == 0 || config.EventId == eventId.Id)
            {
                ConsoleColor originalColor = Console.ForegroundColor;
    
                Console.ForegroundColor = config.LogLevelToColorMap[logLevel];
                Console.WriteLine($"[{eventId.Id,2}: {logLevel,-12}]");
    
                Console.ForegroundColor = originalColor;
                Console.Write($"     {_name} - ");
    
                Console.ForegroundColor = config.LogLevelToColorMap[logLevel];
                Console.Write($"{formatter(state, exception)}");
    
                Console.ForegroundColor = originalColor;
                Console.WriteLine();
            }
        }
    }
  10. Use async iterators on .NET Framework or .NET Standard 2.0

    master

    The Microsoft.Bcl.AsyncInterfaces package provides the necessary definitions for asynchronous stream types (IAsyncEnumerable<T>, IAsyncEnumerator<T>, and IAsyncDisposable<T>) on older platforms like .NET Framework and .NET Standard 2.0. This enables the use of C# async iterators (using await foreach and yield return) which were introduced in C# 8.0.

    Note: This library is not necessary or recommended if you are targeting .NET Core 3.0+ or .NET Standard 2.1+, as these versions include these types natively.

    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    internal static class Program
    {
        private static async Task Main()
        {
            Console.WriteLine("Starting...");
            await foreach (var value in GetValuesAsync())
            {
                Console.WriteLine(value);
            }
            Console.WriteLine("Finished!");
    
            static async IAsyncEnumerable<int> GetValuesAsync()
            {
                for (int i = 0; i < 10; i++)
                {
                    await Task.Delay(TimeSpan.FromSeconds(1));
                    yield return i;
                }
            }
        }
    }
  11. Customize JSON log formatting

    master

    The UseJsonFormatter method allows you to modify the JSON structure. Key customization options include:

    • IncludeProperties: A bitmask to select which properties to include (e.g., Timestamp | LogLevel | Message | ParameterKeyValues).
    • JsonPropertyNames: Allows renaming standard keys (e.g., renaming LogLevel to severity).
    • PropertyKeyValuesObjectName: Defines the name of the nested object for payload key-values.
    • AdditionalFormatter: A callback to add arbitrary JSON objects or fields (e.g., for cloud provider labels).
    // Example: Customizing for Google Cloud Logging
    public static ZLoggerOptions UseCloudLoggingJsonFormat(this ZLoggerOptions options)
    {
        return options.UseJsonFormatter(formatter =>
        {
            formatter.IncludeProperties = Timestamp | LogLevel | Message | ParameterKeyValues;
    
            formatter.JsonPropertyNames = JsonPropertyNames.Default with
            {
                LogLevel = Encode("severity"),
                LogLevelNone = Encode("DEFAULT"),
                LogLevelTrace = Encode("DEBUG"),
                LogLevelDebug = Encode("DEBUG"),
                LogLevelInformation = Encode("INFO"),
                LogLevelWarning = Encode("WARNING"),
                LogLevelError = Encode("ERROR"),
                LogLevelCritical = Encode("CRITICAL"),
                Message = Encode("message"),
                Timestamp = Encode("timestamp"),
            };
    
            formatter.PropertyKeyValuesObjectName = Encode("jsonPayload");
    
            var labels = Encode("logging.googleapis.com/labels");
            var category = Encode("category");
            var eventId = Encode("eventId");
            var userId = Encode("userId");
    
            formatter.AdditionalFormatter = (Utf8JsonWriter writer, in LogInfo logInfo) =>
            {
                writer.WriteStartObject(labels);
                writer.WriteString(category, logInfo.Category.JsonEncoded);
                writer.WriteString(eventId, logInfo.EventId.Name);
    
                if (logInfo.ScopeState != null && !logInfo.ScopeState.IsEmpty)
                {
                    foreach (var item in logInfo.ScopeState.Properties)
                    {
                        if (item.Key == "userId")
                        {
                            writer.WriteString(userId, item.Value!.ToString());
                            break;
                        }
                    }
                }
                writer.WriteEndObject();
            };
        });
    }