CosmoLogs 0.1.0

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

CosmoLogs

ci

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.Logging apps via the CosmoLogs.Extensions.Logging adapter.
  • Familiar Serilog ergonomics: LogContext (AsyncLocal scope), enrichers, namespace level overrides, CLEF (Compact JSON) for Seq/Datadog/Splunk, IConfiguration binding.
  • 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:

  1. Build & test the whole solution at the tagged version.
  2. dotnet pack all four library projects (with symbol packages and SourceLink).
  3. Push them to nuget.org using the NUGET_API_KEY repo secret.
  4. Create a GitHub release with the .nupkg files 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.1.2 1,270 5/24/2026
1.1.1 155 5/23/2026
1.1.0 143 5/23/2026
1.0.1 136 5/21/2026
1.0.0 184 5/8/2026
0.1.2-dev 141 5/8/2026
0.1.1 140 5/8/2026
0.1.0 132 5/8/2026