-
Notifications
You must be signed in to change notification settings - Fork 0
Performance and Internals
How the logger works under the hood, and the design choices behind it. None of this is required reading to use the library — it's here for the curious and for anyone evaluating it for a hot path.
Every public method funnels into one shared internal method, WriteCore. The sequence:
-
Snapshot options. The current
LoggerOptionsreference is read once into a local, so a concurrentConfigurecan't tear the read mid-entry. -
Level check first. If the entry is below
MinimumLevel, it returns immediately — before any formatting, allocation, or string work. Suppressed logs are nearly free. -
Render the template (
Logonly) into the final message and collect(name, value)pairs for the structured fields. -
Resolve source fields. For
Log<T>,class=typeof(T).Nameandmethod= the[CallerMemberName]value. ForLog, both arenull. -
Build the output — either JSON (
Utf8JsonWriterover a pooled-ish byte buffer) or text (StringBuilder). -
Write atomically. A single
lockguards theConsole.Out.WriteLineso concurrent threads never interleave their lines.
Log<T> gets class/method context without inspecting the call stack:
-
Class comes from
typeof(T).Name, resolved once per closed generic type and cached in astatic readonlyfield.Log<OrderService>computes"OrderService"exactly once for the lifetime of the process. -
Method comes from
[CallerMemberName], which the compiler substitutes at the call site as a constant string. No runtime lookup.
This is why performance doesn't degrade as the call stack deepens — there's no StackTrace to
walk. (A stack-walk approach, by contrast, gets slower the deeper you are and needs PDBs for
detail.)
Line numbers were intentionally removed. Capturing them requires a StackTrace and PDB
reads on every call — too costly for a hot path, and only as accurate as the symbols you
actually deployed. Class and method give you the location you usually need, for free.
- Suppressed entries: essentially zero work after the level check.
-
Text mode: a
StringBuilder(pre-sized to 160 chars) per entry. -
JSON mode: an
ArrayBufferWriter<byte>(pre-sized to 256 bytes) and aUtf8JsonWriterper entry, then one UTF-8 → string decode. -
Template parsing: allocates a
Listfor the(name, value)pairs only when the template actually contains placeholders and you passed args; otherwise it short-circuits and returns the template unchanged. -
Log<T>category: allocated once per type, never per call. Empty args are a sharedstatic readonlyarray.
These are deliberately modest, single-entry allocations — fine for typical logging volumes. For extreme throughput you'd batch or use a binary sink, which is out of scope for a zero-dependency console logger.
-
Writing is serialized by a
lock, so each entry is atomic — lines from concurrent threads never interleave. -
Configureswaps the options reference under the same lock; in-flight writes use the snapshot they already took. -
Trace id is an
AsyncLocal<string?>, so it's isolated per async flow — concurrent requests don't see each other's ids. See Trace Correlation.
In JSON mode, the logger writes a fixed set of built-in fields:
timestamp · level · icon · traceId · class · method · message · exception
When emitting your template values as fields, any whose name collides with one of these is
skipped, so user data can't overwrite a built-in field (e.g. a placeholder named level
can't clobber the real log level — which matters because most log tools do last-wins on
duplicate keys). The collision check is a small linear scan over the 8-item list; with the
handful of placeholders a typical entry has, the cost is negligible. The skipped value is still
present in the rendered message — it just doesn't get its own field. See
Message Templates.
The logger writes to Console.Out (standard output). In containers, Lambda, and most cloud
hosts, stdout is captured and shipped to your log system automatically, which is why a simple
Console.Out sink is enough. JSON mode is designed to be one object per line for exactly this
reason.
The JSON writer uses JavaScriptEncoder.UnsafeRelaxedJsonEscaping, so <, >, &, and emoji
are written literally instead of as \uXXXX. This keeps console output human-readable and is
safe for stdout/log files — but is not meant for injecting directly into HTML.
Thushar.Logging · a zero-dependency static logger for .NET
📦 NuGet · 💻 GitHub · 🐛 Issues · 📸 Instagram
dotnet add package Thushar.Logging
Getting around
Features
Using it
Under the hood