Skip to content

Observability

CQLite emits OpenTelemetry traces and metrics at runtime so you can see query, read, write, compaction, and Arrow Flight behavior in a real telemetry backend. This page covers the one-command local stack, configuration for every surface, and the metric catalog.

This is the runtime observability guide. For offline CPU/heap profiling (flamegraphs, dhat), see the profiling docs in the repository (docs/profiling.md).

The runtime foundation must be compiled in: build with the observability feature on the relevant crate. Without it, telemetry calls are inert no-ops.

A ready-to-run Docker Compose stack — OpenTelemetry Collector + Jaeger + Prometheus + Grafana — lives in the repository under docs/observability/.

Terminal window
docker compose -f docs/observability/docker-compose.yml up
ServiceURLPurpose
OTLP gRPChttp://localhost:4317CQLite exports here (default)
OTLP HTTPhttp://localhost:4318with CQLITE_OTEL_PROTOCOL=http
Jaeger UIhttp://localhost:16686traces
Prometheushttp://localhost:9090raw metrics
Grafanahttp://localhost:3000dashboards (anonymous admin)

The “CQLite — Overview” dashboard (throughput, latency, error rate) auto-loads in Grafana under the CQLite folder.

Then point CQLite at it:

Terminal window
CQLITE_OTEL_ENABLED=true CQLITE_OTEL_ENDPOINT=http://localhost:4317 \
cqlite --schema test-data/schemas/basic-types.cql \
--data-dir test-data/datasets/sstables \
--query "SELECT * FROM test_basic.simple_table LIMIT 5"

A SELECT produces this span tree in Jaeger:

query.execute
└─ query.table_scan (or query.point_lookup / query.range_scan)
└─ sstable.partition_lookup.index (BIG) or sstable.partition_lookup.bti_trie (BTI)

Over Arrow Flight the same tree nests under the per-RPC span and honors the client’s traceparent:

flight.rpc (do_get)
└─ query.execute
└─ query.table_scan
└─ sstable.partition_lookup.index

Shared environment variables (CQLITE_OTEL_*)

Section titled “Shared environment variables (CQLITE_OTEL_*)”

Read by every surface. Booleans accept 1/0, true/false, yes/no, on/off.

VariableTypeDefaultMeaning
CQLITE_OTEL_ENABLEDboolfalseMaster switch; when false, init is a no-op.
CQLITE_OTEL_ENDPOINTstringhttp://localhost:4317OTLP collector endpoint (gRPC) or base URL (HTTP).
CQLITE_OTEL_PROTOCOLenumgrpcgrpc or http (HTTP/protobuf).
CQLITE_OTEL_SERVICE_NAMEstringcqliteservice.name resource attribute.
CQLITE_OTEL_SERVICE_VERSIONstringcrate versionservice.version resource attribute.
CQLITE_OTEL_SAMPLING_RATIOf641.0Trace-ID-ratio sampling probability, clamped to [0.0, 1.0].
CQLITE_OTEL_TIMEOUT_MSu6410000Exporter export timeout in milliseconds.
CQLITE_VERIFY_PRESENCE_ORACLEboolfalseOpt-in soundness check: when true, a read whose bloom/BTI-trie reports a key “definitely absent” runs an authoritative confirmation scan and increments cqlite.read.bloom.false_negatives on a contradiction (expected value: 0). Off by default — it is the one presence-oracle counter that costs real work.

One --otel-* flag per setting, each with a CQLITE_OTEL_* env fallback (explicit flag wins; precedence is config file < env < flag): --otel-enabled, --otel-endpoint, --otel-protocol, --otel-service-name, --otel-service-version, --otel-sampling-ratio, --otel-timeout-ms.

Terminal window
cqlite --otel-enabled true --otel-endpoint http://localhost:4317 \
--otel-protocol grpc --otel-service-name cqlite-cli \
--schema test-data/schemas/basic-types.cql \
--data-dir test-data/datasets/sstables \
--query "SELECT * FROM test_basic.simple_table LIMIT 5"

The CLI config file also has an [observability] section (keys mirror the env vars without the prefix): enabled, endpoint, protocol, service_name, service_version, sampling_ratio, timeout_ms.

[observability]
enabled = true
endpoint = "http://localhost:4317"
protocol = "grpc"
service_name = "cqlite-cli"
sampling_ratio = 1.0
timeout_ms = 10000

cqlite.open(...) takes an optional otel_config dict (layered over the environment) and a default traceparent; execute() / execute_streaming() accept a per-call traceparent. Recognised otel_config keys: enabled, endpoint, protocol, service_name, service_version, sampling_ratio, timeout_ms (unknown keys raise ValueError).

import cqlite
with cqlite.open(
"test-data/datasets/sstables",
schema="test-data/schemas/basic-types.cql",
otel_config={"enabled": True, "endpoint": "http://localhost:4317",
"service_name": "cqlite-python"},
traceparent="00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
) as db:
for row in db.execute("SELECT * FROM test_basic.simple_table LIMIT 5"):
print(row.to_dict())

Database.open(dir, options) accepts an optional otel object (and a traceparent string), each field layered over the shared CQLITE_OTEL_* environment variables. Requires a build with the observability feature.

const db = await Database.open('test-data/datasets/sstables', {
schema: 'test-data/schemas/basic-types.cql',
otel: { enabled: true, endpoint: 'http://localhost:4317', protocol: 'grpc',
serviceName: 'cqlite-node', samplingRatio: 1.0, timeoutMs: 10000 },
traceparent: '00-<trace-id>-<span-id>-01',
});

Or configure purely through the shared environment variables:

Terminal window
CQLITE_OTEL_ENABLED=true CQLITE_OTEL_ENDPOINT=http://localhost:4317 \
CQLITE_OTEL_SERVICE_NAME=cqlite-node node app.js

Reads the shared CQLITE_OTEL_* environment at startup and propagates the incoming W3C traceparent gRPC metadata into CQLite’s spans.

Terminal window
CQLITE_OTEL_ENABLED=true CQLITE_OTEL_ENDPOINT=http://localhost:4317 \
CQLITE_OTEL_SERVICE_NAME=cqlite-flight cqlite-flight --listen 0.0.0.0:8815

Names are dot-separated under cqlite.; units use UCUM annotations. The OTel Collector’s Prometheus exporter sanitises dotted names to underscores, appends _total to counters, and adds unit suffixes (_seconds for s, _bytes for By) — e.g. cqlite.read.rowscqlite_read_rows_total.

MetricInstrumentUnitBounded attributes
cqlite.read.rowscounter{row}cqlite.sstable.format
cqlite.read.bytescounterBycqlite.sstable.format, cqlite.compression
cqlite.read.partitionscounter{partition}cqlite.sstable.format
cqlite.read.durationhistogramscqlite.sstable.format
cqlite.read.partition_lookup.totalcounter1cqlite.result, cqlite.query.access_path, cqlite.sstable.format
cqlite.read.bloom.checkscounter1cqlite.result, cqlite.sstable.format
cqlite.read.sstables_prunedcounter{sstable}cqlite.sstable.format
cqlite.read.bloom.false_negativescounter1cqlite.sstable.format
cqlite.storage.open.sstablescounter{sstable}(none)
cqlite.storage.open.bytescounterBy(none)
cqlite.storage.open.tablescounter1(none)
cqlite.sstables.opengauge{sstable}cqlite.sstable.format
cqlite.query.durationhistogramscqlite.subsystem
cqlite.query.rowscounter{row}cqlite.query.access_path, cqlite.query.plan_type
cqlite.query.rows_scannedcounter{row}cqlite.query.access_path
cqlite.query.degraded_path.totalcounter1cqlite.query.fallback_reason
cqlite.write.mutationscounter{row}(none)
cqlite.write.partitionscounter{partition}(none)
cqlite.write.bytescounterBy(none)
cqlite.memtable.size_bytesgaugeBy(none)
cqlite.memtable.rowsgauge{row}(none)
cqlite.wal.sync.durationhistograms(none)
cqlite.flush.durationhistograms(none)
cqlite.flush.rowscounter{row}(none)
cqlite.flush.bytescounterBy(none)
cqlite.flush.sstablescounter{sstable}(none)
cqlite.compression.ratiohistogram1cqlite.compression
cqlite.compaction.durationhistograms(none)
cqlite.compaction.rows_mergedcounter{row}(none)
cqlite.compaction.bytes_writtencounterBy(none)
cqlite.compaction.sstables_incounter{sstable}(none)
cqlite.compaction.sstables_outcounter{sstable}(none)
cqlite.compaction.tombstones_purgedcounter{tombstone}(none)
cqlite.compaction.tombstones_suppressedcounter{tombstone}(none)
cqlite.compaction.tombstones_emittedcounter{tombstone}(none)
cqlite.merge.rows_incounter{row}(none)
cqlite.merge.rows_outcounter{row}(none)
cqlite.compaction.laggauge{sstable}(none)
cqlite.compaction.finalize.durationhistograms(none)
cqlite.compaction.budget.requestedhistograms(none)
cqlite.compaction.budget.consumedhistograms(none)
cqlite.errors.totalcounter{error}cqlite.error.category, cqlite.subsystem
cqlite.rpc.requestscounter1cqlite.rpc.method, cqlite.rpc.status
cqlite.rpc.durationhistogramscqlite.rpc.method, cqlite.rpc.status
cqlite.rpc.in_flightgauge1cqlite.rpc.method
cqlite.rpc.rowscounter{row}cqlite.rpc.method
cqlite.rpc.bytescounterBycqlite.rpc.method

Unbounded values (raw error messages, partition keys, full query text) are NEVER attached as attributes or span fields.