Clip 0.1.0
See the version list below for details.
dotnet add package Clip --version 0.1.0
NuGet\Install-Package Clip -Version 0.1.0
<PackageReference Include="Clip" Version="0.1.0" />
<PackageVersion Include="Clip" Version="0.1.0" />
<PackageReference Include="Clip" />
paket add Clip --version 0.1.0
#r "nuget: Clip, 0.1.0"
#:package Clip@0.1.0
#addin nuget:?package=Clip&version=0.1.0
#tool nuget:?package=Clip&version=0.1.0
Clip
Fast structured logging for .NET 9 / C# 13 with no dependencies.
Why
Most C# loggers make you pick between a familiar API and low overhead. The familiar ones (Serilog, NLog, etc.) allocate 500–1500 bytes per call. Zero-alloc alternatives are verbose. Clip doesn't make you choose.
Clip also skips the printf-style template strings that most loggers use. Instead, messages are plain strings and structured data goes in typed fields alongside them. Messages stay greppable, fields stay separate.
The ergonomic interface takes anonymous objects and costs one small
allocation (~40 bytes). The zero-alloc interface takes Field values on the
stack and allocates nothing. Same output, separate pipelines — you pick which
one to use at each call site.
Speed
Clip formats directly into pooled UTF-8 byte buffers. No intermediate strings, no allocations on the hot path, no background threads hiding latency. Log calls are synchronous by default — when the call returns, the message has been written.
Filtered calls (below minimum level) are free — the level check is inlined, and BenchmarkDotNet can't even measure a cost.
The zero-alloc interface with five fields runs in ~55 ns with zero heap allocations. The ergonomic interface runs in ~76 ns with a single 40-byte Gen0 allocation. For context, Serilog takes ~850 ns and allocates ~970 bytes for the same work.
Async is opt-in via BackgroundSink.
Full results in docs/COMPARE.md. Run make bench (~40 minutes) to
generate comparison data and charts. make pdf renders PDFs.
Design
Fields, not templates. Messages are plain strings. Structured data goes in fields alongside the message, not interpolated into it. Messages stay greppable regardless of field values. Sinks get typed field data directly.
// Serilog — template parsed at runtime, field position matters
Log.Information("User {UserId} logged in from {IP}", userId, ip);
// Clip — message is a constant, fields are named and typed
logger.Info("User logged in", new { UserId = userId, IP = ip });
Zero dependencies. Clip depends only on the .NET 9 runtime. No transitive NuGet packages.
Sinks own everything. The logger is pure dispatch: check level, merge fields, call sinks. It does no formatting, holds no locks, performs no I/O. Each sink owns its own output pipeline. A failing sink doesn't affect other sinks or the caller.
Requirements
- .NET 9.0 or later
Quick Start
using Clip;
var logger = Logger.Create(c => c
.MinimumLevel(LogLevel.Debug)
.WriteTo.Console());
logger.Info("Server started", new { Port = 8080, Env = "production" });
2024-01-15 09:30:00.123 INFO Server started Env=production Port=8080
Interfaces
Ergonomic — pass anonymous objects. One Gen0 allocation per call (~40 bytes). Fields extracted via compiled expression trees, cached per type.
logger.Info("Request handled", new { Method = "GET", Path = "/api/users", Status = 200 });
Zero-alloc — pass Field values directly. Zero heap allocations.
logger.Info("Request handled",
new Field("Method", "GET"),
new Field("Path", "/api/users"),
new Field("Status", 200));
The compiler selects the interface via [OverloadResolutionPriority]. Through
ILogger, only the ergonomic interface is visible. On the concrete Logger
class, both are available.
See docs/USAGE.md for more examples and output samples.
Sinks
Console
ANSI-colored, human-readable output to stderr. Padded messages, sorted fields.
var logger = Logger.Create(c => c.WriteTo.Console());
2024-01-15 09:30:00.123 INFO Starting server host=localhost port=8080
2024-01-15 09:30:00.456 WARN High memory usage threshold=80 used=85
2024-01-15 09:30:01.789 ERRO Connection failed host=db.local
JSON
JSON Lines format via Utf8JsonWriter. Field values map directly to typed JSON
methods — no boxing, no intermediate strings.
var logger = Logger.Create(c => c.WriteTo.Json(stream));
{
"ts": "2024-01-15T09:30:00.123Z",
"level": "info",
"msg": "Starting server",
"fields": {
"host": "localhost",
"port": 8080
}
}
Multiple Sinks
var logger = Logger.Create(c => c
.MinimumLevel(LogLevel.Debug)
.WriteTo.Console()
.WriteTo.Json());
Each sink can have its own minimum level.
Background Sink
Wraps any sink with a bounded channel. The log call enqueues and returns immediately; a background task drains the queue.
var logger = Logger.Create(c => c
.WriteTo.Background(b => b.Json(), capacity: 4096));
On dispose, the channel is drained so no messages are lost.
Custom Sinks
Implement ILogSink:
public interface ILogSink : IDisposable
{
void Write(DateTimeOffset timestamp, LogLevel level, string message,
ReadOnlySpan<Field> fields, Exception? exception);
}
Register with .WriteTo.Sink(mySink). Note that ReadOnlySpan<Field> cannot be
captured — process or copy field data synchronously.
Enrichers
Add fields to every log entry automatically. Good for things like app name, hostname, or environment.
var logger = Logger.Create(c => c
.Enrich.Field("app", "my-service")
.Enrich.With(new MyEnricher())
.WriteTo.Console());
Enrichers can be level-gated — attach verbose data only to warnings and errors:
c.Enrich.With(new RequestBodyEnricher(), minLevel: LogLevel.Warning)
Enricher fields have the lowest priority — context and call-site fields override them on key collision.
Redactors
Scrub sensitive values before they reach any sink. Runs after all fields are merged.
var logger = Logger.Create(c => c
.Redact.Fields("password", "token")
.Redact.Pattern(@"\d{4}-\d{4}-\d{4}-(\d{4})", "****-****-****-$1")
.WriteTo.Console());
Context Scopes
Attach fields to all log calls within a scope. Async-safe via AsyncLocal.
using (logger.AddContext(new { RequestId = "abc-123", UserId = 42 }))
{
logger.Info("Processing"); // includes RequestId + UserId
logger.Info("Done"); // same context
}
logger.Info("Outside"); // context gone
Scopes nest. Inner fields override outer fields with the same key.
Log Levels
Trace · Debug · Info · Warning · Error · Fatal
logger.Trace("detailed diagnostics");
logger.Debug("internal state", new { Queue = 12 });
logger.Info("normal operation");
logger.Warning("something unusual", new { Retries = 3 });
logger.Error("operation failed", exception, new { Code = 500 });
logger.Fatal("unrecoverable");
Filtered calls (below minimum level) are effectively free — the level check is inlined and the rest of the method is never entered.
MEL Adapter
Drop Clip into an existing Microsoft.Extensions.Logging setup via the
Clip.Extensions.Logging package:
builder.Logging.AddClip(options => {
options.MinimumLevel = LogLevel.Debug;
});
Or pass an existing Logger instance:
builder.Logging.AddClip(myLogger);
Analyzers
Clip ships with Roslyn analyzers that catch common mistakes at compile time. Install alongside Clip:
dotnet add package Clip.Analyzers
| ID | Severity | Description | Code Fix |
|---|---|---|---|
| CLIP001 | Error | Invalid fields argument — primitives, strings, arrays not accepted | Wrap in anonymous type |
| CLIP002 | Warning | Message contains {Placeholder} template syntax |
Move to fields |
| CLIP003 | Warning | AddContext return value not disposed |
Add using |
| CLIP004 | Info | Exception not passed to Error in catch block |
Add exception parameter |
| CLIP005 | Warning | Unreachable code after Fatal |
— |
| CLIP006 | Warning | Interpolated string in log message | Extract to fields |
| CLIP007 | Info | Exception wrapped in fields anonymous type | Use Error overload |
| CLIP008 | Info | Empty or whitespace log message | — |
| CLIP009 | Info | Log message starts with lowercase | Capitalize |
Build & Test
make help # show all targets
make check # build + test
make bench # full benchmark suite (slow)
make docs # generate charts + COMPARE.md from existing artifacts
make demo # run demo app
License
| 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
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Clip:
| Package | Downloads |
|---|---|
|
Clip.Extensions.Logging
Microsoft.Extensions.Logging provider for the Clip structured logging library. Drop Clip into any ILoggingBuilder pipeline. |
|
|
Clip.OpenTelemetry
OpenTelemetry Protocol (OTLP) sink for the Clip structured logging library. Exports logs via gRPC or HTTP/protobuf. |
GitHub repositories
This package is not used by any popular GitHub repositories.