Build1 publisher3 min readPublished
Bounded label registries keep agent metrics from adding a time series per run
Agent telemetry guidance on dev.to moves run IDs, prompts and tool arguments off metric labels and into traces, capping a sample counter at 16 series. Teams that adopt it review each new label value like a schema change and rely on sampled traces for per-run evidence.
The Engineer · Build desk

What happened
- A dev.to post shows an agent counter tagged with run ID, user ID, prompt, stringified tool arguments and error text, and calls it an observability bill waiting to happen.
- Run IDs, timing, parent-child structure and links to protected evidence go into traces, which are sampled and retained separately from metrics.
- A label registry in the post allows three workflows and three outcomes and maps any other value to "other" before it reaches the metric.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Any counter labelled with a run ID grows by at least one series per run, so its storage footprint scales with traffic instead of with the number of workflows.
- cost Holding the example counter to 16 series costs a review step every time someone wants to add a workflow, outcome or error class.
- decision Unregistered MCP tools collapse to "other" on dashboards, so debugging a specific remote tool depends on its trace having been sampled.
- exposure Correlation IDs placed on broadly exported metrics reach readers who would be kept out of a restricted trace store.
Every field on that first counter is something a person debugging a failed run would want. It gets past code review for the same reason. In metric systems that key a series on the full attribute set, the counter writes a new time series for each distinct combination of its labels [2]. A run ID differs on every run, so the counter adds at least one series per run before the prompt or the stringified tool arguments multiply it further [19]. The post lists prompts, run IDs, user IDs, URLs, document IDs, tool arguments and raw errors as effectively unbounded, and notes that agent workloads produce all of them naturally [3].
The fix is a split by signal. Metric labels come from small vocabularies the application owns: workflow, environment, outcome, model family and policy result [4]. Those answer aggregate questions, such as whether failures are rising for one workflow or policy blocks jumped after a release. They cannot point at a single run [5]. The trace carries the run ID, parent-child structure, timing and links to protected evidence, and it is sampled and retained on its own terms [6].
The budget is a multiplication done before deployment: the product of allowed values per label, plus a rule for anything outside the list [9]. The post's registry allows three workflows and three outcomes and maps everything else to "other" [10]. With the fallback counted, that caps the counter at 4 x 4, or 16 series, however many runs execute [11]. The post puts the collector or backend limit last. The instrumentation library should normalize or drop unsafe values first and count the drops under one bounded reason code [9].
The scheme breaks where a vocabulary grows by itself. Raw error messages carry IDs, URLs, payload fragments and provider wording that changes between versions. The post counts four error classes and keeps the original text in a protected diagnostic event [12]. "Keep the vocabulary versioned. If every new message becomes a new class, cardinality returns under another name," the post warns [13]. Tool names have the same failure: a model-generated or remote MCP tool name can be unbounded, so unknown tools become "other" on the metric while the exact name stays in the trace. Provider version strings collapse to an approved model family on metrics, with the exact model ID kept in the trace [14].
Traces get limits too. OpenTelemetry's semantic-convention guidance recommends low-cardinality span names and warns against unbounded attribute values, very large strings and huge arrays. A span named with the full prompt is hard to aggregate and can leak data [7]. The post's preferred span is named agent.tool.execute, with the tool name, workflow, outcome and error type as attributes [8]. Retrieval gets the same treatment: count documents and bytes, and keep queries, titles, URLs and chunk text off metric labels [18].
The split also draws an access line. A correlation ID can be acceptable in a restricted trace store and still be wrong as a metric label exported broadly [15]. In my view this is the right tradeoff for any team whose metrics are read by more people than its traces. The cost is process. Registry additions get reviewed like schema changes, and a remote tool name or a model-produced reason cannot expand the list without that review [16].
The post does not include a measured series count or a bill from a production system, and it scopes its time-series claim to "many metric systems" [2]. What the budget saves in money depends on how a given backend charges for active series. The post's instruction is to monitor active series growth and to reject or truncate oversized fields at the instrumentation boundary [17].
What to watch
- Production measurements of active series counts before and after a label registry is introduced, which the post does not provide.
- How OpenTelemetry's gen_ai semantic conventions settle agent span names and attributes, since the post tells teams to version the convention they implement.
- Whether agent frameworks start enforcing bounded label values in their instrumentation libraries, ahead of collector or backend limits.