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:
"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.
Subscribe
Section titled “Subscribe”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.
Keyed / named stores
Section titled “Keyed / named stores”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.
What it emits
Section titled “What it emits”Metrics
Section titled “Metrics”Instrument names and tags follow the OpenTelemetry database client semantic conventions, so any OTel backend understands them without custom mapping.
| Instrument | Kind | Unit | Meaning |
|---|---|---|---|
db.client.operation.duration | Histogram | s | Duration of each operation — the primary signal (latency percentiles, throughput, error rate all derive from it). |
db.client.operations | Counter | {operation} | Count of operations executed. |
db.client.response.returned_rows | Histogram | {row} | Documents returned or affected (e.g. BatchInsert count, query result size, Get hit/miss as 1/0). |
db.client.unit_of_work.operations | Histogram | {operation} | Buffered writes flushed per IDocumentSession.SaveChanges — write-batching / amplification insight. |
Every measurement is tagged with:
| Tag | Example | Notes |
|---|---|---|
db.system.name | sqlite, postgresql, mongodb, cosmosdb | Derived from the wrapped store. |
db.operation.name | insert, get, query.to_list, history | The store operation. |
db.collection.name | Order | The document type name (low-cardinality). |
db.namespace | orders | The logical store name. Present only for keyed/named stores — the name you registered with AddDocumentStore("orders", …). Omitted on the non-keyed path. |
outcome | success / error | |
error.type | System.InvalidOperationException | Present only on failures. |
Traces
Section titled “Traces”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.
Coverage
Section titled “Coverage”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-terminalNearestVectors(the builder operators do no I/O and are not traced). - All temporal operations from
ITemporalDocumentStore—History,AsOf,AsOfAll,ChangesByActor,ChangesBetween,Restore,GetDiffBetween. - A session’s
SaveChanges(atransactionspan over the inner writes) and itsunit_of_workparent span.
Not instrumented (by design):
NotifyOnChangeandSubscribeChanges— long-lived subscriptions, passed through without per-event telemetry.- Provider internals the
IDocumentStoreboundary can’t see — raw SQL text, connection-pool acquisition, retries, Cosmos request-charge (RU). For those, layer provider-specific instrumentation alongside (theLoggingoption on each provider already exposes raw SQL).
Privacy
Section titled “Privacy”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.
A faithful decorator
Section titled “A faithful decorator”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.
Caveats
Section titled “Caveats”- Keyed registrations (the named
AddDocumentStore(name, …)overload) are not auto-decorated — wrap those stores manually withnew InstrumentedDocumentStore(inner, metrics). - The fluent builder operators (
Where,OrderBy,Select, …) are not spans; the terminal that executes the query is.