Log2 1.0.0
dotnet add package Log2 --version 1.0.0
NuGet\Install-Package Log2 -Version 1.0.0
<PackageReference Include="Log2" Version="1.0.0" />
<PackageVersion Include="Log2" Version="1.0.0" />
<PackageReference Include="Log2" />
paket add Log2 --version 1.0.0
#r "nuget: Log2, 1.0.0"
#:package Log2@1.0.0
#addin nuget:?package=Log2&version=1.0.0
#tool nuget:?package=Log2&version=1.0.0
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:
src/Log2.ControlPlane/README.md— embedded agent for live inspection and reconfigurationsrc/Log2.Cli/README.md—log2global tool
Table of Contents
- Features
- Install
- Quick Start
- Builder API
- Logging Methods
- Log Levels
- Log Sources
- Structured Properties
- Scoped Context (AsyncLocal)
- Runtime Level Switching
- Per-Source Level Overrides
- Layout Patterns
- Outputs
- JSON Configuration
- Dynamic Output Management
- Binary Logging
- Diagnostics
- Graceful Shutdown
- Architecture
- Performance
- License
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 buffer —
NativeRingBufferbacked byNativeMemory.AlignedAlloc; zero managed references, O(1) GC Mark cost - Lock-free MPSC — single CAS per write,
PaddedLongcounters on separate 128-byte cache lines,HeadCacheavoids cross-core volatile reads - Native slab allocator —
NativeSlabreplacesArrayPool<byte>for overflow payloads; ABA-safe free list, one CAS per rent/return - SIMD-accelerated — ASCII fast path via
Ascii.FromUtf16,SearchValuesfor 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 context —
LogContext.Push()withAsyncLocalflow acrossawaitboundaries - Runtime level switching — change log levels without restart via
LogLevelSwitch(~2–3 ns overhead) - Per-source overrides — different min/max levels per
LogSourcevia builder orLogLevelSwitch - Compiled layouts — hand-optimized render methods for built-in patterns bypass the token loop
- Configurable overflow —
Drop(default, never blocks) orBlock(guaranteed delivery for audit/compliance) - JSON configuration — load from
log2.jsonor 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 countLOG2GEN002— 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
boolread (~1 ns),AsyncLocalis never touched. - Scopes nest up to 64 levels deep (
MAX_DEPTH). Exceeding this is silently reported viaLog2Diagnostics. - Always dispose
LogContextHandlewithusing— 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 & b <c> |
%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)
LogContextscope 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.ps1for a 3-run variance-aware measurement. Seedocs/bench-methodology.mdfor 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) substituiArrayPool<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.FromUtf16eSearchValues - 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 | Versions 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. |
-
net9.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
- Relay (>= 1.0.0)
- System.Diagnostics.EventLog (>= 9.0.0)
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 | 360 | 5/10/2026 |
v1.0.0 — initial release. See CHANGELOG.md for details.