@comis/observability-otel): an OpenTelemetry exporter that pushes traces,
metrics, and logs over OTLP and serves a Prometheus /metrics pull endpoint —
plus five Grafana dashboards-as-code, recording/alert rules, and a docker-compose
that stands the whole thing up in one command.
It is off by default and content-free by construction. This page is the
complete operator guide; the native Observe dashboard
is the complementary zero-dependency lens (no external services required).
Two lenses, one truth. The built-in Observe SPA reads Comis’s in-process
collectors directly from SQLite — zero external dependencies, always available.
The Prometheus/Grafana surface here is the standards lens for operators who
already run a Prometheus + Grafana stack (or want long-term retention, alerting,
and cross-service correlation). Both render the same underlying signals; pick
whichever fits your environment, or run both.
Enabling it
The exporter loads only when you turn it on. Both surfaces are independent — you can run the Prometheus/metrics endpoint without an OTLP collector, and vice
versa. Add to ~/.comis/config.yaml:
prometheus.enabled or otel.enabled is true, the daemon loads the
extension via a config-gated dynamic import. If the package is somehow
unavailable, the daemon logs a warning and continues with telemetry disabled — it
never crashes boot. The full key reference is in
Configuration → observability.
The OTLP transport is
http/protobuf. Setting
protocol: "grpc" validates, but the daemon falls back to http/protobuf with a
logged warning (gRPC support is planned — the fallback is never a silent wrong-transport).Security posture — trusted-operator, honestly
The Prometheus/metrics endpoint has no built-in authentication. The OpenTelemetry
PrometheusExporter opens its own HTTP listener; it is not mounted on Comis’s
gateway and is not gated by the gateway token. The access boundary is:
- The loopback bind.
hostdefaults to127.0.0.1, so the endpoint is reachable only from the local host. Comis never binds0.0.0.0implicitly. - Your reverse proxy / firewall. To expose
/metrics(or let a remote Prometheus scrape it), front it with a reverse proxy that adds TLS and auth, or restrict it at the firewall — exactly as you would any internal metrics endpoint. See Reverse proxy.
What’s exported (content-free)
Every metric is an id, enum, or count — never message content, never a secret. The label set is deliberately low-cardinality (agent, tenant, provider,
model, operation, reason, state, outcome, kind, …); session, trace,
and user ids are never labels (a label-cardinality blowup is a denial-of-service
and a PII surface). Both label keys and values are bounded: the reason label
maps to the closed cache-break-reason set (an unknown reason buckets to other),
provider/model are the config-resolved ids (not raw request strings), and the
cardinalityCap (default 10 000) is enforced, not just warned — once the
distinct-series estimate reaches the cap, further distinct series collapse into a
single _overflow bucket (and a WARN fires), so a label mistake cannot grow the
series set without bound.
The exporter boundary enforces a closed attribute allowlist: only the known
metric-label keys and the known span/log attribute keys leave the daemon; any
other key is dropped before it can become a label or span attribute. Allowed
values are then re-redacted independently of any upstream scrubbing — so nothing
leaks even with the GenAI semantic-convention attributes enabled, and a future
careless attribute addition cannot smuggle a high-entropy secret out under a
benign key.
The exported series cover cost & spend (corrected comis_cost_usd_total, the
spend-vs-ceiling gauges, pricing coverage), cache health (comis_cache_break_total
by reason, savings), fleet health (degraded rate, error kinds, breaker trips,
turn-duration histograms), security & audit (comis_audit_events_total,
secret-access, injection-detected), and reliability (run-duration histograms,
offloads). The single source of truth for the metric set is the catalog in
@comis/observability-otel, and every catalogued metric has a live producer
(an event subscription that increments it, or a registered gauge) — a CI guard
fails the build if a catalogued metric has no producer, and the dashboard/rule
drift guard fails if a panel or alert ever references a metric the exporter does
not actually produce (not merely one absent from the catalog). So a shipped
panel never silently renders “No data” and a shipped alert can always fire.
GenAI semantic conventions (pre-stable)
Settingotel.genaiSemconv: true switches the span attributes to the latest
OpenTelemetry GenAI convention namespace (the standard
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental gate). That convention
is still Development status upstream — it will churn — so the flag is off by
default and documented as tracking a pre-stable spec. Content stays off
regardless: the three GenAI message-content span attributes are spec-level
Opt-In and are omitted unless you separately set otel.captureContent: true,
and even then the exporter re-redacts at the boundary.
One-command stand-up (docker-compose)
The repository shipsdocker/observability/docker-compose.yml — Prometheus
(scraping a Comis daemon’s /metrics) plus Grafana, auto-provisioned with the
Comis datasource and all five dashboards:
observability.prometheus.enabled: true
running on the docker host, exposing /metrics on 127.0.0.1:9464. The compose
stack scrapes it via host.docker.internal:9464; it deliberately does not
launch the daemon (the daemon’s lifecycle, data directory, and secrets are yours,
not the stack’s).
The Grafana admin credentials in the compose file (
admin/admin) are for a
local sandbox only. Change them — and do not expose ports 3000/9090 to an
untrusted network without a reverse proxy. The same trusted-operator posture
applies as for /metrics.Importing into an existing Grafana
If you already run Grafana, point a file or dashboards provisioner atgrafana/dashboards/*.json (and the datasource at
grafana/provisioning/datasources/prometheus.yaml), or import the dashboard JSON
files manually. Each dashboard carries template variables for datasource,
tenant, agent, provider, and model.
Chart → comis explain drill-down
Every dashboard panel carries a data link to Comis’s incident report
(comis explain) — the chart-to-root-cause pivot. Click through from a spiking
series to the deterministic IncidentReport for that session
(outcome, cost, per-tool failures, breaker timeline, and the cache-break and
audit sections).
Exemplar honesty. OpenMetrics exemplars (the trace id riding on a histogram
sample) are the ideal pivot, but the installed Prometheus exporter
(
@opentelemetry/exporter-prometheus) does not render exemplars on the
/metrics pull surface. So the drill-down keys on the comis.trace_id span
attribute (carried on the OTLP push path) templated into the panel’s data-link
URL instead. The live exemplar-on-the-sample pivot is available when you route
through an OTLP collector that supports it.Recording & alerting rules
prometheus/rules/comis-recording.yml pre-computes the derived series (degraded
rate, cache-break rate by reason, dollars per hour, pricing-gap rate);
prometheus/rules/comis-alerts.yml ships five alerts: spend approaching the
ceiling, a degraded-rate spike, a breaker-trip burst, a pricing gap, and an
audit-sink failure.
The spend-approaching-ceiling alert fires at the same threshold as the
in-process spend kill-switch — observability.spend.warnAtFraction (0.8). They
are a single source of truth: a test asserts the alert literal equals the config
default, so the dashboard alert and the kill-switch can never drift apart.
Validate the rules with promtool (part of the Prometheus distribution) before
deploying:
Environment variables
The exporter honors the standard OpenTelemetry SDK environment variables (read by the underlying SDK, not stored by Comis; theconfig.yaml values take
precedence for the keys Comis controls):
OTEL_EXPORTER_OTLP_ENDPOINT— the OTLP collector endpoint whenotel.endpointis empty.OTEL_SEMCONV_STABILITY_OPT_IN— set togen_ai_latest_experimentalalongsideotel.genaiSemconv: trueto opt into the latest GenAI convention.OTEL_SERVICE_NAME— overrides the OTLP service name (defaults tocomis).
Operator-verified steps
A few steps need a live Linux host with the running daemon and the docker stack — they cannot be validated by Comis’s CI (which asserts the artifacts’ validity, not a live render):docker compose -f docker/observability/docker-compose.yml up— the live Prometheus + Grafana stand-up (needs a daemon emitting/metrics).- Opening Grafana and confirming the five dashboards render with live data.
- Clicking a spiking series through to
comis explain(the data-link pivot). promtool check rules prometheus/rules/*.yml— the semantic PromQL lint.
