Log2 1.0.0

dotnet add package Log2 --version 1.0.0
                    
NuGet\Install-Package Log2 -Version 1.0.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="Log2" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Log2" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Log2" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Log2 --version 1.0.0
                    
#r "nuget: Log2, 1.0.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.
#:package Log2@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Log2&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Log2&version=1.0.0
                    
Install as a Cake Tool

Log2

Ultra-high-performance structured logging library for .NET 9+.

Lock-free MPSC ring buffer backed by native memory (zero GC roots), zero-allocation hot path, SIMD-accelerated layout rendering, 30+ outputs (built-in + NuGet packages), file rotation with cleanup policies, circuit breakers for network outputs, and per-source log level overrides.

Runtime control plane and CLI tool are covered in separate documents:


Table of Contents


Features

  • Zero-allocation hot path — producer threads encode UTF-8 inline (40 bytes) and return immediately; no new, no boxing, no closures
  • GC-invisible ring bufferNativeRingBuffer backed by NativeMemory.AlignedAlloc; zero managed references, O(1) GC Mark cost
  • Lock-free MPSC — single CAS per write, PaddedLong counters on separate 128-byte cache lines, HeadCache avoids cross-core volatile reads
  • Native slab allocatorNativeSlab replaces ArrayPool<byte> for overflow payloads; ABA-safe free list, one CAS per rent/return
  • SIMD-accelerated — ASCII fast path via Ascii.FromUtf16, SearchValues for CSV quoting
  • Single background consumer — reads entries, builds ref struct LogRecord, routes to all outputs, frees slab blocks
  • 30+ outputs — 15 built-in + 16 external NuGet packages (databases, cloud, messaging, observability)
  • File rotation and cleanup — by age, count, or total size (combinable)
  • Circuit breakers — network outputs auto-pause after consecutive failures, auto-resume
  • Structured properties — zero-allocation binary encoding with ReadOnlySpan<LogProperty>
  • Scoped contextLogContext.Push() with AsyncLocal flow across await boundaries
  • Runtime level switching — change log levels without restart via LogLevelSwitch (~2–3 ns overhead)
  • Per-source overrides — different min/max levels per LogSource via builder or LogLevelSwitch
  • Compiled layouts — hand-optimized render methods for built-in patterns bypass the token loop
  • Configurable overflowDrop (default, never blocks) or Block (guaranteed delivery for audit/compliance)
  • JSON configuration — load from log2.json or build programmatically

Install

dotnet add package Log2

Optional output packages:

dotnet add package Log2.Outputs.Elasticsearch
dotnet add package Log2.Outputs.Seq
dotnet add package Log2.Outputs.PostgreSQL
dotnet add package Log2.Outputs.SqlServer
dotnet add package Log2.Outputs.Redis
dotnet add package Log2.Outputs.ClickHouse
dotnet add package Log2.Outputs.RabbitMQ
dotnet add package Log2.Outputs.Kafka
dotnet add package Log2.Outputs.Discord
dotnet add package Log2.Outputs.Slack
dotnet add package Log2.Outputs.MicrosoftTeams
dotnet add package Log2.Outputs.AmazonCloudWatch
dotnet add package Log2.Outputs.GoogleCloudLogging
dotnet add package Log2.Outputs.AzureAppInsights
dotnet add package Log2.Outputs.OpenTelemetry
dotnet add package Log2.Outputs.Telegram

Quick Start

Minimal setup

using Log2;
using Log2.Api;

using var log = Log2.Api.Log2.Builder()
    .MinLevel(LogLevel.Debug)
    .WithConsole()
    .Build();

log.Info("Application started");
log.Warn("Something looks wrong");
log.Error("Connection failed");

Global static logger

Configure once at startup, use anywhere without passing instances:

// Program.cs / startup
using var log = Log2.Api.Log2.Builder()
    .MinLevel(LogLevel.Debug)
    .WithConsole()
    .WithFile("logs", "myapp")
    .Build();

Log2.Api.Log2.Configure(log);

// Anywhere in the codebase
Log2.Api.Log2.Info("Logged via static API");
Log2.Api.Log2.Error(ex);
Log2.Api.Log2.Warn("Low memory", LogSource.System);

Console + File + JSON

using var log = Log2.Api.Log2.Builder()
    .MinLevel(LogLevel.Debug)
    .WithConsole(useColors: true)
    .WithFile("logs", "myapp",
              maxSizeMB: 100,
              cleanup: FileOutput.CleanupMode.Age | FileOutput.CleanupMode.TotalSize,
              maxAgeDays: 30,
              maxTotalSizeMB: 1024)
    .WithJson("logs", "myapp",
              maxSizeMB: 200,
              cleanup: FileOutput.CleanupMode.Count,
              maxFileCount: 20)
    .Build();

Production setup with level switch and source overrides

var levelSwitch = new LogLevelSwitch(LogLevel.Info);

using var log = Log2.Api.Log2.Builder()
    .WithLevelSwitch(levelSwitch)
    .Override(LogSource.Database, LogLevel.Warn)      // less noise from DB
    .Override(LogSource.Security, LogLevel.Trace)     // full trace for security
    .ChannelCapacity(65536)
    .Overflow(OverflowMode.Drop)
    .DrainTimeout(TimeSpan.FromSeconds(10))
    .DefaultLayout("[%level:-8] %timestamp:time | %source:-14 | T%thread:-4 | %message%newline")
    .WithConsole()
    .WithFile("logs", "myapp", maxSizeMB: 100, min: LogLevel.Info)
    .WithSmtp("smtp.example.com", "alerts@myapp.com", "oncall@myapp.com",
              port: 587, useSsl: true, min: LogLevel.Error)
    .Build();

Log2.Api.Log2.Configure(log);

Load from JSON

using Log2.Config;

using var log = Log2ConfigLoader.FromFile("log2.json");
Log2.Api.Log2.Configure(log);

Microsoft.Extensions.Logging integration

The Log2.Extensions.Logging package plugs Log2 into the standard ILogger/ILogger<T> pipeline. Inject ILogger<T> anywhere and every entry routes through Log2's zero-allocation hot path.

using Log2.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var services = new ServiceCollection();
services.AddLogging(b => b.AddLog2(log => log
    .MinLevel(LogLevel.Info)
    .WithConsole()
    .WithFile("logs", "myapp")));

var sp = services.BuildServiceProvider();
var logger = sp.GetRequiredService<ILogger<MyService>>();
logger.LogInformation("Routed through Log2 with {Property}", value);

Generic-host shortcut — replaces all default providers with Log2:

using Log2.Extensions.Logging;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder(args)
    .UseLog2(log => log
        .MinLevel(LogLevel.Info)
        .WithConsole()
        .WithFile("logs", "myapp"))
    .Build();

The DI container owns the resulting Log2Instance; disposing the IServiceProvider (or shutting the host down) drains the ring buffer and flushes outputs. Log levels are mapped via Log2.Compat.LogLevelMapper (see docs/MIGRATION.md for the full table).


Builder API

All builder methods return this for fluent chaining.

Engine configuration

Log2.Api.Log2.Builder()
    .MinLevel(LogLevel.Debug)               // lowest level to accept (default: Trace)
    .MaxLevel(LogLevel.Fatal)               // highest level to accept (default: Fatal)
    .ChannelCapacity(65536)                 // ring buffer size, must be power of 2
    .Overflow(OverflowMode.Drop)            // Drop (default) or Block
    .DrainTimeout(TimeSpan.FromSeconds(10)) // how long Dispose() waits for drain
    .DefaultLayout("[%level:-8] %timestamp:time | %message%newline")
    .WithLevelSwitch(levelSwitch)           // enable runtime level changes
    .Override(LogSource.Database, LogLevel.Warn) // per-source override
    .Build();

Output methods

// ─── Local I/O ──────────────────────────────────────────────
.WithConsole(useColors: true)
.WithFile("logs", "myapp", maxSizeMB: 100)
.WithCsv("logs", "myapp", maxSizeMB: 50)
.WithJson("logs", "myapp", maxSizeMB: 200)
.WithXml("logs", "myapp")
.WithStdout()
.WithNull()                                   // discard (testing / benchmarking)

// ─── Network / IPC ──────────────────────────────────────────
.WithUdp(port: 5557)                          // fire-and-forget, wire protocol
.WithTcp(port: 5558)                          // 4-byte framing, auto-reconnect
.WithNamedPipe("MyPipe")                      // Windows named pipe
.WithUnixSocket("/tmp/myapp.sock")            // Unix domain socket
.WithSharedMemory("MyMap", capacityMb: 16)    // Windows memory-mapped file

// ─── Alerting ───────────────────────────────────────────────
.WithSmtp("smtp.example.com", "from@x.com", "to@x.com",
          port: 587, useSsl: true, min: LogLevel.Error)
.WithWebService("https://logs.example.com/ingest",
                authHeader: "Bearer token", min: LogLevel.Warn)
.WithEventViewer("MyApp", min: LogLevel.Error)  // Windows only

// ─── External NuGet packages ────────────────────────────────
.WithElasticsearch("http://localhost:9200", indexPrefix: "myapp")
.WithSeq("http://localhost:5341", apiKey: "...")
.WithPostgreSQL("Host=localhost;Database=logs", tableName: "log2_entries")
.WithSqlServer("Server=.;Database=Logs", tableName: "Log2Entries")
.WithRedis("localhost:6379", key: "log2:logs", mode: RedisMode.Stream)
.WithClickHouse("http://localhost:8123", table: "logs")
.WithRabbitMQ("localhost", exchange: "logs")
.WithKafka("localhost:9092", topic: "logs")
.WithDiscord("https://discord.com/api/webhooks/...", min: LogLevel.Error)
.WithSlack("https://hooks.slack.com/...", min: LogLevel.Error)
.WithMicrosoftTeams("https://outlook.office.com/webhook/...", min: LogLevel.Error)
.WithAmazonCloudWatch("my-log-group", "my-stream")
.WithGoogleCloudLogging("my-project-id", logName: "myapp")
.WithAzureAppInsights("InstrumentationKey=...")
.WithOpenTelemetry("http://localhost:4318")

All outputs accept min and max level parameters and an optional custom layout.


Logging Methods

Per-level convenience methods

log.Trace("Entering method X");
log.Debug("Variable value: 42");
log.Info("User logged in");
log.Warn("Disk usage above 80%");
log.Error("Failed to connect");
log.Error(exception);             // logs full exception (type + message + stack trace)
log.Severe("Data corruption detected");
log.Critical("Out of memory");
log.Alert("Security breach detected");
log.Fatal("Unrecoverable failure");

With source categorization

log.Info("Query executed in 3ms", LogSource.Database);
log.Warn("Retry 2/3", LogSource.Network);
log.Error("Invalid token", LogSource.Authentication);
log.Debug("Config reloaded", LogSource.Configuration);

Generic Log method

// Dynamic level and source at call site
log.Log("Custom message", LogLevel.Warn, LogSource.Security);

ReadOnlySpan<char> overload (zero string allocation)

ReadOnlySpan<char> msg = "Connection closed".AsSpan();
log.Log(msg, LogLevel.Info, LogSource.Network);

With inline structured properties

ReadOnlySpan<LogProperty> props =
[
    new LogProperty("OrderId",    "ORD-123"),
    new LogProperty("Amount",     "99.90"),
    new LogProperty("Currency",   "BRL"),
];
log.Log("Order placed", LogLevel.Info, LogSource.Application, props);

Static global API

Log2.Api.Log2.Configure(log); // once at startup

Log2.Api.Log2.Info("No instance needed");
Log2.Api.Log2.Error(exception);
Log2.Api.Log2.Warn("Low memory", LogSource.System);
Log2.Api.Log2.Log("Custom", LogLevel.Debug, LogSource.Core, props);
Log2.Api.Log2.Flush();

long dropped = Log2.Api.Log2.DroppedCount;

Structured templates — LogTemplate

Attach named properties extracted from a message template. Properties are resolved at compile time (hole names) and encoded zero-alloc after warm-up.

// Arity 1
log.LogTemplate("User {UserId} logged in", userId, LogLevel.Info, LogSource.Authentication);

// Arity 2
log.LogTemplate("Request {Method} {Path} processed", method, path);

// Arity 3
log.LogTemplate("Order {OrderId} for {Customer} total {Total}", orderId, customer, total);

[Log2Message] source generator

Reference Log2.SourceGenerators as an analyzer and declare partial methods. The Roslyn generator emits the implementation at build time — zero runtime overhead beyond the LogTemplate call itself.


<ItemGroup>
  <ProjectReference Include="..\..\src\Log2.SourceGenerators\Log2.SourceGenerators.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>
using Log2.Api;

public static partial class AppLog
{
    // Arity 0 — no holes, emits log.Log("Server started", ...)
    [Log2Message("Server started")]
    public static partial void ServerStarted(this Log2Instance log);

    // Arity 1 — emits log.LogTemplate("User {UserId} logged in", userId, ...)
    [Log2Message("User {UserId} logged in")]
    public static partial void UserLoggedIn(this Log2Instance log, string userId);

    // Custom level
    [Log2Message("Retry attempt {N}", LogLevel.Warn)]
    public static partial void RetryAttempt(this Log2Instance log, int n);
}

// Usage — identical to any other log call
log.UserLoggedIn("alice");
log.RetryAttempt(3);

Diagnostics emitted by the generator:

  • LOG2GEN001 — hole count ≠ parameter count
  • LOG2GEN002 — more than 3 data parameters

Log Levels

9 severity levels plus Off, ordered from lowest to highest:

Level Value Typical use
Trace 0 Method entry/exit, variable dumps
Debug 1 Diagnostic detail for development
Info 2 Normal operational events
Warn 3 Unusual conditions, degraded state
Error 4 Failures that don't stop the application
Severe 5 Data loss or corruption detected
Critical 6 Resource exhaustion, imminent failure
Alert 7 Requires immediate human attention
Fatal 8 Application cannot continue
Off 255 Disables a source or output entirely

Log Sources

Categorize entries by the subsystem that produced them. The source parameter appears in the layout (%source) and is available for per-source level overrides and control-plane filtering.

log.Info("User logged in",          LogSource.Authentication);
log.Warn("Slow query: 450ms",       LogSource.Database);
log.Error("Connection timeout",     LogSource.Network);
log.Debug("Config loaded from disk",LogSource.Configuration);
log.Info("File written",            LogSource.FileSystem);
log.Warn("License expiring in 7d",  LogSource.License);

Available sources: Application, Network, Database, FileSystem, Security, System, Service, Connection, Authentication, Configuration, Interface, Console, WebService, Log, Event, License, Core, Client, Server, Statistic, OperatingSystem, Custom.


Structured Properties

Attach key-value pairs to log entries with zero heap allocation. Properties are binary-encoded into the native ring buffer (no intermediate strings on the hot path). Outputs that support structured data — JSON, Seq, Elasticsearch, PostgreSQL, etc. — decode them on the consumer thread.

Inline at the call site

// Single property
ReadOnlySpan<LogProperty> props = [ new LogProperty("UserId", "USR-456") ];
log.Log("Login successful", LogLevel.Info, LogSource.Authentication, props);

// Multiple properties
ReadOnlySpan<LogProperty> orderProps =
[
    new LogProperty("OrderId",    "ORD-789"),
    new LogProperty("CustomerId", "CST-123"),
    new LogProperty("Total",      "1250.00"),
    new LogProperty("Channel",    "web"),
];
log.Log("Order completed", LogLevel.Info, LogSource.Application, orderProps);

Reading properties in a custom output

// record.PropertyBytes + record.PropertyCount available in ILog2Output.Write()
var enumerator = new LogPropertyEnumerator(record.PropertyBytes, record.PropertyCount);
while (enumerator.MoveNext())
{
    string key   = enumerator.CurrentKey;
    string value = enumerator.CurrentValue;
}

// Jump directly to a key (UTF-8 literal, no allocation):
ReadOnlySpan<byte> orderId = enumerator.FindValue("OrderId"u8);

Scoped Context (AsyncLocal)

Properties pushed to LogContext are automatically attached to every log entry within the scope and flow across await boundaries.

Basic nesting

using (LogContext.Push("RequestId", Guid.NewGuid().ToString()))
{
    log.Info("Processing request");       // carries RequestId

    using (LogContext.Push("Step", "Validate"))
    {
        log.Info("Validating input");     // carries RequestId + Step
    }                                     // Step leaves scope here

    await ProcessAsync();
    log.Info("Request completed");        // still carries RequestId only
}

log.Info("Outside scope");               // no context properties

Push multiple properties at once

ReadOnlySpan<LogProperty> scope =
[
    new LogProperty("TenantId",       tenantId),
    new LogProperty("CorrelationId",  correlationId),
    new LogProperty("Environment",    "production"),
];

using (LogContext.Push(scope))
{
    log.Info("Multi-tenant operation");  // carries all three
    await DoWorkAsync();                 // context flows across awaits
}

ASP.NET Core middleware example

app.Use(async (ctx, next) =>
{
    string requestId = ctx.TraceIdentifier;
    string userId    = ctx.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";

    using (LogContext.Push("RequestId", requestId))
    using (LogContext.Push("UserId",    userId))
    {
        log.Info("Request started", LogSource.WebService);
        await next();
        log.Info("Request finished", LogSource.WebService);
    }
});

Performance notes

  • When no scope is active: a single static bool read (~1 ns), AsyncLocal is never touched.
  • Scopes nest up to 64 levels deep (MAX_DEPTH). Exceeding this is silently reported via Log2Diagnostics.
  • Always dispose LogContextHandle with using — a leaked handle creates a permanent scope chain for the async flow.

Runtime Level Switching

LogLevelSwitch allows changing the minimum log level at runtime without restarting the application. The change takes effect on the very next log call (~2–3 ns volatile-read overhead per call).

Basic usage

var levelSwitch = new LogLevelSwitch(LogLevel.Info);

using var log = Log2.Api.Log2.Builder()
    .WithLevelSwitch(levelSwitch)
    .WithConsole()
    .Build();

log.Debug("filtered out");          // below Info — dropped at ~2 ns
log.Info("visible");                // passes

// Increase verbosity at runtime (thread-safe)
levelSwitch.MinLevel = LogLevel.Debug;
log.Debug("now visible");

// Restrict to errors only
levelSwitch.MinLevel = LogLevel.Error;
log.Info("filtered out again");
log.Error("still visible");

Restricting the max level

var sw = new LogLevelSwitch(LogLevel.Warn, LogLevel.Error);
// Only Warn and Error pass; everything else is filtered.

sw.MaxLevel = LogLevel.Fatal;  // now Warn through Fatal pass

Controlled by the control plane

When Log2.ControlPlane is attached, set level debug in the CLI updates the LogLevelSwitch automatically. See src/Log2.ControlPlane/README.md.


Per-Source Level Overrides

Apply different log levels per LogSource. An override takes precedence over the global min/max for that specific source.

Via builder (static, set at build time)

using var log = Log2.Api.Log2.Builder()
    .MinLevel(LogLevel.Info)
    .Override(LogSource.Database, LogLevel.Warn)                     // DB: Warn+ only
    .Override(LogSource.Security, LogLevel.Trace)                    // Security: full trace
    .Override(LogSource.Network,  LogLevel.Debug, LogLevel.Error)    // Network: Debug..Error only
    .WithConsole()
    .Build();

log.Info("App event");                                  // passes  (global Info)
log.Info("Query cached",   LogSource.Database);         // filtered (DB override: Warn+)
log.Warn("Slow query",     LogSource.Database);         // passes
log.Trace("Auth attempt",  LogSource.Security);         // passes  (Security override: Trace+)
log.Fatal("DB is down",    LogSource.Database);         // filtered (DB override: max Error)

Via LogLevelSwitch (dynamic, change at runtime)

var sw = new LogLevelSwitch(LogLevel.Info);

using var log = Log2.Api.Log2.Builder()
    .WithLevelSwitch(sw)
    .WithConsole()
    .Build();

// Quieten DB logs at runtime
sw.SetOverride(LogSource.Database, LogLevel.Warn);
log.Info("Query", LogSource.Database);    // filtered

// Restore it
sw.ClearOverride(LogSource.Database);
log.Info("Query", LogSource.Database);    // passes (falls back to global Info)

// Remove all overrides at once
sw.ClearAllOverrides();

Via JSON configuration

{
  "log2": {
    "minLevel": "Info",
    "overrides": [
      { "source": "Database", "minLevel": "Warn" },
      { "source": "Security", "minLevel": "Trace",  "maxLevel": "Fatal" },
      { "source": "Network",  "minLevel": "Debug",  "maxLevel": "Error" }
    ],
    "sinks": [ ... ]
  }
}

Layout Patterns

Tokens

Token Description Example
%timestamp:time HH:mm:ss.ffffff 14:30:15.123456
%timestamp:date yyyy-MM-dd HH:mm:ss.ffffff 2026-03-25 14:30:15.123456
%timestamp:iso ISO 8601 2026-03-25T14:30:15.123456
%timestamp:unix Unix epoch seconds 1742920215
%level Full level name Info
%level-short Single char I
%level:-8 Left-aligned, 8 chars Info
%source Source name Application
%source:-14 Left-aligned, 14 chars Application
%thread Managed thread ID 7
%thread:-4 Left-aligned, 4 chars 7
%typecode Binary payload type tag 1
%message Raw message
%message:json JSON-escaped Hello \"world\"
%message:xml XML-escaped a &amp; b &lt;c&gt;
%message:csv RFC 4180 quoted "say ""hi"""
%property:Key Named property value
%newline OS line break
%space Single space
%tab Tab character

Width modifiers: negative = left-align (%level:-8), positive = right-align (%thread:4).

Built-in presets

Preset Use case
LogLayout.Default Console, text files — [%level:-8] %timestamp:time \| %source:-14 \| T%thread:-4 \| %message%newline
LogLayout.Compact Minimal — %level-short %timestamp:time %message%newline
LogLayout.Full Detailed files — %timestamp:iso %level:-8 %source:-14 T%thread:-4 %message%newline
LogLayout.MessageOnly Pipe to external tool — %message%newline
LogLayout.JsonLines JSON-Lines files — {"ts":"...","level":"...","source":"...","thread":N,"msg":"..."}
LogLayout.Xml XML fragments — <entry ts="..." level="..." source="..." thread="N">...</entry>
LogLayout.Csv CSV files — %timestamp:iso,%level,%source,%thread,%message:csv%newline

Built-in presets use compiled renderers — no token loop overhead.

Custom layout examples

// Compact with ISO timestamp
.DefaultLayout("%level-short %timestamp:iso %message%newline")

// JSON-Lines with extra field
.DefaultLayout("{\"ts\":\"%timestamp:iso\",\"lvl\":\"%level\",\"src\":\"%source\",\"msg\":\"%message:json\"}%newline")

// Minimal for piped consumers
.DefaultLayout("%message%newline")

Per-output layout override

var jsonLayout = LogLayout.Parse(
    "{\"ts\":\"%timestamp:iso\",\"level\":\"%level\",\"msg\":\"%message:json\"}%newline"
);

using var log = Log2.Api.Log2.Builder()
    .WithConsole(layout: LogLayout.Compact)
    .WithFile("logs", "myapp", layout: LogLayout.Full)
    .WithJson("logs", "myapp",  layout: jsonLayout)
    .Build();

Outputs

Built-in (included in Log2)

Output Description Key features
Console ANSI-colored terminal Pinned 4 KB buffer, per-level color
File Text file with rotation Size-based rotation, Age/Count/TotalSize cleanup, file headers
CSV RFC 4180 CSV SIMD smart quoting, auto column header
JSON JSON-Lines format Rotation, %message:json escaping
XML XML fragment file Rotation, %message:xml escaping
Null Discards everything Testing, benchmarking
Stdout Raw stdout (no colors) Container / pipe scenarios
UDP Fire-and-forget datagrams Wire protocol, DontFragment, circuit breaker
TCP Reliable length-framed Wire protocol, Nagle disabled, auto-reconnect, circuit breaker
Named Pipe Windows named pipe Low-latency local IPC
Unix Socket Unix domain socket Local IPC on Linux/macOS
Shared Memory Memory-mapped file Zero-copy local IPC on Windows
SMTP Email alerts Bounded channel, SSL/TLS, circuit breaker
WebService HTTP POST Bearer auth, bounded channel, circuit breaker
EventViewer Windows Event Log Windows only, maps level to EventLog severity

External NuGet packages

Package Destination Protocol / Format
Log2.Outputs.Elasticsearch Elasticsearch Bulk API, NDJSON, date-based indices
Log2.Outputs.Seq Seq CLEF over HTTP
Log2.Outputs.PostgreSQL PostgreSQL Binary COPY protocol
Log2.Outputs.SqlServer SQL Server SqlBulkCopy with double-buffering
Log2.Outputs.Redis Redis List (RPUSH), Stream (XADD), or PubSub
Log2.Outputs.ClickHouse ClickHouse HTTP + JSONEachRow
Log2.Outputs.RabbitMQ RabbitMQ Exchange publish, persistent messages
Log2.Outputs.Kafka Kafka Fire-and-forget produce, LZ4 compression
Log2.Outputs.Discord Discord Webhook embeds with emoji
Log2.Outputs.Slack Slack Webhook with color and emoji
Log2.Outputs.MicrosoftTeams Microsoft Teams MessageCard webhook
Log2.Outputs.AmazonCloudWatch CloudWatch Logs Batch PutLogEvents
Log2.Outputs.GoogleCloudLogging GCP Logging Cloud Logging v2 API
Log2.Outputs.AzureAppInsights Application Insights TraceTelemetry with severity mapping
Log2.Outputs.OpenTelemetry OTLP Collector OTLP/HTTP JSON
Log2.Outputs.Telegram Telegram Bot API messages

All outputs support per-output minLevel / maxLevel filtering and custom layouts.

File rotation and cleanup

.WithFile("logs", "myapp",
    maxSizeMB:      100,                                    // rotate when file hits 100 MB
    cleanup:        FileOutput.CleanupMode.Age              // delete files older than N days
                  | FileOutput.CleanupMode.TotalSize,       // AND enforce total size cap
    maxAgeDays:     30,
    maxTotalSizeMB: 1024,                                   // keep at most 1 GB total
    maxFileCount:   50,                                     // keep at most 50 files
    fileHeader:     "# Application log\n")                  // written to every new/rotated file

Cleanup modes combine with |. All enabled criteria run on every cleanup pass in order: Age → Count → TotalSize.


JSON Configuration

Basic

{
  "log2": {
    "minLevel": "Debug",
    "maxLevel": "Fatal",
    "channelCapacity": 65536,
    "overflowMode": "Drop",
    "layout": "[%level:-8] %timestamp:time | %message%newline",
    "sinks": [
      { "id": "console", "type": "Console", "useColors": true },
      {
        "id": "file", "type": "File",
        "directory": "logs", "prefix": "myapp",
        "maxSizeMB": 100, "cleanup": "Age|TotalSize", "maxAgeDays": 30
      }
    ]
  }
}

With per-source overrides

{
  "log2": {
    "minLevel": "Info",
    "overrides": [
      { "source": "Database", "minLevel": "Warn" },
      { "source": "Security", "minLevel": "Trace" },
      { "source": "Network",  "minLevel": "Debug", "maxLevel": "Error" }
    ],
    "sinks": [
      { "id": "console", "type": "Console" },
      { "id": "file",    "type": "File", "directory": "logs", "prefix": "myapp" }
    ]
  }
}

Full example — all built-in output types

{
  "log2": {
    "minLevel": "Trace",
    "channelCapacity": 65536,
    "layout": "[%level:-8] %timestamp:time | %source:-14 | T%thread:-4 | %message%newline",
    "sinks": [
      { "id": "console", "type": "Console", "useColors": true, "minLevel": "Debug" },
      {
        "id": "file", "type": "File",
        "directory": "logs", "prefix": "myapp",
        "maxSizeMB": 100, "cleanup": "Age|TotalSize",
        "maxAgeDays": 60, "maxTotalSizeMB": 500, "minLevel": "Info"
      },
      {
        "id": "csv", "type": "Csv",
        "directory": "logs", "prefix": "myapp",
        "maxSizeMB": 50, "cleanup": "Count", "maxFileCount": 10
      },
      { "id": "json", "type": "Json", "directory": "logs", "prefix": "myapp", "minLevel": "Warn" },
      { "id": "udp",  "type": "Udp",  "port": 5557 },
      { "id": "tcp",  "type": "Tcp",  "port": 5558, "minLevel": "Info" },
      {
        "id": "smtp", "type": "Smtp", "minLevel": "Error",
        "smtpHost": "smtp.example.com", "smtpPort": 587,
        "from": "alerts@example.com", "to": "oncall@example.com",
        "useSsl": true, "username": "user", "password": "pass"
      },
      {
        "id": "web", "type": "WebService", "minLevel": "Warn",
        "endpoint": "https://logs.example.com/ingest",
        "authHeader": "Bearer <token>"
      },
      { "id": "evtlog", "type": "EventViewer", "minLevel": "Error", "eventSource": "MyApp" }
    ]
  }
}

Loading

using Log2.Config;

using var log = Log2ConfigLoader.FromFile("log2.json");
Log2.Api.Log2.Configure(log);

See log2.example.json for a fully commented reference including RabbitMQ and Kafka.


Dynamic Output Management

Add or remove outputs at runtime without stopping the logger:

using var log = Log2.Api.Log2.Builder()
    .WithConsole()
    .Build();

// Add a file output after startup
var file = new FileOutput("file", "logs", "myapp", LogLayout.Default,
                          LogLevel.Info, LogLevel.Fatal);
log.AddOutput(file);

// Remove by ID
log.RemoveOutput("file");

// Inspect what is currently active
ILog2Output[] current = log.Outputs;
foreach (var o in current)
    Console.WriteLine($"{o.Id}  {o.MinLevel}–{o.MaxLevel}");

Binary Logging

Log any unmanaged struct directly with zero managed allocations. The struct is copied as raw bytes into the native ring buffer inline payload (or slab for structs > 40 bytes):

[StructLayout(LayoutKind.Sequential)]
struct SensorReading
{
    public float Temperature;
    public float Humidity;
    public long  TimestampTicks;
}

var reading = new SensorReading
{
    Temperature   = 23.5f,
    Humidity      = 65.0f,
    TimestampTicks = DateTime.UtcNow.Ticks,
};

log.LogBinary(in reading, LogLevel.Info, LogSource.Application, typeCode: 1);

Binary entries are identified by EntryKind.Binary and the typeCode field. Custom outputs receive the raw bytes via LogRecord and can decode them based on the type code.


Diagnostics

Monitor the logging pipeline itself — output failures, dropped entries, circuit breaker transitions:

// Custom handler
Log2Diagnostics.Enable(msg => Console.Error.WriteLine($"[Log2] {msg}"));

// Built-in stderr shortcut
Log2Diagnostics.EnableToStdErr();

// Disable
Log2Diagnostics.Disable();

Zero overhead when disabled — the implementation is a single null check (s_handler?.Invoke).

What gets reported:

  • Output write failures (with exception details)
  • Entries dropped due to full ring buffer
  • Circuit breaker state changes (Open ↔ Closed)
  • LogContext scope chain depth exceeded (MAX_DEPTH = 64)
  • Engine errors

Graceful Shutdown

Dispose() signals the consumer thread, drains remaining entries from the native ring buffer, and flushes all outputs:

var log = Log2.Api.Log2.Builder()
    .DrainTimeout(TimeSpan.FromSeconds(10))  // wait up to 10 s for pending entries
    .WithFile("logs", "myapp")
    .Build();

// ... application runs ...

log.Dispose();  // flushes all outputs, waits for drain, frees native memory

If a control plane agent is attached, dispose it before the logger:

cp?.Dispose();  // closes all sessions and listeners
log.Dispose();  // drains and flushes

Architecture

Producers (N threads)
  +-- LogInstance.EnqueueString / EnqueueBinary
       +-- FilterEngine (bitmask: AND + shift + compare, 2 instructions)
            +-- NativeRingBuffer.TryWrite (single CAS)

Consumer (1 background thread — LogEngine)
  +-- NativeRingBuffer.TryRead
       +-- NativeLogEntry --> LogRecord (ref struct, zero alloc)
            +-- SinkRouter.Route --> ILogSink[]
                 |-- IO/   Console, File, Csv, Json, Xml, Stdout
                 |-- IPC/  NamedPipe, UnixSocket, SharedMemory
                 |-- Net/  Tcp (WireEncoder), Udp (WireEncoder)
                 |-- Sys/  EventViewer, Smtp
                 +-- Web/  WebService, RabbitMq, Kafka, Elasticsearch, ...

Native Memory (GC-invisible)
  +-- NativeRingBuffer : NativeLogEntry[65536] + int[65536] (published flags)
  +-- NativeSlab       : 16384 x 512B blocks  (overflow payloads > 40B)

Layout : LayoutParser --> LayoutToken[] --> LayoutRenderer (Span<byte>, SIMD escaping)
Config : LogConfigLoader --> LogConfig --> LogBuilder --> LogEngine + SinkRouter

For detailed internal component documentation see docs/TOPOLOGY.md.


Performance

Key optimisations

Principle Implementation
Zero GC roots in ring buffer NativeRingBuffer + NativeLogEntry (64 B, zero managed refs) in NativeMemory
Zero allocation hot path 40 B inline payload covers ~95 % of messages; no new, no boxing
Lock-free MPSC Single CAS per write; PaddedLong on 128-byte cache lines
Native slab allocator NativeSlab replaces ArrayPool<byte>; ABA-safe free list, one CAS per rent/return
Minimal instructions Filter = AND + shift + compare; bitmask check before any work
Cache-line alignment NativeLogEntry = exactly one cache line (64 B); arrays 64 B-aligned
False sharing prevention PaddedLong (128 B) for _claimedTail, _head, _headCache, _dropped
No syscalls on hot path FastClock instead of DateTime.UtcNow; [ThreadStatic] thread ID cache
SIMD acceleration Ascii.FromUtf16 for ASCII fast path; SearchValues for CSV quoting
Compiled layouts Hand-optimized render methods for built-in presets (no token loop)
Single-output fast path OutputRouter._sole: skip loop when one output covers [Trace, Fatal]
Lazy context LogContext.s_anyActive gate: AsyncLocal never touched when no scope is active

GC impact

Metric Before FIX #6 After FIX #6
Managed refs in ring 131,072 (65K slots × 2 byte[]) 0
GC Mark cost O(N) — scans every ref O(1) — nothing to scan
Pool contention ArrayPool bucket locks Single CAS (NativeSlab)
Inline messages None (always rents from pool) ~95 % (40 B inline capacity)

Benchmarks — Log2 vs Serilog vs NLog

End-to-end throughput comparison on .NET 9, Windows 11 (AMD Ryzen, March 2026). Each scenario runs 9 rounds with rotating execution order to eliminate JIT/cache bias. Results show the best round per library, normalized to the fastest (100%).

Single-thread text file
Scenario Log2 Serilog NLog
Short (60 B) × 50K 100% 51% 46%
Medium (200 B) × 50K 32% 44% 100%
Long (1 KB) × 20K 41% 54% 100%
Mixed size+level × 50K 69% 100% 73%
Unicode (CJK/emoji) × 30K 79% 98% 100%
Multi-thread text file
Scenario Log2 Serilog NLog
4T × 25K (100K total) 100% 33% 47%
8T × 12.5K (100K total) 100% 61% 85%
16T × 6.25K (100K total) 100% 41% 69%
JSON file
Scenario Log2 Serilog NLog
Short (60 B) × 50K 69% 82% 100%
Medium (200 B) × 50K 57% 97% 100%
Long (1 KB) × 20K 100% 34% 48%
Mixed × 50K 100% 87% 63%
Unicode × 30K 100% 81% 50%
4T × 25K (100K total) 100% 29% 30%
CSV file
Scenario Log2 Serilog NLog
Clean × 50K 100% 45% 52%
Specials (",\n,\t) × 30K 47% 62% 100%
Specials 4T × 25K 73% 93% 100%
Structured properties
Scenario Log2 Serilog NLog
1 prop × 50K 73% 61% 100%
4 props × 50K 100% 17% 38%
4 props mixed level × 50K 100% 42% 62%
4 props 4T × 25K 100% 18% 22%
Scoped context (AsyncLocal)
Scenario Log2 Serilog NLog
Baseline (no scope) × 50K 100% 50% 76%
1 prop × 50K 85% 42% 100%
4 props × 50K 100% 66% 76%
4 props 4T × 25K 100% 24% 43%
Fanout (multiple outputs)
Scenario Log2 Serilog NLog
2 outputs × 50K 95% 80% 100%
3 outputs × 50K 85% 100% 60%
3 outputs 4T × 25K 100% 80% 78%
Filtering
Scenario Log2 Serilog NLog
Filtered-out 100K 84% 55% 100%
Pass-through 100K 100% 78% 82%
Mixed-levels (20% pass) 100K 100% 31% 30%

Summary: Log2 wins 20 of 33 scenarios. Dominates in concurrency (all multi-thread tests), heavy structured properties (4.5–5.5× faster), and long/mixed JSON. NLog leads in some single-thread medium/long text. Serilog leads in a few mixed workloads. Run dotnet test tests/Log2.Tests/ --filter "FullyQualifiedName~Comparison" --logger "console;verbosity=detailed" to reproduce.

BDN single-message baseline (Log2 94.87 ns vs ZLogger 224 ns vs Serilog 364 ns vs NLog 590 ns, .NET 9, post-parity build). Run .\scripts\run-comparison-bench.ps1 for a 3-run variance-aware measurement. See docs/bench-methodology.md for interpretation guidelines.


Project Structure

Project Path Description
Log2 src/Log2 Core library — ring buffer, hot path, layout rendering, outputs
Log2.SourceGenerators src/Log2.SourceGenerators Roslyn incremental source generator ([LogMessage])
Log2.Extensions.Logging src/Log2.Extensions.Logging Microsoft.Extensions.Logging adapter
Log2.ControlPlane src/Log2.ControlPlane Embedded agent for live inspection and runtime reconfiguration
Log2.Cli src/Log2.Cli log2 global tool (CLI)
Input2Log src/Input2Log Stdin-to-Log2 bridge utility
Log2.Sinks src/Log2.Sinks Shared sink base classes (BatchSinkBase, HttpBatchSinkBase, ChannelSinkBase)
Log2.Sinks.Memory src/Log2.Sinks.Memory In-memory ring-buffer sink for testing and runtime inspection
Log2.Sinks.Slack src/Log2.Sinks.Slack Slack webhook sink
Log2.Sinks.Discord src/Log2.Sinks.Discord Discord webhook sink
Log2.Sinks.MicrosoftTeams src/Log2.Sinks.MicrosoftTeams Microsoft Teams webhook sink
Log2.Sinks.Elasticsearch src/Log2.Sinks.Elasticsearch Elasticsearch Bulk API sink
Log2.Sinks.Kafka src/Log2.Sinks.Kafka Apache Kafka producer sink
Log2.Sinks.Redis src/Log2.Sinks.Redis Redis List/Stream/PubSub sink
Log2.Sinks.RabbitMQ src/Log2.Sinks.RabbitMQ RabbitMQ AMQP sink
Log2.Sinks.SqlServer src/Log2.Sinks.SqlServer SQL Server bulk-insert sink
Log2.Sinks.PostgreSQL src/Log2.Sinks.PostgreSQL PostgreSQL COPY protocol sink
Log2.Sinks.ClickHouse src/Log2.Sinks.ClickHouse ClickHouse HTTP insert sink
Log2.Sinks.AmazonCloudWatch src/Log2.Sinks.AmazonCloudWatch Amazon CloudWatch Logs sink
Log2.Sinks.AzureAppInsights src/Log2.Sinks.AzureAppInsights Azure Application Insights sink
Log2.Sinks.GoogleCloudLogging src/Log2.Sinks.GoogleCloudLogging Google Cloud Logging sink
Log2.Sinks.OpenTelemetry src/Log2.Sinks.OpenTelemetry OpenTelemetry OTLP sink
Log2.Sinks.Observability src/Log2.Sinks.Observability Meta-package: Seq, Elasticsearch, OpenTelemetry via Relay
Log2.Tests tests/Log2.Tests Core unit, integration, benchmark, and stress tests
Log2.Tests.Sinks tests/Log2.Tests.Sinks Shared sink test helpers and cross-sink integration tests
Log2.Tests.Sinks.Memory tests/Log2.Tests.Sinks.Memory Tests for LogMemorySink
Log2.Tests.Sinks.Slack tests/Log2.Tests.Sinks.Slack Tests for SlackSink
Log2.Tests.Sinks.Discord tests/Log2.Tests.Sinks.Discord Tests for DiscordSink
Log2.Tests.Sinks.MicrosoftTeams tests/Log2.Tests.Sinks.MicrosoftTeams Tests for MicrosoftTeamsSink
Log2.Tests.Sinks.Elasticsearch tests/Log2.Tests.Sinks.Elasticsearch Tests for ElasticsearchSink
Log2.Tests.Sinks.Kafka tests/Log2.Tests.Sinks.Kafka Tests for KafkaSink
Log2.Tests.Sinks.Redis tests/Log2.Tests.Sinks.Redis Tests for RedisSink
Log2.Tests.Sinks.RabbitMQ tests/Log2.Tests.Sinks.RabbitMQ Tests for RabbitMQSink
Log2.Tests.Sinks.SqlServer tests/Log2.Tests.Sinks.SqlServer Tests for SqlServerSink
Log2.Tests.Sinks.PostgreSQL tests/Log2.Tests.Sinks.PostgreSQL Tests for PostgreSqlSink
Log2.Tests.Sinks.ClickHouse tests/Log2.Tests.Sinks.ClickHouse Tests for ClickHouseSink
Log2.Tests.Sinks.AmazonCloudWatch tests/Log2.Tests.Sinks.AmazonCloudWatch Tests for AmazonCloudWatchSink
Log2.Tests.Sinks.AzureAppInsights tests/Log2.Tests.Sinks.AzureAppInsights Tests for AzureAppInsightsSink
Log2.Tests.Sinks.GoogleCloudLogging tests/Log2.Tests.Sinks.GoogleCloudLogging Tests for GoogleCloudLoggingSink
Log2.Tests.Sinks.OpenTelemetry tests/Log2.Tests.Sinks.OpenTelemetry Tests for OpenTelemetrySink
Log2.Tests.SourceGenerators tests/Log2.Tests.SourceGenerators Tests for [LogMessage] source generator
Log2.Tests.Extensions.Logging tests/Log2.Tests.Extensions.Logging Tests for ILogger adapter
Log2.Tests.ControlPlane tests/Log2.Tests.ControlPlane Tests for ControlPlane agent
Log2.Tests.Cli tests/Log2.Tests.Cli Tests for log2 CLI tool
Log2.Benchmarks tests/Log2.Benchmarks BenchmarkDotNet throughput and comparison benchmarks
Log2.Example examples/Log2.Example Runnable usage example

Resumo (PT-BR)

Log2 é uma biblioteca de logging estruturado de ultra-alta performance para .NET 9+.

O produtor codifica a mensagem inline em 40 bytes no buffer nativo e retorna imediatamente — sem alocações, sem boxing. Um único thread consumidor em background lê o ring buffer, constrói um ref struct LogRecord sem heap e roteia para todos os outputs configurados.

Destaques:

  • Ring buffer MPSC lock-free com memória nativa (NativeRingBuffer) — zero referências gerenciadas, custo O(1) no GC
  • Slab allocator nativo (NativeSlab) substitui ArrayPool<byte> — ~95% das mensagens cabem no payload inline de 40 bytes
  • Zero alocação no caminho quente: sem new, sem boxing, sem closures
  • Aceleração SIMD via Ascii.FromUtf16 e SearchValues
  • 30+ outputs — 15 built-in + 16 pacotes NuGet externos
  • Troca de nível em runtime via LogLevelSwitch (~2–3 ns de overhead por chamada)
  • Overrides por LogSource, contexto async (LogContext.Push) e propriedades estruturadas zero-alocação
  • Circuit breakers em outputs de rede com auto-recovery

Instalação:

dotnet add package Log2

Benchmarks comparativos vs. Serilog e NLog na seção Performance. Documentação interna em docs/TOPOLOGY.md.


License

MIT

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (22)

Showing the top 5 NuGet packages that depend on Log2:

Package Downloads
Log2.Sinks

Shared infrastructure for Log2 sink packages. Provides HttpBatchSinkBase, ChannelSinkBase, and CircuitBreaker for building high-performance remote sinks.

Log2.Sinks.Elasticsearch

Log2 output for Elasticsearch using the Bulk API over HTTP.

Log2.Sinks.RabbitMQ

Log2 output for RabbitMQ using RabbitMQ.Client 7.x async-first API with persistent message support.

Log2.Benchmarks

Package Description

Log2.Sinks.Slack

Log2 output that delivers log entries to Slack via Incoming Webhooks.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 361 5/10/2026

v1.0.0 — initial release. See CHANGELOG.md for details.