Author and modify OpenTelemetry metrics in apps/api with low-cardinality attributes, canonical names, and the right histogram/counter split. Use when adding a new counter or histogram, editing apps/api/src/observability/metrics.ts, wiring a metric call inside a Fastify route or worker, or debugging a blown-up metrics bill / cardinality explosion in SigNoz.
Metrics are for aggregates ("how many", "how long", "what fraction"),
not for individual events. Every metric attribute becomes part of the
time-series key; putting a high-cardinality value there (like call_id)
creates one time series per value, which blows up ClickHouse storage
and ruins dashboards.
Quick reference: .cursor/rules/otel-metrics.mdc. Metrics live in
metrics.ts. Pair
with .cursor/rules/wide-event-logging.mdc.
If a field can take more than ~20 distinct values across the lifetime of the service, it does not belong on a metric attribute. Put it on the wide event instead.
Individual business details (call_id, load_id, mc_number, raw
rate, city name) go on the wide event, not the metric. The metric
counts categories.
Format: carrier_sales.<domain>.<event>, snake_case, singular noun.
| Name | Kind | Unit | What |
|---|---|---|---|
carrier_sales.negotiation.rounds | histogram | rounds | Rounds taken until accept/max/decline |
carrier_sales.booking.outcome | counter | -- | Offer outcomes tagged by result |
carrier_sales.carrier.verification | counter | -- | FMCSA verifications tagged by eligible |
carrier_sales.webhook.received | counter | -- | Inbound webhooks tagged by signature_state (valid/invalid/absent) and status |
carrier_sales.sentiment | counter | -- | Sentiments from transcript classification |
carrier_sales.call.outcome | counter | -- | Call outcomes from transcript classification |
carrier_sales.load.search.results | histogram | loads | Loads returned per search |
When adding a metric, follow this list's style: a concrete verb/noun
pair under the right domain. Avoid generic names (carrier_sales.events,
carrier_sales.count).
export const myFeatureOutcomeCounter = meter.createCounter(
'carrier_sales.my_feature.outcome',
{ description: 'What the metric counts (one sentence)' },
)
export const myFeatureLatencyHistogram = meter.createHistogram(
'carrier_sales.my_feature.latency',
{ description: 'Time spent doing X', unit: 'ms' },
)
description is required; it shows up in SigNoz.unit; counters don't.carrier-sales-api.*);
OTel's resource attributes already carry that.Only these keys may appear on metric attributes today. Treat this as