PicoLog.Json
2026.10.0
dotnet add package PicoLog.Json --version 2026.10.0
NuGet\Install-Package PicoLog.Json -Version 2026.10.0
<PackageReference Include="PicoLog.Json" Version="2026.10.0" />
<PackageVersion Include="PicoLog.Json" Version="2026.10.0" />
<PackageReference Include="PicoLog.Json" />
paket add PicoLog.Json --version 2026.10.0
#r "nuget: PicoLog.Json, 2026.10.0"
#:package PicoLog.Json@2026.10.0
#addin nuget:?package=PicoLog.Json&version=2026.10.0
#tool nuget:?package=PicoLog.Json&version=2026.10.0
PicoLog
High-performance structured logging for .NET Native AOT.
English | 简体中文 | 日本語 | Español | Português | 繁體中文 | 한국어 | Français | Deutsch | Русский
Performance
PicoLog vs Microsoft.Extensions.Logging on .NET 10.0.5 (Windows 10, X64, Release).
| Test Case | PicoLog (ns) | MEL Baseline (ns) | Speedup |
|---|---|---|---|
| Cached message, single write | 303 | 434 | 1.43x |
| Cached + 1 scope | 402 | 434 | 1.08x |
| Cached + 4 properties | 324 | 434 | 1.34x |
| Timestamp acquisition only | 59 | 434 | 7.36x |
| LogEntry allocation only | 46 | 434 | 9.33x |
Summary: 13/30 wins, average 3.51x speedup, max 25.76x.
PicoLog is faster than MEL on cached-message and allocation-heavy paths where source generation eliminates boxing and reflection. On single-message throughput (N=1), PicoLog is deliberately slower — roughly 700ns vs MEL's 200ns per call. This is not a bug; it's the cost of an async-first pipeline architecture.
Design Rationale: Async-First Pipeline
Every Log() call traverses the same pipeline regardless of whether you call
the synchronous or asynchronous overload:
Log() → CreateEntry → CategoryPipeline → Channel → ProcessEntriesAsync → DispatchEntryAsync → Sink
This is fundamentally different from MEL's synchronous model:
Log() → Logger.Log() → foreach (logger in loggers) → logger.Log() // direct, no channel
| Aspect | PicoLog (async-first) | MEL (sync-first) |
|---|---|---|
| Delivery | Guaranteed through bounded channel | Best-effort; sync discard can drop if logger throws |
| Backpressure | DropOldest / DropWrite / Wait — configurable per factory |
None — calling thread blocks or message is lost |
| Flush | FlushAsync() drains all pending entries before shutdown |
No flush API; Dispose() is instant |
| Timestamp | Precise — captured at CreateEntry() before enqueue |
Captured inside logger, may be delayed |
| Thread safety | Channel-based; no lock on hot path | Each logger manages its own synchronization |
| Per-call overhead | ~500–800ns (Channel + dispatcher + sink) | ~100–200ns (direct dispatch) |
PicoLog chose the async pipeline because production logging systems need guaranteed delivery, backpressure, and graceful shutdown more than they need the absolute lowest single-message latency. If you're logging at 1M msg/s, the per-message overhead dominates. If you're logging at 1K msg/s with flush semantics, PicoLog's guarantees matter more.
For direct throughput comparisons across all scenarios, see benchmark results.
Quick Start
dotnet add package PicoLog
using PicoLog;
using PicoLog.Abs;
var sink = new ColoredConsoleSink(new ConsoleFormatter());
using var factory = new LoggerFactory([sink],
new LoggerFactoryOptions { MinLevel = LogLevel.Info });
var logger = factory.CreateLogger("App");
logger.Info("Application started");
Log Levels
| Level | Value | Usage |
|---|---|---|
| Emergency | 0 | System is unusable |
| Alert | 1 | Action must be taken immediately |
| Critical | 2 | Critical conditions |
| Error | 3 | Error conditions |
| Warning | 4 | Warning conditions |
| Notice | 5 | Normal but significant |
| Info | 6 | Informational messages |
| Debug | 7 | Debug-level messages |
| Trace | 8 | Detailed diagnostic tracing |
| None | 255 | Disables all logging |
Core API
LoggerFactory
using var factory = new LoggerFactory(
sinks: [new ColoredConsoleSink(new ConsoleFormatter())],
options: new LoggerFactoryOptions
{
MinLevel = LogLevel.Info,
QueueCapacity = 65535,
QueueFullMode = LogQueueFullMode.DropOldest,
SyncWriteTimeout = TimeSpan.FromMilliseconds(250),
ShutdownTimeout = TimeSpan.FromSeconds(5)
});
ILogger logger = factory.CreateLogger("MyComponent");
Typed Logger
public sealed class OrderService(ILoggerFactory factory)
{
private readonly ILogger<OrderService> _logger = new Logger<OrderService>(factory);
public void Process(Order order)
{
_logger.Info($"Processing order {order.Id}");
}
}
Extension Methods
Level-specific convenience helpers provided as extension methods on ILogger. Not part of the interface contract — consumers only implement the 8 core interface methods.
logger.Trace("Detailed diagnostic");
logger.Debug("Debug information");
logger.Info("Application event");
logger.Notice("Significant event");
logger.Warning("Unexpected condition");
logger.Error("Operation failed", exception);
logger.Critical("Severe failure");
logger.Alert("Immediate action required");
logger.Emergency("System is down");
Each has a FormattableString overload for deferred formatting and an EventId overload for structured event identification. Async variants (TraceAsync, InfoAsync, etc.) are available with CancellationToken support.
Message Templates
// FormattableString — template and arguments preserved for deferred formatting
logger.Info($"Processing order {orderId} for {customer}");
// Structured properties — explicit key-value pairs
logger.LogStructured(LogLevel.Error, "Payment failed",
properties: [new("OrderId", orderId), new("Amount", amount)],
exception: ex);
// EventId — tagged for monitoring and alerting
logger.Info(new EventId(1001, "OrderPlaced"), $"Order {orderId} placed");
Source-Generated Messages
The [PicoLogMessage] generator is embedded in PicoLog.Abs (no extra package). Reference PicoLog.Abs and the analyzer activates automatically:
public static partial class AppLogs
{
[PicoLogMessage(LogLevel.Info, EventId = 1001, Message = "Order {OrderId} placed")]
public static partial void OrderPlaced(this ILogger logger, string orderId);
[PicoLogMessage(LogLevel.Error, EventId = 2001,
EventName = "PaymentFailed", Message = "Payment of {Amount} failed")]
public static partial void PaymentFailed(this ILogger logger, decimal amount);
}
logger.OrderPlaced("ORD-12345");
logger.PaymentFailed(99.99m);
The generator emits Log calls with FormattableString at compile time — zero runtime reflection.
Sinks
Built-in
// Plain console
new ConsoleSink(new ConsoleFormatter())
// Color-coded console (Trace=Gray, Debug=Cyan, Info=Green, Error=Red, ...)
new ColoredConsoleSink(new ConsoleFormatter())
// File with batching
new FileSink(new ConsoleFormatter(), new FileSinkOptions
{
FilePath = "logs/app.log",
BatchSize = 32,
QueueCapacity = 4096,
FlushInterval = TimeSpan.FromMilliseconds(100)
})
Custom
public sealed class CustomSink : ILogSink
{
public Task WriteAsync(LogEntry entry, CancellationToken ct = default)
{
// Write to your backend
return Task.CompletedTask;
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
Implement IFlushableLogSink to support explicit flush:
public sealed class BufferedSink : IFlushableLogSink
{
public Task WriteAsync(LogEntry entry, CancellationToken ct = default) { /* buffer */ }
public ValueTask FlushAsync(CancellationToken ct = default) { /* flush buffer */ }
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
Custom Formatter
public sealed class JsonFormatter : ILogFormatter
{
public string Format(LogEntry entry)
{
return JsonSerializer.Serialize(new
{
entry.Timestamp,
Level = entry.Level.ToString(),
entry.Category,
entry.Message,
entry.Exception?.Message
});
}
}
Logging Scopes
using (logger.BeginScope(new { RequestId = "abc-123" }))
{
logger.Info("Processing request");
// Logs include scope context
}
Configuration
Queue Behavior
Three backpressure strategies when the queue is full:
| Mode | Behavior |
|---|---|
| DropOldest (default) | Evict the oldest entry, accept the new one |
| DropWrite | Discard the new entry |
| Wait | Block or await until space is available |
var options = new LoggerFactoryOptions
{
QueueCapacity = 10000,
QueueFullMode = LogQueueFullMode.Wait,
SyncWriteTimeout = TimeSpan.FromMilliseconds(500)
};
Category Filtering
var options = new LoggerFactoryOptions
{
MinLevel = LogLevel.Info, // global default
FilterRules =
{
new LogFilterRule("Microsoft", LogLevel.Warning),
new LogFilterRule("MyApp.Data", LogLevel.Debug)
}
};
Rules are applied in reverse order — the last matching rule wins.
Dropped Message Notification
var options = new LoggerFactoryOptions
{
QueueCapacity = 1000,
OnMessagesDropped = (category, count) =>
Console.Error.WriteLine($"Dropped {count} messages for {category}")
};
Timestamp Control
var options = new LoggerFactoryOptions
{
TimestampProvider = TimeProvider.System // default; inject fake for testing
};
Flush & Shutdown
using var factory = new LoggerFactory(sinks, options);
// Flush without shutdown — drains pending entries, factory remains usable
await factory.FlushAsync();
// Dispose drains all pipelines and sinks, aggregates errors
await factory.DisposeAsync();
Set ShutdownTimeout to bound the drain:
var options = new LoggerFactoryOptions
{
ShutdownTimeout = TimeSpan.FromSeconds(5) // TimeSpan.Zero = wait indefinitely
};
On shutdown timeout, sinks receive a drain CancellationToken. The factory waits for the processing pipeline to finish draining before disposing resources.
DI Integration (PicoLog.DI)
dotnet add package PicoLog.DI
using PicoLog.DI;
container.AddPicoLog(o =>
{
o.MinLevel = LogLevel.Info;
o.WriteTo.ColoredConsole();
o.WriteTo.File("logs/app.log");
o.WriteTo.File(opts =>
{
opts.FilePath = "logs/errors.log";
opts.BatchSize = 1; // write immediately
});
o.Factory.QueueFullMode = LogQueueFullMode.DropOldest;
});
// Resolve typed logger
var logger = container.CreateScope().GetService<ILogger<Program>>();
Consume Registered Sinks
// Register a custom sink in DI, then tell PicoLog to use it
container.RegisterSingleton<ILogSink>(_ => new CustomSink());
container.AddPicoLog(o =>
{
o.ReadFrom.RegisteredSinks(); // discover ILogSink from container
o.WriteTo.ColoredConsole(); // additional owned sinks
});
Custom Sink Registration
container.AddPicoLog(o =>
{
o.WriteTo.Sink(new CustomSink());
o.WriteTo.Sink(() => new LazySink()); // factory
o.WriteTo.Sink(fmt => new FormattedSink(fmt)); // formatter-aware factory
});
OpenTelemetry Metrics
PicoLogMetrics exposes standard counters via System.Diagnostics.Metrics:
| Metric | Description |
|---|---|
picolog.entries.enqueued |
Total entries accepted |
picolog.entries.dropped |
Entries dropped due to queue full |
picolog.sinks.failures |
Sink write failures |
picolog.writes.rejected_after_shutdown |
Writes rejected after factory shutdown |
picolog.queue.entries |
Current queued entries (observable gauge) |
picolog.shutdown.drain.duration |
Shutdown drain time in ms (histogram) |
All metrics are AOT-compatible and integrate with OpenTelemetry collectors.
Packages
| Package | TFM | Description |
|---|---|---|
| PicoLog | net10.0 | Logging runtime: LoggerFactory, sinks, formatters |
| PicoLog.Abs | net10.0 | ILogger, ILogSink, LogLevel, LogEntry, EventId |
| PicoLog.Gen | netstandard2.0 | [PicoLogMessage] source generator (embedded in PicoLog.Abs; not a standalone package) |
| PicoLog.DI | net10.0 | DI integration (AddPicoLog, WriteTo, ReadFrom) |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- PicoJetson (>= 2026.10.0)
- PicoLog (>= 2026.10.0)
- PicoLog.Abs (>= 2026.10.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2026.10.0 | 79 | 9/18/2026 |
| 2026.9.3 | 80 | 9/15/2026 |
| 2026.9.2 | 81 | 9/14/2026 |
| 2026.9.1 | 114 | 8/24/2026 |
| 2026.9.0 | 100 | 8/23/2026 |
| 2026.8.9 | 101 | 8/22/2026 |
| 2026.8.8 | 97 | 8/22/2026 |
| 2026.8.7 | 94 | 8/21/2026 |
| 2026.8.6 | 95 | 8/21/2026 |
| 2026.8.5 | 109 | 8/16/2026 |
| 2026.8.4 | 108 | 8/13/2026 |
| 2026.8.3 | 102 | 8/8/2026 |
| 2026.8.2 | 100 | 8/5/2026 |
| 2026.8.1 | 99 | 8/5/2026 |
| 2026.8.0 | 100 | 8/5/2026 |
| 2026.7.3 | 112 | 7/12/2026 |
| 2026.7.2 | 112 | 7/12/2026 |
| 2026.7.1 | 110 | 7/12/2026 |
| 2026.6.17 | 115 | 7/12/2026 |
| 2026.6.16 | 116 | 7/9/2026 |