CosmoLogs 1.1.2
dotnet add package CosmoLogs --version 1.1.2
NuGet\Install-Package CosmoLogs -Version 1.1.2
<PackageReference Include="CosmoLogs" Version="1.1.2" />
<PackageVersion Include="CosmoLogs" Version="1.1.2" />
<PackageReference Include="CosmoLogs" />
paket add CosmoLogs --version 1.1.2
#r "nuget: CosmoLogs, 1.1.2"
#:package CosmoLogs@1.1.2
#addin nuget:?package=CosmoLogs&version=1.1.2
#tool nuget:?package=CosmoLogs&version=1.1.2
CosmoLogs
Fast, structured, leveled logging for .NET 10 — a C# port of Uber's Zap, layered on System.IO.Pipelines.
Status: alpha. Builds clean, 197/197 tests passing, benchmarks honest. API is stable enough to use; expect occasional breaking changes until 1.0.
Why CosmoLogs?
- Zero-allocation hot path.
logger.Info("msg", String("k", "v"))allocates 0 B/call on the typical structured-logging path. Verified by BenchmarkDotNet. - ~730× less allocation than Serilog in concurrent workloads (3 KB vs 2.15 MB across 32 threads × 100 logs).
- Drop-in for ASP.NET Core /
Microsoft.Extensions.Loggingapps via theCosmoLogs.Extensions.Loggingadapter. - Familiar Serilog ergonomics:
LogContext(AsyncLocal scope), enrichers, namespace level overrides, CLEF (Compact JSON) for Seq/Datadog/Splunk,IConfigurationbinding. - Real Seq integration out of the box — HTTP-batching sink with retry/backoff and configurable batch boundaries.
Solution layout
src/
├── CosmoLogs/ core library
├── CosmoLogs.Extensions.Logging/ Microsoft.Extensions.Logging adapter
├── CosmoLogs.Extensions.Configuration/ IConfiguration binding (appsettings.json)
└── CosmoLogs.Sinks.Seq/ Datalust Seq HTTP sink (CLEF)
tests/
├── CosmoLogs.Tests/ 178 unit tests (xUnit)
├── CosmoLogs.Extensions.Logging.Tests/ 9 MEL bridge tests
└── CosmoLogs.Sinks.Seq.Tests/ 10 Seq integration tests
benchmarks/
└── CosmoLogs.Benchmarks/ 38 BenchmarkDotNet pairs vs Serilog
Build / test / bench
# Build everything
dotnet build CosmoLogs.sln
# Run the full test suite (197 tests)
dotnet test CosmoLogs.sln
# Run benchmarks (uses BenchmarkDotNet — Release mode required)
dotnet run -c Release --project benchmarks/CosmoLogs.Benchmarks -- --filter '*'
# Single category
dotnet run -c Release --project benchmarks/CosmoLogs.Benchmarks -- --filter '*ThreeFields*'
Usage
Quick start
using CosmoLogs;
using static CosmoLogs.Fields;
// Production preset: JSON to stderr, InfoLevel and above, with sampling.
var logger = Loggers.NewProduction();
logger.Info("user logged in",
String("userId", "alice"),
Int("attempt", 3),
Duration("elapsed", TimeSpan.FromMilliseconds(42)));
logger.Sync(); // flush before exit
Output (Compact JSON, one line per entry):
{"level":"info","ts":1712345678.123,"msg":"user logged in","userId":"alice","attempt":3,"elapsed":0.042}
Development mode (human-readable console)
var logger = Loggers.NewDevelopment();
logger.Info("starting up", String("port", "8080"));
2026-05-08T01:51:11.009+03:00 INFO Program.cs:7 starting up {"port": "8080"}
Structured fields (the typed API)
The fast path. Each constructor returns a Field struct — no boxing, no allocation:
logger.Info("hi",
String("k1", "v"), // string
Int("count", 42), // long
Bool("retry", true), // bool
Float64("ratio", 0.95), // double
Duration("elapsed", TimeSpan.FromSeconds(1)),
Time("at", DateTimeOffset.UtcNow),
Binary("blob", new byte[] { 1, 2 }),
Error(ex), // Exception → "error"+"errorVerbose" pair
Stack("trace")); // captures the current stacktrace
For more than 4 fields, use *Many overloads with a ReadOnlySpan<Field>:
Field[] fs = [String("a","1"), String("b","2"), String("c","3"), String("d","4"), String("e","5")];
logger.InfoMany("five fields", fs);
Loose key/value (Sugar API)
When you don't want to write String(...) everywhere:
var sugar = logger.Sugar();
sugar.Infow("user logged in",
"userId", "alice",
"attempt", 3,
"err", ex); // Exception detected, becomes "error" field
sugar.Infof("processed {0} records in {1}", count, elapsed); // .NET-style format
sugar.Info("just", "a", "concatenated", "message"); // print-style
Child loggers
var requestLogger = logger
.Named("http.api")
.With(String("requestId", "r-42"), String("user", "alice"));
requestLogger.Info("processing"); // includes requestId + user
requestLogger.Warn("retry", Int("attempt", 2)); // also includes them
Ambient context (LogContext)
For properties that flow through async/await without manually threading a logger:
using (LogContext.PushProperty("requestId", Guid.NewGuid()))
using (LogContext.PushProperty("userId", "alice"))
{
await ProcessAsync(); // every log inside picks up requestId + userId
}
// In ProcessAsync:
async Task ProcessAsync()
{
logger.Info("step 1"); // includes requestId, userId
await logger.Info("step 2 (after await)");
}
To wire up LogContext properties, add the enricher:
var logger = Loggers.NewProduction(
Options.Enrich(Enrichers.FromLogContext, Enrichers.WithThreadId));
Microsoft.Extensions.Logging integration
// Program.cs
using CosmoLogs;
using CosmoLogs.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args);
var cosmo = Loggers.NewProduction(
Options.AddCaller(),
Options.Enrich(Enrichers.FromLogContext));
builder.Logging.ClearProviders();
builder.Logging.AddCosmoLogs(cosmo);
var app = builder.Build();
app.MapGet("/", (ILogger<Program> log) => {
log.LogInformation("hello {Name}", "world");
return "ok";
});
app.Run();
Configure from appsettings.json
{
"CosmoLogs": {
"Level": "Info",
"Encoding": "json",
"OutputPaths": ["stdout"],
"ErrorOutputPaths": ["stderr"],
"InitialFields": { "service": "api", "env": "prod" },
"Sampling": { "Initial": 100, "Thereafter": 100 },
"MinimumLevelOverrides": {
"Microsoft": "Warn",
"Microsoft.AspNetCore.Hosting": "Warning",
"System.Net.Http": "Warning"
}
}
}
using CosmoLogs.Extensions.Configuration;
var cfg = builder.Configuration;
var logCfg = CosmoLogsConfiguration.ReadFrom(cfg);
var nsLevels = CosmoLogsConfiguration.BuildNamespaceLevels(cfg);
var logger = logCfg.Build(
nsLevels is null ? Options.AddCaller() : Options.WithNamespaceLevels(nsLevels));
builder.Logging.AddCosmoLogs(logger);
Send to Seq
using CosmoLogs.Sinks.Seq;
var logger = SeqExtensions.ForSeq(
serverUrl: "http://localhost:5341",
apiKey: "MY-API-KEY",
minLevel: Level.Info,
options: Options.Enrich(Enrichers.WithMachineName));
The sink batches CLEF events, retries on 5xx with exponential backoff, drops the batch on 4xx, and exposes OnDropped for permanent-failure callbacks.
To use the URL-scheme registry (so Seq can be configured from appsettings.json):
SeqExtensions.RegisterSchemes(); // call once at startup
{
"CosmoLogs": {
"Encoding": "clef",
"OutputPaths": ["seq+http://localhost:5341/?apiKey=MY-KEY&batchSize=500"]
}
}
Rolling file sink
using CosmoLogs.Core;
using CosmoLogs.Sinks;
var sink = new RollingFileSink
{
PathTemplate = "logs/app-{Date}.log",
Interval = RollingInterval.Day,
MaxFileSizeBytes = 50 * 1024 * 1024, // 50 MB before size-rollover
RetainedFileCountLimit = 30,
};
var enc = JsonEncoder.New(Config.NewProductionEncoderConfig());
var logger = Logger.New(CoreFactory.NewCore(enc, sink, Level.Info));
Sampling
Cap log volume on hot paths — log the first N entries for a given level+message in each tick, then 1 in M after that:
var inner = CoreFactory.NewCore(enc, sink, Level.Info);
var sampled = Sampler.NewSamplerWithOptions(inner,
tick: TimeSpan.FromSeconds(1),
first: 100,
thereafter: 100,
new SamplerOptions
{
Hook = (entry, decision) =>
{ if ((decision & SamplingDecision.Dropped) != 0) Metrics.LogsDropped++; }
});
var logger = Logger.New(sampled);
Async (non-blocking) sink
For high-throughput workloads where the underlying sink (file, network, Seq) can't keep up:
var cfg = Config.NewProductionConfig();
cfg.AsyncSink = true; // wrap output in AsyncWriteSyncer
cfg.AsyncSinkCapacity = 50_000; // bounded queue; oldest drops on overflow
var logger = cfg.Build();
Producer threads do not block — writes go to a Channel<T> and a single drainer task hits the inner sink. Trade-off: events queued at process crash are lost.
Runtime level changes (HTTP endpoint)
var atomicLevel = AtomicLevel.At(Level.Info);
var logger = Logger.New(CoreFactory.NewCore(enc, sink, atomicLevel));
// Wire into ASP.NET Core (or any HTTP framework):
app.MapGet ("/log/level", () => LevelHttpHandler.Handle(atomicLevel, "GET"));
app.MapPut ("/log/level", async (HttpRequest req) => {
var resp = LevelHttpHandler.Handle(
atomicLevel,
method: "PUT",
contentType: req.ContentType,
body: req.Body,
queryLevel: req.Query["level"]);
return Results.Bytes(resp.Body, resp.ContentType);
});
// curl http://localhost:8080/log/level → {"level":"info"}
// curl -X PUT http://localhost:8080/log/level?level=debug → {"level":"debug"}
Test helpers (Observer)
For unit tests that want to assert on log entries without hitting disk:
using CosmoLogs.Test;
var (core, logs) = Observer.New(Level.Debug);
var logger = Logger.New(core);
logger.Info("hi", String("user", "alice"));
var entry = logs.All().Single();
Assert.Equal(Level.Info, entry.Entry.Level);
Assert.Equal("hi", entry.Entry.Message);
Assert.Equal("alice", entry.ContextMap()["user"]);
ObservedLogs exposes FilterLevelExact, FilterMessage, FilterMessageSnippet, FilterField, FilterFieldKey, FilterLoggerName, AllUntimed — same shape as zaptest.observer.
Performance
dotnet run -c Release --project benchmarks/CosmoLogs.Benchmarks -- --filter '*ThreeFields*'
| Method | Mean | Allocated | Ratio |
|---|---|---|---|
Cosmo_ThreeFields |
360 ns | 0 B | 1.00 |
Serilog_ThreeFields |
391 ns | 976 B | 1.08 |
See benchmarks/README.md for the full picture (38 scenarios across 8 categories, including MEL bridge, sampler reject path, enricher pipeline, ambient scope, CLEF head-to-head, console encoder, and 32-thread concurrent throughput).
Architecture
The pipeline matches Zap's:
logger.Info(msg, fields)
↓
Logger.CheckCore(level, msg)
↓ — early-out if level < threshold
ICore.Check(entry, ce) ← wrappers chain here (sampler, enricher, namespace levels, hooks)
↓
CheckedEntry.Write(fields) ← dispatches to all registered cores; returns to pool
↓
IOCore.Write(ent, fields)
↓
IEncoder.EncodeEntry(ent, fields) ← JsonEncoder / ConsoleEncoder / CompactJsonEncoder
↓ produces a pooled Buffer
IWriteSyncer.Write(buffer) ← Stream / PipeWriter / Async / Buffered / Multi / Discard
Every box is replaceable: implement ICore, IEncoder, or IWriteSyncer and plug it in.
Releasing
Tag a commit with v* and push. The .github/workflows/release.yml workflow will:
- Build & test the whole solution at the tagged version.
dotnet packall four library projects (with symbol packages and SourceLink).- Push them to nuget.org using the
NUGET_API_KEYrepo secret. - Create a GitHub release with the
.nupkgfiles attached and auto-generated notes.
# release 0.1.0
git tag v0.1.0
git push origin v0.1.0
Manual run (without a tag) is also supported via the Actions → release → Run workflow UI; type the version into the input field.
The NUGET_API_KEY secret needs to be set once in
Settings → Secrets and variables → Actions with a key scoped to push these four package IDs.
License
MIT — see LICENSE. Original Zap copyright © Uber Technologies preserved.
| 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
- Cosmo.Transport (>= 1.0.2)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on CosmoLogs:
| Package | Downloads |
|---|---|
|
CosmoLogs.Extensions.Logging
Microsoft.Extensions.Logging adapter for CosmoLogs. Plug a CosmoLogs Logger into any ILogger-using app. |
|
|
CosmoMail
Lightweight .NET SMTP and IMAP client library with MIME generation, templating, attachments, inline images, and STARTTLS support. |
|
|
CosmoLogs.Sinks.Seq
Seq (datalust.co) sink for CosmoLogs. HTTP-batching, CLEF-formatted ingestion with retry/backoff. |
|
|
CosmoLogs.Extensions.Configuration
IConfiguration binding for CosmoLogs — read your logger from appsettings.json. |
GitHub repositories
This package is not used by any popular GitHub repositories.