Naming Metrics and Choosing Units

A metric's name is its interface. Queries, dashboards, alerts and recording rules all refer to it, often across many services, and every inconsistency — milliseconds in one service and seconds in another, requests here and reqs there — becomes a bug in every query that crosses them. A small set of conventions prevents most of it. This article covers base units, the naming rules for prometheus_client and OpenTelemetry, how OpenTelemetry names are translated for Prometheus backends, and the mistakes that cause real query errors. It belongs to metric types and cardinality in the Python metrics and instrumentation section.

Anatomy of a metric name A Prometheus metric name, http_server_request_duration_seconds, divided into parts: a namespace http, a subject server_request, the quantity duration, and the base unit suffix seconds. Histogram series add _bucket, _count and _sum. A counter, orders_placed_total, shows the _total suffix used only for counters. Below, the OpenTelemetry equivalent: an instrument named http.server.request.duration with a separate unit field of s. The Prometheus exporter joins them into the same name by replacing dots with underscores and appending the unit's Prometheus spelling. The note says the name carries what is measured and in which unit; everything else — route, status, service — is a label. Prometheus name http server_request duration seconds namespacesubjectquantitybase unit + _bucket / _count / _sum counter:orders_placed_total_total only on counters OpenTelemetry instrument name: http.server.request.duration unit: s → exporter joins them the name says what and in which unit — route, status, service are labels
A name identifies one quantity in one unit. Everything that varies across instances of that quantity belongs in labels.

Prerequisites

Familiarity with the metric types in choosing between counter, gauge, histogram and summary.

Implementation steps

Step 1 — Use base units. Durations in seconds, sizes in bytes, ratios as fractions between zero and one, temperatures in Celsius. Base units make every metric of a kind comparable and let dashboards scale for display. A Python service measuring with time.perf_counter() already has seconds; converting to milliseconds before recording adds a step, loses precision in integer histograms, and creates a unit mismatch with every other service.

t0 = time.perf_counter()
...
LATENCY.observe(time.perf_counter() - t0)      # seconds, as measured

Step 2 — Follow the path's naming convention. For prometheus_client: snake_case, a unit suffix in its plural base form (_seconds, _bytes), and _total on counters, which the library adds automatically. For OpenTelemetry: lowercase dotted names, the unit in the instrument's unit field rather than the name, and no _total.

# prometheus_client
Histogram("http_server_request_duration_seconds", "...")
Counter("orders_placed", "...")                      # exposed as orders_placed_total

# OpenTelemetry
meter.create_histogram("http.server.request.duration", unit="s")
meter.create_counter("orders.placed", unit="{order}")

Step 3 — Use semantic-convention names where they exist. OpenTelemetry defines names and attributes for HTTP servers and clients, database clients, messaging, and runtime metrics. Using them means dashboards, alerts and vendor integrations built for the convention work unchanged, and different services' metrics line up. Automatic instrumentation already emits them; hand-written instruments should match when they measure the same thing. The same idea applies to spans, as in naming spans and semantic conventions.

Step 4 — Put dimensions in labels, not names. http_requests_orders_total and http_requests_payments_total are one metric split into two names; a query for all requests must now list every name. http_requests_total{route="/orders"} is one metric, and sum(rate(http_requests_total[5m])) covers everything.

Step 5 — Check translated names before writing queries. OpenTelemetry names are translated when exported to Prometheus: dots become underscores, the unit is appended, counters gain _total. A curl of the exporter's endpoint, or a look at the backend's metric list, shows the real name.

How names cross from OpenTelemetry to Prometheus Four translations. A histogram named http.server.request.duration with unit s becomes http_server_request_duration_seconds with _bucket, _count and _sum series. A counter named orders.placed with unit {order} becomes orders_placed_total — the curly-brace annotation is dropped, since it is not a real unit. An up-down counter named db.client.connection.count with unit {connection} becomes db_client_connection_count, with no _total because it is not monotonic. A gauge named process.memory.usage with unit By becomes process_memory_usage_bytes. The note says the unit field is the source of the suffix, so an instrument with its unit in the name as well ends up with the unit twice. OpenTelemetry name · unitPrometheus name http.server.request.duration · shttp_server_request_duration_seconds orders.placed · {order}orders_placed_total db.client.connection.count · {connection}db_client_connection_count process.memory.usage · Byprocess_memory_usage_bytes unit in the name and the unit field → request_duration_ms_milliseconds
The exporter builds the suffix from the unit field. Putting the unit in the name as well doubles it.

Names for business and domain metrics

Semantic conventions cover infrastructure. Domain metrics — orders, payments, searches, jobs — need names the team chooses, and a few habits keep them consistent.

A namespace per domain, not per service. orders.placed and orders.cancelled share a prefix that groups them in metric browsers and autocomplete. If three services all record order events, they use the same names and are distinguished by the service resource attribute; a query for all orders placed then works across the fleet.

Past-tense verbs for event counters. orders.placed, payments.failed, emails.sent read as counts of things that happened. Present-tense or noun-only names — order, payment_status — leave readers guessing the type.

Nouns for states. queue.length, cache.entries, pool.connections read as current values, which is what gauges and up-down counters report.

Outcomes as labels. payments.processed{outcome="failed"} rather than a separate payments.failed lets one query compute a failure ratio. Either is defensible; the labelled form makes ratios and breakdowns easier, and the separate form makes the most important signal findable by name. Consistency within a service matters more than which one is chosen.

A short naming guide in the repository — a page listing the namespaces in use, the unit rules and a few examples — saves more time than any linting, because most naming inconsistency comes from someone not knowing a name already exists.

Label names deserve the same care

Everything above applies to label names too, and label names are where inconsistency does the most damage, because joins and aggregations match on them exactly. A service labelling status as status and another as code cannot be summed in one expression without relabelling. A route labelled route in one service and path in another, or handler in a third, makes every fleet-wide dashboard a union of special cases.

The semantic conventions again give a shared vocabulary: http.request.method, http.response.status_code, http.route, server.address, db.system. In a Prometheus backend these become http_request_method, http_response_status_code and so on. Services using prometheus_client directly can adopt the same names in snake_case, which lets their metrics sit beside OpenTelemetry-instrumented services without translation.

Label values matter as well. Status codes as strings of digits ("200"), not classes in one service and codes in another. Methods in upper case. Booleans as "true" and "false", not a mix of those with "1" and "yes". A value vocabulary written down once, beside the names, keeps queries that filter on values from silently missing half the fleet.

Renaming a metric without losing history

Sooner or later a name turns out to be wrong — the wrong unit, a service name embedded in it, a convention adopted after the fact. Renaming is straightforward in code and disruptive everywhere else: every dashboard, alert and recording rule refers to the old name, and the backend's history stays under it.

Renaming in three phases A timeline of a metric rename in three phases. In phase one, lasting one release, the service emits both the old name, request_latency_ms, and the new name, http_server_request_duration_seconds, recording the same observations in their respective units. In phase two, lasting as long as the dashboards and alerts take to update — typically a few weeks — every query is moved to the new name, and a recording rule can present the old name's history under the new one for long-range panels. In phase three, the old name is removed from the code, and its series go stale in the backend and age out with retention. Alerts are moved first, because an alert on a name that stops being emitted goes silent rather than firing. rename: request_latency_ms → http_server_request_duration_seconds 1 · emit both one release same observations, each in its own unit 2 · move queries alerts first, then dashboards recording rule bridges long-range history 3 · remove old drop from code old series go stale age out with retention move alerts first — an alert on a name that stops being emitted goes silent, not red an absent-data alert on the new name catches a rename that went wrong
Emit both, move every query, then remove the old name. Alerts move first because a missing metric silences them rather than triggering them.

The ordering in phase two matters. A dashboard on a name that stops being emitted goes blank, which someone notices. An alert on it goes silent, which nobody notices until the incident it should have caught. Moving alerts before anything else, and adding an absent-data rule for the new name as described in writing alert rules for Python services, makes the rename safe to finish.

Help text and descriptions

Every metric carries a description — the help string in prometheus_client, the description argument in OpenTelemetry — and it is the one piece of documentation guaranteed to travel with the metric into every backend and metric browser. A good description says what is counted or measured, where, and anything surprising about it: "Requests handled by the ASGI app, excluding /metrics and /health; latency measured to the last body byte." That sentence answers the questions a person writing a query at two in the morning actually has, and it costs nothing at runtime. Keeping descriptions accurate when behaviour changes — a new exclusion, a different measurement point — matters as much as writing them, since a wrong description is worse than a missing one.

Descriptions also help catch naming mistakes in review. A description that has to explain the name away — "latency in milliseconds, despite the suffix" — is a signal the name should change now, before anything depends on it.

Configuration options

Rule prometheus_client OpenTelemetry
Case and separator snake_case lower.dotted
Unit suffix: _seconds, _bytes unit field: s, By
Counter suffix _total (automatic) none; exporter adds
Counts of things _total on the counter {thing} annotation
Ratios _ratio, 0–1 unit 1
Service identity target labels resource attributes
Dimensions labels attributes

Verification

List the names a service actually exposes and check them against the rules:

curl -s localhost:9464/metrics | grep -v '^#' | sed 's/[{ ].*//' | sort -u

Expected Output: base-unit suffixes, _total only on counters, no service names or route names inside metric names.

http_server_request_duration_seconds_bucket
http_server_request_duration_seconds_count
http_server_request_duration_seconds_sum
orders_placed_total
process_memory_usage_bytes

A name ending in _ms, _milliseconds_seconds or containing a route or service is a candidate for renaming — before dashboards depend on it, because a rename later means changing every query and losing continuity in history.

Common mistakes

Milliseconds in one service, seconds in another. Error signature: a cross-service latency panel where one line is a thousand times the others. Root cause: non-base units. Remediation: seconds everywhere; convert in the dashboard.

Unit in the OpenTelemetry name. Error signature: request_duration_ms_milliseconds in Prometheus. Root cause: the unit in both name and field. Remediation: the unit field only.

_total on a gauge. Error signature: rate() applied to a value that goes down, producing nonsense. Root cause: the name implies a counter. Remediation: _total only on counters.

Dimensions in names. Error signature: dashboards listing twenty metric names for one quantity. Root cause: routes or targets encoded in names. Remediation: one name, labels for dimensions.

Renaming without a transition. Error signature: dashboards going blank after a deploy. Root cause: queries still using the old name. Remediation: emit both names for a period, or use recording rules to bridge.

Inconsistent label names across services. Error signature: fleet-wide queries that silently cover only some services. Root cause: status in one service, code in another. Remediation: semantic-convention label names everywhere.

Empty or generic help strings. Error signature: metric browsers full of names nobody can interpret. Root cause: descriptions left as the name repeated. Remediation: one sentence on what, where and any exclusions.

Frequently Asked Questions

Should latency be in seconds or milliseconds?

Seconds, as a floating-point value. It is the Prometheus convention and the OpenTelemetry semantic convention for durations, and mixing units across services makes every cross-service query a unit-conversion exercise. Dashboards display milliseconds when that reads better.

Why does my OpenTelemetry metric have a different name in Prometheus?

The Prometheus exporters translate names: dots become underscores, the unit is appended as a suffix in its Prometheus spelling, and counters gain _total. An instrument named http.server.request.duration with unit s becomes http_server_request_duration_seconds.

What should the _total suffix be used for?

Counters only. prometheus_client adds it automatically to Counter names, and OpenTelemetry's Prometheus exporters add it to monotonic sums. Using it on a gauge misleads anyone reading the name about how to query it.

Should the service name be in the metric name?

No. The service is a label or resource attribute added by the scrape target or the SDK resource. Metric names describe what is measured; putting the service in the name prevents one query from covering every service.

How are units written in OpenTelemetry?

As UCUM strings in the instrument's unit field: s for seconds, By for bytes, 1 for dimensionless ratios, and curly-brace annotations such as {request} or {connection} for counts of things.