Clip 0.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Clip --version 0.1.0
                    
NuGet\Install-Package Clip -Version 0.1.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="Clip" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Clip" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Clip" />
                    
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 Clip --version 0.1.0
                    
#r "nuget: Clip, 0.1.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 Clip@0.1.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=Clip&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Clip&version=0.1.0
                    
Install as a Cake Tool

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

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

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.
  • 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.

Version Downloads Last Updated
0.4.0 130 3/25/2026
0.3.0 117 3/24/2026
0.2.0 124 3/24/2026
0.1.0 104 3/22/2026