-
Notifications
You must be signed in to change notification settings - Fork 0
FAQ
Common questions and gotchas.
- Why are there two loggers,
LogandLog<T>? - Why isn't my
{placeholder}substituted when I useLog<T>? - Can I get the method name without using
Log<T>? - Why are there no line numbers?
- Why are the methods named
Informationand notLogInformation? - Why don't I need a
usingstatement in some files? - Do
UseClassName/UseMethodNameaffect the non-genericLog? - What is the reserved-keys list for?
- Is the logger thread-safe?
- Where does the output go? Can I log to a file?
- Does it run on .NET 8, 9, and 10?
- Is a static logger a good fit for my library?
Log gives you message templates (structured {placeholder} fields). Log<T> gives you
the class and method an entry came from. They can't be merged into one call because of a C#
language constraint — see the next two answers and the full write-up in
Log vs Log<T>.
Because Log<T> messages are plain strings — it does not do template substitution. The
trailing parameter slot that Log uses for template values is used by Log<T> for the
[CallerMemberName] method name instead. So:
Log<OrderService>.Information("User {id} in", 42);
// message is logged literally as "User {id} in"Need template fields? Use Log. Need class/method? Use Log<T>. Details:
Log vs Log<T>.
Not cleanly through the non-generic Log, and that's by design. Two mechanisms exist:
-
[CallerMemberName](whatLog<T>uses) — free and reliable, but it needs a trailing optional parameter, which can't coexist with theparams object?[] argsthatLoguses for templates. SoLogcan't also capture the method. -
Stack walk (
new StackTrace()…) — works at runtime without PDBs for names, but it's the costly, inlining-fragile approach the library deliberately avoids.
So for method/class context, use Log<T> — it's the free path. See
Log vs Log<T>.
Capturing a line number requires a StackTrace and PDB symbol reads on every call —
too expensive for a logging hot path, and only as accurate as the symbols you actually
deployed. Class and method (which Log<T> provides for free) give you the location you usually
need without that cost. See
Performance & Internals.
Because the class is already named Log, so the call reads as a natural verb-object phrase:
Log.Information(...) ("log information"). Naming it LogInformation would stutter —
Log.LogInformation(...).
Microsoft.Extensions.Logging uses LogInformation because those are extension methods on an
ILogger instance (typically _logger.LogInformation(...)), where the receiver is a noun and
needs the verb baked in. Here the receiver Log is the verb. This matches Serilog, which also
uses Log.Information(...).
If your code's namespace is a child of Thushar.Logging — for example
Thushar.Logging.Samples.Lambda — the logger's types are already in scope through the namespace
hierarchy, because C# resolves names by walking outward through enclosing namespaces:
Thushar.Logging.Samples.Lambda → Thushar.Logging.Samples → Thushar.Logging → (global)
Thushar.Logging is one of those enclosing scopes, so Log, Log<T>, etc. resolve without a
using. If your namespace is not under Thushar.Logging (e.g. MyApp.Services), you'll
need using Thushar.Logging;. It is not coming from implicit/global usings — those only add
System.*.
No. They control the class / method fields, which only Log<T> produces. The non-generic
Log never captures a class or method, so these toggles are no-ops for it. They work as
expected for Log<T>:
Log.Configure(o => o.UseMethodName(false));
Log<OrderService>.Information("started"); // [OrderService] started (method suppressed)
Log.Information("started"); // started (toggle irrelevant)It stops your template placeholder names from overwriting the logger's own JSON fields. The
built-in fields are timestamp, level, icon, traceId, class, method, message, exception. If a
placeholder shares one of those names, it's skipped as a separate field (so a placeholder named
level can't clobber the real log level — which matters because JSON consumers usually take
last-wins on duplicate keys). The value still appears in the rendered message. See
Message Templates.
Yes. Each entry is written under a lock, so concurrent threads never interleave their output.
Configure swaps options atomically, and the trace id is an AsyncLocal isolated per async
flow. See Performance & Internals.
The logger writes to Console.Out (standard output). In containers, AWS Lambda, and most cloud
hosts, stdout is captured and forwarded to your log system automatically. To capture to a file
yourself, redirect stdout (e.g. myapp > app.log) or set a custom Console.SetOut(...) writer
at startup.
Yes — the package is multi-targeted with native assemblies for net8.0, net9.0, and net10.0, and NuGet picks the right one for your project automatically. (A single net8.0 build would also run on 9 and 10 via backward compatibility, but the multi-target package gives each framework its own optimized assembly.)
For apps, internal libraries, console tools, and Lambdas — absolutely; the lack of DI plumbing
is the whole point. For a widely-distributed public library, weigh that your consumers can't
swap the logger via DI or redirect it through their own ILogger pipeline. If that flexibility
matters to your consumers, expose your own abstraction or accept an ILogger. If simplicity and
zero dependencies matter more, the static logger is a fine choice.
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