Skip to Content
BackendObservabilityMetrics

Metrics

Push counters and histograms about business events to Dash0 so we can chart, alert, and slice them by attributes — without inflating trace volume or grepping logs.

This page covers how to add a metric correctly. The OTel SDK is already wired in @repo/tracer; you just import and use it.

When to use metrics (vs traces vs logs)

SignalQuestion it answersExample
Metric”How often?” / “How much?” / “How fast?”Orders placed per hour, by marketplace
Trace”What happened in this specific request?”Why did this checkout take 8 seconds?
Log”What does this specific event look like?”The exact error message for order #abc123

If your question is aggregate (counts, rates, distributions) → metric. If it’s about a specific occurrence → trace or log.

⚠️ The cardinality trap — read before adding a metric

Dash0 bills per unique (metric name, attribute set) time series, at roughly $0.20 per 1M datapoints. That’s cheap if you keep cardinality bounded.

  • { game: "pokemon", action: "create" } — 5 games Ă— 3 actions = 15 series âś“
  • { userId: "abc123" } — millions of users = millions of series âś— ($200+/mo and unusable charts)

Attribute values MUST be enums or bounded categorical sets. Things like user IDs, order IDs, session IDs, IP addresses, free-text — those belong on traces or logs, never metrics. If you’re not sure how many distinct values an attribute can take, don’t add it.

The API

Three things to know.

import { metrics } from "@repo/tracer" // 1. Get a meter — one per domain/package, conventionally the package name. const meter = metrics.getMeter("@repo/inventory") // 2. Declare counters as module-level constants (NOT inside handlers — would // re-register on every call). const inventoryMutations = meter.createCounter("inventory.mutations", { description: "Inventory create/update/delete operations", }) // 3. Call .add() at every event site. inventoryMutations.add(1, { game: gameSlug, // pokemon / mtg / lorcana / ... action: "create", // create / update / delete source: "csv-import" // ui / csv-import / api / admin })

Counter types

MethodUse forExample
meter.createCounter(name, opts)Monotonic count of eventsorders.placed, users.signups
meter.createUpDownCounter(name, opts)Quantity that goes up AND downQueue depth, active sessions
meter.createHistogram(name, opts)Distribution of values (latency, size)Request latency, payload bytes

Most cases are counters. Reach for histograms when you need percentiles (p50/p95/p99), and up-down counters when the value can decrement.

Naming conventions

Lowercase, dotted, scoped to a domain. Match how you’d want to filter in Dash0.

NameTypeDescription
inventory.mutationsCounterInventory create/update/delete operations
orders.placedCounterSuccessful order checkouts
users.signupsCounterAccount creations
users.loginsCounterSuccessful logins
auth.failed_loginsCounterFailed login attempts (distinct metric — different question)
orders.fulfillment.duration_msHistogramTime from placed → shipped

Rules:

  • <domain>.<event> or <domain>.<sub>.<event>
  • Plural events (signups, not signup).
  • Include unit in the name only when ambiguous (.duration_ms, .bytes). Counters of events don’t need a unit suffix.
  • Errors and successes go in separate metrics, not one metric with a success: true/false attribute. The question “how many failures” is independent of “how many successes” and you’ll want to alert on them differently.

Worked example — “user signs up”

Before

// packages/backend/user/src/auth/signup.service.ts @injectable() export class SignupService { async signup(input: SignupInput): Promise<Result<User, SignupError>> { // ... validate, create user, send welcome email ... return ok(user) } }

After

// packages/backend/user/src/auth/signup.service.ts import { metrics } from "@repo/tracer" // Module-level — registered once when the file loads. const meter = metrics.getMeter("@repo/user") const signupsCounter = meter.createCounter("users.signups", { description: "Successful account creations", }) @injectable() export class SignupService { async signup(input: SignupInput): Promise<Result<User, SignupError>> { // ... validate, create user, send welcome email ... signupsCounter.add(1, { source: input.source, // "web" | "mobile" | "discord-oauth" country: input.country, // ISO country code (~250 values — fine) }) return ok(user) } }

Two attributes, both bounded enums. Total cardinality: ~3 sources Ă— ~250 countries = ~750 series. Cheap and useful.

If you wanted to track signup failures, that’s a separate counter (users.signup_failures) with attributes like { reason: "email_taken" | "invalid_email" | "rate_limited" } — not a success: false flag on the same counter.

Where to view in Dash0

In Dash0, switch to the dataset that matches your environment (dev / staging / prod), then go to the Metrics explorer. Filter by:

  • service.name = "repo/backend" (or "repo/public-api")
  • The metric name (e.g. users.signups)

Group by attributes (e.g. source, country) to slice the chart. The standard resource attributes (deployment.environment.name, aws.ecs.task.arn, cloud.region) are attached automatically by the OTel SDK + Collector — no need to set them yourself on the counter.

Local development

Metrics export honors OTEL_SDK_DISABLED=true — same flag that skips the tracer. If you want metrics to flow in local dev:

  1. Set OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
  2. Run a local OTel Collector pointed at Dash0 (or use the debug exporter to dump metrics to stdout)

Most engineers don’t need this — verify the counter is wired correctly with a unit test or by deploying to dev and checking Dash0.

Adding a new metric — checklist

Pick the name

Lowercase, dotted, scoped to a domain. See Naming conventions.

Pick the attributes (and verify cardinality)

Each attribute should be an enum or bounded set. Multiply the cardinalities together — if you’re under ~1000 series for the metric, you’re fine. Above that, drop or coarsen attributes.

Declare the counter as a module-level constant

const fooCounter = meter.createCounter(...) at the top of the file, NOT inside the function that calls it.

Call .add() at every event site

Pass 1 (or the relevant value for histograms) and the attribute object. Keep attribute keys identical across all call sites for the same metric — different keys produce different series.

Verify in Dash0 after deploy

Within ~60 seconds of the first event after deploy (the SDK exports on a 60s interval), the metric should appear in the Dash0 Metrics explorer under the matching dataset.

Code Locations

ComponentLocation
metrics API re-exportpackages/backend/tracer/src/index.ts
MeterProvider + OTLP exporter wiringpackages/backend/tracer/src/lib/tracer.ts
Collector pipeline that ships metrics to Dash0specs/dash0/infra.md (§3.3 — the YAML in aws_ssm_parameter.otel_collector_config)
Last updated on