Table of Contents

Observability

Four complementary surfaces: logs for the queries as they run, metrics for the dashboards, traces for the distributed picture, Explain for what will happen before you run it. All of them carry the facts only the engine knows.

Logs — Microsoft.Extensions.Logging, the way EF does it

The engine logs through the standard logging abstractions with stable categories and event ids — no logger-specific packages, because none are needed: Serilog, NLog and the console all plug in through the MEL providers they already ship. The ritual is the one you know from EF:

// Serilog:
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Override("eQuantic.Core.Data", LogEventLevel.Information)   // all providers
    // or per provider: "eQuantic.Core.Data.postgresql.Command"
    .WriteTo.Console()
    .CreateLogger();

Categories are eQuantic.Core.Data.{provider}.Command (postgresql, mysql, mariadb, sqlserver, cassandra, mongodb) and eQuantic.Core.Data.cosmosdb.Request. The events:

Event Id Level Carries
CommandExecuted 10001 Information statement (placeholders), elapsed, rows where known; Cosmos adds status and the RU charge
CommandFailed 10002 Error statement + the exception
CommitExecuted 10101 Information staged writes flushed, elapsed
ClientEvaluation 10201 Warning the residual that ran client-side (behind its opt-in)
AllowFiltering 10202 Warning a Cassandra query running as a declared scan
QuerySplit 10203 Warning an OR filter fanned out into parallel native queries
ConcurrencyConflict 10301 Warning expected vs affected on a lost race

The gates logging at Warning is deliberate: an opt-in that quietly became a hot path's habit should surface in production logs, not in an incident review.

Parameter values never log by default — statements carry placeholders, the same policy the traces follow. Turn values on the way you turn EF's on: deliberately, per environment —

services.AddSingleton(new DataConventions { EnableSensitiveDataLogging = true });

(On MongoDB this also gates command bodies, which inherently carry values; on Cosmos DB request bodies never log.)

Metrics — AddMeter("eQuantic.Core.Data")

services.AddOpenTelemetry().WithMetrics(metrics => metrics
    .AddMeter("eQuantic.Core.Data")
    .AddOtlpExporter());
Instrument Type Meaning
equantic.commands / equantic.command.failures counter commands executed / failed (tag db.system)
equantic.command.duration histogram (ms) command latency
equantic.commits / equantic.writes counter flushes and the staged writes they carried
equantic.client_evaluations counter queries whose residual ran client-side
equantic.allow_filtering counter declared scans (Cassandra)
equantic.query_splits counter OR fan-outs
equantic.concurrency_conflicts counter lost optimistic-concurrency races

The gate counters make the engine's honesty graphable — a rising client_evaluations on a dashboard is an alert, not an archaeology project.

Traces — eQuantic.Core.Data

Every provider emits System.Diagnostics.Activity spans on the shared source. Subscribe with any OpenTelemetry setup:

services.AddOpenTelemetry().WithTracing(tracing => tracing
    .AddSource("eQuantic.Core.Data")
    .AddOtlpExporter());

Spans follow OTel database conventions (db.system, db.statement with placeholders — never parameter values) and add the engine's own tags, the ones no driver instrumentation can know:

Tag Meaning
equantic.client_evaluation a residual filter ran in memory on this query
equantic.split_queries the OR-split branch count (Cassandra)
equantic.partition_scoped the fetch was pinned to a partition
equantic.allow_filtering the query ran with a declared scan (Cassandra)
equantic.writes staged writes flushed by this commit

These make the gates operable: alert on client_evaluation spans in a hot path, graph partition_scoped ratios on Cosmos, catch an accidental allow_filtering in production.

Explain — before you run

Every repository implements IExplainableRepository<TEntity>; every model has Explain():

Console.WriteLine(repo.Explain(options));   // the plan: statement, residual, gates, notes
Console.WriteLine(model.Explain(dialect));  // the mapping: names, types, keys, tokens, indexes

Treat both as testable artifacts — pin the lines you rely on:

var plan = repo.Explain(options);
Assert.That(plan.ClientEvaluation, Is.False, "this query must stay fully server-side");
Assert.That(plan.PartitionScoped, Is.True, "this query must pin the partition");

That turns performance regressions (a refactor that un-pins a partition, a filter that silently grew a residual) into failing tests instead of production incidents.

Query tags

options.WithTag("catalog-page") stamps the query — the tag lands where the store shows it (an SQL comment, trace tags), connecting a slow statement in the store's own tooling back to the call site that issued it.