{"id":48544050,"url":"https://github.com/localhots/clip","last_synced_at":"2026-04-08T06:02:02.577Z","repository":{"id":346242768,"uuid":"1185753974","full_name":"localhots/clip","owner":"localhots","description":"Fast structured logger, brother of blip.","archived":false,"fork":false,"pushed_at":"2026-03-23T00:27:11.000Z","size":141,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-23T18:33:47.857Z","etag":null,"topics":["csharp13","dotnet9","logging","zero-alloc"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/localhots.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-18T23:02:31.000Z","updated_at":"2026-03-23T00:27:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/localhots/clip","commit_stats":null,"previous_names":["localhots/clip"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/localhots/clip","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localhots%2Fclip","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localhots%2Fclip/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localhots%2Fclip/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localhots%2Fclip/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/localhots","download_url":"https://codeload.github.com/localhots/clip/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localhots%2Fclip/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31542384,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T16:28:08.000Z","status":"online","status_checked_at":"2026-04-08T02:00:06.127Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp13","dotnet9","logging","zero-alloc"],"created_at":"2026-04-08T06:02:02.093Z","updated_at":"2026-04-08T06:02:02.565Z","avatar_url":"https://github.com/localhots.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Clip\n\nFast structured logging for .NET 9 / C# 13. Very fast. Very opinionated.\n\n## Why\n\nMost C# loggers make you pick between a convenient API and low overhead. The\nfamiliar ones (Serilog, NLog, etc.) allocate 500–1500 bytes per call. Zero-alloc\nalternatives are verbose.\n\nClip also skips the printf-style template strings that most loggers use.\nInstead, messages are plain strings and structured data goes in typed fields\nalongside them.\n\nThe **ergonomic interface** (`Clip.ILogger`) takes anonymous objects and costs\none small allocation (~40 bytes). The **zero-alloc interface**\n(`Clip.IZeroLogger`) takes `Field` values on the stack and allocates nothing.\nSame output, separate pipelines.\n\n## Speed\n\nClip formats directly into pooled UTF-8 byte buffers. No intermediate strings,\nno allocations on the hot path, no background threads hiding latency. Log calls\nare synchronous by default, `BackgroundSink` can be used for async.\n\nFiltered calls (below minimum level) are free — the level check is inlined and\nnothing else runs. MEL's source-generated `[LoggerMessage]` variant gets\nfiltered calls down to ~0.6 ns, somehow still measurably slower than Clip.\nThat's the one scenario where source generation significantly outperforms\nstandard MEL.\n\nThe zero-alloc interface with five fields runs in ~138 ns with zero heap\nallocations. The ergonomic interface runs in ~176 ns with a 72-byte Gen0\nallocation. For context, MEL takes ~807 ns and allocates ~808 bytes for the\nsame work.\n\nFull results in [docs/COMPARE.md](docs/COMPARE.md). To run the benchmarks:\n\n- `make bench` takes ~40 minutes\n- `make pdf` produces nice PDFs with charts and notes\n\n## Design\n\n**Fields, not templates.** Messages are plain strings. Structured data goes in\nfields alongside the message, not interpolated into it. Sinks get typed field\ndata directly.\n\n```csharp\n// Serilog — template parsed at runtime, field position matters\nLog.Information(\"User {UserId} logged in from {IP}\", userId, ip);\n\n// Clip — message is a constant, fields are named and typed\nlogger.Info(\"User logged in\", new { UserId = userId, IP = ip });\n```\n\n**Zero dependencies.** Clip depends only on the .NET 9 runtime. No transitive\nNuGet packages.\n\n**Sinks own everything.** The logger is pure dispatch: check level, merge\nfields, call sinks. It does no formatting, holds no locks, performs no I/O. Each\nsink owns its own output pipeline. Failing sinks don't affect the others or the\ncaller.\n\n## Requirements\n\n- [.NET 9.0](https://dotnet.microsoft.com/download/dotnet/9.0) or later\n\n## Install\n\n```bash\ndotnet add package Clip\n```\n\nPackages are published to [NuGet.org](https://www.nuget.org/packages/Clip) and\n[GitHub Packages](https://github.com/localhots?tab=packages\u0026repo_name=clip).\n\n## Quick Start\n\n```csharp\nusing Clip;\n\nvar logger = Logger.Create(c =\u003e c\n    .MinimumLevel(LogLevel.Debug)\n    .WriteTo.Console());\n\nlogger.Info(\"Server started\", new { Port = 8080, Env = \"production\" });\n```\n\n```\n2024-01-15 09:30:00.123 INFO Server started                           Env=production Port=8080\n```\n\n## Interfaces\n\n**Ergonomic** — pass anonymous objects. One Gen0 allocation per call (~40 bytes).\nFields extracted via compiled expression trees, cached per type.\n\n```csharp\nlogger.Info(\"Request handled\", new { Method = \"GET\", Path = \"/api/users\", Status = 200 });\n```\n\n**Zero-alloc** — pass `Field` values directly. Zero heap allocations.\n\n```csharp\nlogger.Info(\"Request handled\",\n    new Field(\"Method\", \"GET\"),\n    new Field(\"Path\", \"/api/users\"),\n    new Field(\"Status\", 200));\n```\n\nThe compiler selects the interface via `[OverloadResolutionPriority]`. Through\n`ILogger`, only the ergonomic interface is visible. On the concrete `Logger`\nclass, both are available.\n\nSee [docs/USAGE.md](docs/USAGE.md) for more examples and output samples.\n\n## Sinks\n\n### Console\n\nANSI-colored, human-readable output to stderr. Padded messages, sorted fields.\n\n```csharp\nvar logger = Logger.Create(c =\u003e c.WriteTo.Console());\n```\n\n```\n2024-01-15 09:30:00.123 INFO Starting server                          host=localhost port=8080\n2024-01-15 09:30:00.456 WARN High memory usage                        threshold=80 used=85\n2024-01-15 09:30:01.789 ERRO Connection failed                        host=db.local\n```\n\n### JSON\n\nJSON Lines format via `Utf8JsonWriter`. Field values map directly to typed JSON\nmethods — no boxing, no intermediate strings.\n\n```csharp\nvar logger = Logger.Create(c =\u003e c.WriteTo.Json(stream));\n```\n\n```json\n{\n  \"ts\": \"2024-01-15T09:30:00.123Z\",\n  \"level\": \"info\",\n  \"msg\": \"Starting server\",\n  \"fields\": {\n    \"host\": \"localhost\",\n    \"port\": 8080\n  }\n}\n```\n\n### Multiple Sinks\n\n```csharp\nvar logger = Logger.Create(c =\u003e c\n    .MinimumLevel(LogLevel.Debug)\n    .WriteTo.Console()\n    .WriteTo.Json());\n```\n\nEach sink can have its own minimum level.\n\n### Background Sink\n\nWraps any sink with a bounded channel. The log call enqueues and returns\nimmediately; a background task drains the queue.\n\n```csharp\nvar logger = Logger.Create(c =\u003e c\n    .WriteTo.Background(b =\u003e b.Json(), capacity: 4096));\n```\n\nOn dispose, the channel is drained so no messages are lost.\n\n### OpenTelemetry (OTLP)\n\nExport structured logs to any OpenTelemetry-compatible backend (Jaeger, Grafana,\nDatadog, etc.) via gRPC or HTTP/protobuf. Separate package with minimal\ndependencies — no OpenTelemetry SDK required.\n\n```bash\ndotnet add package Clip.OpenTelemetry\n```\n\n```csharp\nusing Clip.OpenTelemetry;\n\nvar logger = Logger.Create(c =\u003e c\n    .WriteTo.Otlp(opts =\u003e {\n        opts.Endpoint = \"http://collector:4317\";\n        opts.ServiceName = \"my-service\";\n    }));\n```\n\nLogs are batched internally and exported on a background thread. Clip fields map\ndirectly to OTLP attributes, log levels map to OTLP severity numbers. Supports\n`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL`,\n`OTEL_EXPORTER_OTLP_HEADERS`, and `OTEL_SERVICE_NAME` environment variables.\n\nTo use HTTP/protobuf instead of gRPC:\n\n```csharp\nopts.Protocol = OtlpProtocol.HttpProtobuf;\nopts.Endpoint = \"http://collector:4318\";\n```\n\n### Custom Sinks\n\nImplement `ILogSink`:\n\n```csharp\npublic interface ILogSink : IDisposable\n{\n    void Write(DateTimeOffset timestamp, LogLevel level, string message,\n               ReadOnlySpan\u003cField\u003e fields, Exception? exception);\n}\n```\n\nRegister with `.WriteTo.Sink(mySink)`. Note that `ReadOnlySpan\u003cField\u003e` cannot be\ncaptured — process or copy field data synchronously.\n\n## Enrichers\n\nAdd fields to every log entry automatically. Good for things like app name,\nhostname, or environment.\n\n```csharp\nvar logger = Logger.Create(c =\u003e c\n    .Enrich.Field(\"app\", \"my-service\")\n    .Enrich.With(new MyEnricher())\n    .WriteTo.Console());\n```\n\nEnrichers can be level-gated — attach verbose data only to warnings and errors:\n\n```csharp\nc.Enrich.With(new RequestBodyEnricher(), minLevel: LogLevel.Warning)\n```\n\nEnricher fields have the lowest priority — context and call-site fields override\nthem on key collision.\n\n## Filters\n\nExclude fields entirely — filtered fields never reach redactors or sinks.\n\n```csharp\nvar logger = Logger.Create(c =\u003e c\n    .Filter.Fields(\"_internal\", \"debug_trace\")\n    .Filter.Pattern(@\"^temp_\")\n    .WriteTo.Console());\n```\n\nCustom filters implement `ILogFilter`:\n\n```csharp\npublic class PrefixFilter(string prefix) : ILogFilter\n{\n    public bool ShouldSkip(string key) =\u003e key.StartsWith(prefix);\n}\n\n// Register: .Filter.With(new PrefixFilter(\"_\"))\n```\n\n## Redactors\n\nScrub sensitive values before they reach any sink. Runs after all fields are\nmerged. Unlike filters (which remove fields), redactors replace values — the\nfield key remains visible.\n\n```csharp\nvar logger = Logger.Create(c =\u003e c\n    .Redact.Fields(\"password\", \"token\")\n    .Redact.Pattern(@\"\\d{4}-\\d{4}-\\d{4}-(\\d{4})\", \"****-****-****-$1\")\n    .WriteTo.Console());\n```\n\n## Context Scopes\n\nAttach fields to all log calls within a scope. Async-safe via `AsyncLocal`.\n\n```csharp\nusing (logger.AddContext(new { RequestId = \"abc-123\", UserId = 42 }))\n{\n    logger.Info(\"Processing\");      // includes RequestId + UserId\n    logger.Info(\"Done\");            // same context\n}\nlogger.Info(\"Outside\");             // context gone\n```\n\nScopes nest. Inner fields override outer fields with the same key.\n\n## Log Levels\n\n`Trace` · `Debug` · `Info` · `Warning` · `Error` · `Fatal`\n\n```csharp\nlogger.Trace(\"detailed diagnostics\");\nlogger.Debug(\"internal state\", new { Queue = 12 });\nlogger.Info(\"normal operation\");\nlogger.Warning(\"something unusual\", new { Retries = 3 });\nlogger.Error(\"operation failed\", exception, new { Code = 500 });\nlogger.Fatal(\"unrecoverable\");\n```\n\nFiltered calls (below minimum level) are effectively free — the level check is\ninlined and the rest of the method is never entered.\n\n## MEL Adapter\n\nDrop Clip into an existing `Microsoft.Extensions.Logging` setup via the\n`Clip.Extensions.Logging` package:\n\n```csharp\nbuilder.Logging.AddClip(options =\u003e {\n    options.MinimumLevel = LogLevel.Debug;\n});\n```\n\nOr pass an existing `Logger` instance:\n\n```csharp\nbuilder.Logging.AddClip(myLogger);\n```\n\n## Analyzers\n\nClip ships with Roslyn analyzers that catch common mistakes at compile time.\nInstall alongside Clip:\n\n```bash\ndotnet add package Clip.Analyzers\n```\n\n| ID      | Severity | Description                                                        | Code Fix                |\n|---------|----------|--------------------------------------------------------------------|-------------------------|\n| CLIP001 | Error    | Invalid fields argument — primitives, strings, arrays not accepted | Wrap in anonymous type  |\n| CLIP002 | Warning  | Message contains `{Placeholder}` template syntax                   | Move to fields          |\n| CLIP003 | Warning  | `AddContext` return value not disposed                             | Add `using`             |\n| CLIP004 | Info     | Exception not passed to `Error` in catch block                     | Add exception parameter |\n| CLIP005 | Warning  | Unreachable code after `Fatal`                                     | —                       |\n| CLIP006 | Warning  | Interpolated string in log message                                 | Extract to fields       |\n| CLIP007 | Info     | Exception wrapped in fields anonymous type                         | Use `Error` overload    |\n| CLIP008 | Info     | Empty or whitespace log message                                    | —                       |\n| CLIP009 | Info     | Log message starts with lowercase                                  | Capitalize              |\n\n## Build \u0026 Test\n\n```bash\nmake help          # show all targets\nmake check         # build + test\nmake bench         # run benchmark suite (~40 minutes)\nmake pdf           # generate charts + PDFs\nmake demo          # run demo app\n```\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flocalhots%2Fclip","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flocalhots%2Fclip","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flocalhots%2Fclip/lists"}