Skip to content
thushxr edited this page Jun 27, 2026 · 1 revision

FAQ

Common questions and gotchas.


Why are there two loggers, Log and Log<T>?

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>.

Why isn't my {placeholder} substituted when I use 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>.

Can I get the method name without using Log<T>?

Not cleanly through the non-generic Log, and that's by design. Two mechanisms exist:

  1. [CallerMemberName] (what Log<T> uses) — free and reliable, but it needs a trailing optional parameter, which can't coexist with the params object?[] args that Log uses for templates. So Log can't also capture the method.
  2. 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>.

Why are there no line numbers?

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.

Why are the methods named Information and not LogInformation?

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(...).

Why don't I need a using statement in some files?

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.*.

Do UseClassName / UseMethodName affect the non-generic Log?

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)

What is the reserved-keys list for?

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.

Is the logger thread-safe?

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.

Where does the output go? Can I log to a file?

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.

Does it run on .NET 8, 9, and 10?

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.)

Is a static logger a good fit for my library?

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.

Clone this wiki locally