Skip to content
Client v5: BLE, BLE Hosting, HTTP, Jobs - Linux, MacOS, & Blazor Support! Full AOT, RX on BLE only & MANY other features! Power up!

Telemetry & Diagnostics

The core Shiny.DocumentDb package emits OpenTelemetry-native metrics and distributed trace spans for every store operation — embedded and always-on, on every provider and every construction path (DI or new …DocumentStore(options)). It’s built on the standard .NET primitives (System.Diagnostics.Metrics.Meter and ActivitySource), so it plugs straight into OpenTelemetry, the .NET Aspire dashboard, Application Insights, Prometheus/Grafana, or anything else that listens. There is no decorator, no wrapper type, and nothing to opt into.

Separately, when a store is created from the container (via AddDocumentStore, a provider’s Add…DocumentStore, or the provider’s IServiceProvider constructor) and an ILoggerFactory is registered, every SQL / operation statement is logged through ILogger at Debug under the Shiny.DocumentDb category (plus a one-time store-initialized line) — on the relational core and all six non-relational providers alike. Turn it on with a log-level filter — no code changes:

appsettings.json
"Logging": { "LogLevel": { "Shiny.DocumentDb": "Debug" } }

The options.Logging string callback still fires alongside it (they compose), and the container-free new DocumentStore(options) path is unaffected. SQL is parameterized, so values ride as @parameters rather than being inlined into the logged text.

It is zero-cost when nobody is listening: with no meter subscriber the instruments no-op, and with no ActivityListener the spans are never allocated. That matters on mobile/embedded — instrumentation only costs something once you opt in.

Instrumentation is embedded and always-on — there is nothing to register on the store. Just point your OpenTelemetry pipeline at the meter / source named Shiny.DocumentDb:

services.AddDocumentStore(o => o.DatabaseProvider = new SqliteDatabaseProvider("Data Source=app.db"));
services.AddOpenTelemetry()
.WithMetrics(m => m.AddMeter("Shiny.DocumentDb"))
.WithTracing(t => t.AddSource("Shiny.DocumentDb"));

The db.system.name tag is derived from the store’s backend, so SQLite, PostgreSQL, MongoDB, CosmosDB, and the rest are all reported correctly with no per-provider configuration — including a store built with plain new …DocumentStore(options), which the old decorator could never reach.

A store registered with the keyed overload AddDocumentStore("orders", …) tags every measurement and span with db.namespace = "orders", so you can tell signals from multiple stores apart — automatically, no extra call.

Instrument names and tags follow the OpenTelemetry database client semantic conventions, so any OTel backend understands them without custom mapping.

InstrumentKindUnitMeaning
db.client.operation.durationHistogramsDuration of each operation — the primary signal (latency percentiles, throughput, error rate all derive from it).
db.client.operationsCounter{operation}Count of operations executed.
db.client.response.returned_rowsHistogram{row}Documents returned or affected (e.g. BatchInsert count, query result size, Get hit/miss as 1/0).
db.client.unit_of_work.operationsHistogram{operation}Buffered writes flushed per IDocumentSession.SaveChanges — write-batching / amplification insight.

Every measurement is tagged with:

TagExampleNotes
db.system.namesqlite, postgresql, mongodb, cosmosdbDerived from the wrapped store.
db.operation.nameinsert, get, query.to_list, historyThe store operation.
db.collection.nameOrderThe document type name (low-cardinality).
db.namespaceordersThe logical store name. Present only for keyed/named stores — the name you registered with AddDocumentStore("orders", …). Omitted on the non-keyed path.
outcomesuccess / error
error.typeSystem.InvalidOperationExceptionPresent only on failures.

Each operation starts an ActivityKind.Client span named {system}.{operation} (e.g. sqlite.insert) carrying the same tags. On failure the span status is set to Error and the exception is recorded on the span. Spans nest naturally: an IDocumentSession opens a {system}.unit_of_work parent span (tagged db.session.id) that every operation in the unit of work nests under, and the writes flushed by SaveChanges are child spans of the enclosing {system}.transaction span — so a unit of work reads as one correlated subtree.

Instrumented:

  • All CRUD — Insert, BatchInsert, Update, Upsert, SetProperty, RemoveProperty, Get, GetDiff, Remove, Clear.
  • String Query/QueryStream, Count, spatial (WithinRadius/WithinBoundingBox/NearestNeighbors) and vector (NearestVectors).
  • The fluent query terminals — ToList, ToAsyncEnumerable, Count, Any, ExecuteDelete, ExecuteUpdate, Max/Min/Sum/Average, and query-terminal NearestVectors (the builder operators do no I/O and are not traced).
  • All temporal operations from ITemporalDocumentStoreHistory, AsOf, AsOfAll, ChangesByActor, ChangesBetween, Restore, GetDiffBetween.
  • A session’s SaveChanges (a transaction span over the inner writes) and its unit_of_work parent span.

Not instrumented (by design):

  • NotifyOnChange and SubscribeChanges — long-lived subscriptions, passed through without per-event telemetry.
  • Provider internals the IDocumentStore boundary can’t see — raw SQL text, connection-pool acquisition, retries, Cosmos request-charge (RU). For those, layer provider-specific instrumentation alongside (the Logging option on each provider already exposes raw SQL).

Only metadata is recorded — operation, document type name, outcome, and counts. Document bodies, ids, and parameter values are never put on spans or metric tags. db.collection.name is the bounded set of your mapped types, so metric cardinality stays low; never add a document id or tenant id as a tag yourself.

InstrumentedDocumentStore implements IDocumentStore, ITemporalDocumentStore, IObservableDocumentStore, and IChangeFeedDocumentStore, so a cast or pattern-match keeps working after wrapping. If you call an optional capability the underlying provider doesn’t support, it throws NotSupportedException — the same convention the library uses elsewhere. The original store is reachable via the Inner property.

  • Keyed registrations (the named AddDocumentStore(name, …) overload) are not auto-decorated — wrap those stores manually with new InstrumentedDocumentStore(inner, metrics).
  • The fluent builder operators (Where, OrderBy, Select, …) are not spans; the terminal that executes the query is.