Logging and tracing
The SDK emits structured logs through ILogger and distributed-tracing spans
through ActivitySource. Both are free when nobody is listening and require
no configuration to stay silent.
Crucially, the SDK does not depend on any telemetry product. It writes to .NET's standard abstractions, and whatever you have wired up — Serilog, Seq, Application Insights, Datadog, OpenTelemetry, Sentry — receives it automatically.
Logging
With dependency injection there is nothing to do. AddTcgDex picks up the
container's ILoggerFactory:
builder.Services.AddTcgDex();
Without a container, pass one in:
using TcgDexClient client = TcgDexClient.Create(loggerFactory: myLoggerFactory);
// or
TcgDexClient client = new(httpClient, options, myLoggerFactory);
Everything is logged under the single category TcgDex, so one rule
configures all of it:
{
"Logging": {
"LogLevel": {
"TcgDex": "Debug"
}
}
}
What you get at each level
| Level | What appears |
|---|---|
Trace |
Cache hits, misses, coalesced requests. High volume. |
Debug |
Every request and its status and duration; cache revalidations; missing resources. |
Information |
Client configuration, once at startup. Nothing per-request. |
Warning |
Timeouts, unparseable responses, dropped GraphQL entries. |
Error |
Failed requests with the API's own problem detail. |
A missing card logs at Debug, not Warning. Asking for something that does
not exist is an ordinary outcome that returns null, and logging it louder makes
normal use look broken.
Nothing is logged at Information per request. A library that does floods
its consumer's default configuration, which is how people end up turning SDK
logging off entirely.
Event ids
Stable across versions, so you can alert on them.
| Range | Area | Ids |
|---|---|---|
| 1000 | Request lifecycle | 1000 sending · 1001 completed · 1002 not found · 1003 failed · 1004 errored · 1005 timed out · 1006 deserialization failed |
| 1100 | Caching | 1100 hit · 1101 revalidated · 1102 miss · 1103 coalesced · 1104 evicted |
| 1200 | Configuration | 1200 configured |
| 1300 | GraphQL | 1300 search completed · 1301 errors · 1302 dropped entries |
Cost when disabled
Messages are built with the LoggerMessage source generator, which emits a
cached delegate and an IsEnabled check per message. A disabled level costs a
branch — no string formatting, no boxing, no allocation.
That is not true of interpolated logging: logger.LogDebug($"…{uri}") formats
the string and boxes the arguments before the level is checked, so it costs
something on every call whether or not anyone is listening. A test asserts that
nothing is formatted when logging is off.
What is not logged
- Raw HTTP request and response bodies. When registered through
AddTcgDex,IHttpClientFactoryalready logs every request and its timing underSystem.Net.Http.HttpClient. Duplicating that would double the noise and disagree on the detail. - Response payloads. The API carries no secrets, but logging whole bodies is a habit that goes badly wrong the moment an SDK is used against something that does.
Tracing
Subscribe to the source and spans flow into any OpenTelemetry-compatible backend:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource(TcgDexActivity.SourceName) // "TcgDex.CSharpSdk"
.AddHttpClientInstrumentation());
Each SDK operation becomes one client span tagged with url.full,
http.request.method and http.response.status_code. Failures set the span
status to Error with an error.type tag, following OpenTelemetry conventions
so they surface as errors without extra mapping.
Spans cover SDK operations, not raw HTTP. HttpClient emits its own spans,
so adding AddHttpClientInstrumentation nests the transport detail underneath
rather than duplicating it.
When nothing subscribes, starting an activity returns null and the cost is a
null check.
Using Sentry, Datadog, or anything else
Wire it up in your application, not through this SDK. Sentry's logging integration is an example:
builder.Logging.AddSentry();
Every SDK log then flows into Sentry with no further work, because the SDK
writes to ILogger rather than to a specific backend.
This is deliberate, and it is worth being explicit about why the SDK does not take that dependency itself:
- A library must not choose your telemetry vendor. A hard dependency would force Sentry on every consumer, including those using something else or nothing at all — along with its version constraints and transitive packages.
- A library must never phone home. An SDK that reported errors to its author's account from your process would be exfiltrating your data to a third party you never agreed to. That is a property of malware, not of a well-behaved dependency, regardless of intent.
ILoggerandActivitySourcealready are the integration point. Anything a vendor-specific dependency would buy is available by configuring that vendor in the application that owns the decision.
The same reasoning applies to metrics, crash reporting and alerting: the SDK's job is to emit good signals through standard abstractions and let the application decide where they go.