Observability
DotNet Query emits distributed traces, metrics, and structured log messages out of the box. It uses only BCL APIs — System.Diagnostics.ActivitySource, System.Diagnostics.Metrics.Meter, and Microsoft.Extensions.Logging.ILogger — so no OpenTelemetry package is required in the library itself. Consumers wire up collection on their side and the standard hooks are picked up automatically.
How It Works
All telemetry flows through a single public entry point:
// DotNetQuery.Core.Observability
public static class QueryTelemetry
{
public const string SourceName = "DotNetQuery";
public static readonly ActivitySource ActivitySource = new(SourceName);
public static readonly Meter Meter = new(SourceName);
}
QueryTelemetry.SourceName ("DotNetQuery") is the name you use when subscribing to traces or metrics in your app.
Enabling Logging
Pass a logger when creating the client. With DI the ILoggerFactory is resolved automatically:
// DI (recommended) — no extra configuration needed
builder.Services.AddDotNetQuery();
Without DI, pass a logger to the factory:
ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddConsole());
ILogger logger = loggerFactory.CreateLogger(QueryTelemetry.SourceName);
IQueryClient client = QueryClientFactory.Create(new QueryClientOptions(), logger: logger);
Enabling OpenTelemetry
Add the OpenTelemetry packages to your app project (not to the library):
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.Console # or any other exporter
Then subscribe to the "DotNetQuery" source in Program.cs:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource(QueryTelemetry.SourceName)
.AddConsoleExporter())
.WithMetrics(metrics => metrics
.AddMeter(QueryTelemetry.SourceName)
.AddConsoleExporter());
Any OpenTelemetry-compatible exporter works — Jaeger, Zipkin, OTLP, Prometheus, Azure Monitor, etc.
Traces
DotNet Query creates one activity span per operation:
| Span name | When |
|---|---|
query.fetch |
Every time a query fetches data |
mutation.execute |
Every time a mutation runs |
Query fetch span
The query.fetch span is tagged with the query key and status:
| Tag | Value |
|---|---|
query.key |
The string representation of the QueryKey (e.g. users:42) |
otel.status_code |
Ok on success, Error on failure |
error.type |
Exception type name (only on failure) |
Mutation execute span
The mutation.execute span carries the final status:
| Tag | Value |
|---|---|
otel.status_code |
Ok on success, Error on failure |
error.type |
Exception type name (only on failure) |
Metrics
All metrics use the "DotNetQuery" meter name. Attach a tag filter in your metrics pipeline if needed.
| Instrument | Type | Unit | Description |
|---|---|---|---|
dotnetquery.fetch.duration |
Histogram | ms | Duration of each fetch operation |
dotnetquery.fetch.active |
UpDownCounter | — | Number of fetch operations currently in flight |
dotnetquery.cache.hits |
Counter | — | Cache lookups that found an existing entry |
dotnetquery.cache.misses |
Counter | — | Cache lookups that created a new entry |
dotnetquery.mutation.duration |
Histogram | ms | Duration of each mutation operation |
Tags on metrics
| Metric | Tags |
|---|---|
dotnetquery.fetch.duration |
query.key, status (success / failure) |
dotnetquery.fetch.active |
query.key |
dotnetquery.cache.hits |
query.key |
dotnetquery.cache.misses |
query.key |
dotnetquery.mutation.duration |
status (success / failure) |
Log Messages
All log messages use the category "DotNetQuery" (the same string as QueryTelemetry.SourceName).
| Level | Message |
|---|---|
| Debug | Fetch started for key '{QueryKey}' |
| Debug | Fetch succeeded for key '{QueryKey}' in {Duration}ms |
| Warning | Fetch failed for key '{QueryKey}' after {Duration}ms (+ exception) |
| Debug | Fetch cancelled for key '{QueryKey}' |
| Debug | Cache hit for key '{QueryKey}' |
| Debug | Cache miss for key '{QueryKey}' |
| Debug | Mutation started |
| Debug | Mutation succeeded in {Duration}ms |
| Warning | Mutation failed after {Duration}ms (+ exception) |
| Debug | Mutation cancelled |
Filtering Log Output
Because all messages share the "DotNetQuery" category, you can control verbosity with a single filter:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"DotNetQuery": "Warning"
}
}
}
This suppresses the Debug-level fetch/cache messages and keeps only warnings (failures and retries).