Status: proposal / request for comment
Motivation
Core's Prometheus metrics use a deliberately low-cardinality label set: namespace, task_queue, workflow_type, activity_type, poller_type. Per-execution identifiers — run_id, workflow_id, activity_id — are absent, correctly, since as labels they would multiply series count by every execution the worker handles.
The cost is that latency histograms are unactionable at the tail. When temporal_workflow_task_execution_latency p99 jumps, the metrics establish that something was slow and offer no path to what. Exemplars are the OpenMetrics feature for exactly this: a bounded number of high-cardinality sample pointers attached to buckets, stored outside the series and ignored by aggregation.
For Core this is unusually cheap, because the identifiers are already in scope at the record sites. In crates/sdk-core/src/worker/workflow/managed_run.rs, the call recording workflow task latency sits two lines from an existing use of self.wfm.machines.run_id. No propagation, no changes to the worker/client boundary.
Proposal
- An
exemplars option on PrometheusExporterOptions, default off. When off, exposition output is byte-identical to today.
- Core attaches
run_id where it already has it — starting with the workflow task and end-to-end latency histograms.
- Lang SDKs can supply exemplar labels for custom metrics, via an additive trait method with a defaulted body, so existing
CoreMeter implementations compile unchanged and ignore exemplars.
- OpenMetrics exposition, content-negotiated. Exemplars have no representation in text format 0.0.4, so emit OpenMetrics on
Accept: application/openmetrics-text when the option is on; every other request keeps the current TextEncoder path.
Non-goals: trace_id exemplars — Core has no distributed tracing, tracing-opentelemetry is not a dependency and SpanContext/trace_id appear nowhere in crates/common/src/telemetry/, so SDKs would supply these via item 3; gauges, which OpenMetrics disallows; and the OTel and buffered meters, which ignore exemplars via the defaulted method.
Implementation notes
The prometheus crate's histogram type has no exemplar slot, and we depend on it with default-features = false, so there is no Exemplar type in scope either. Cheapest path is a side table on the custom Registry in prometheus_meter.rs, keyed by family name, label set and bucket index, plus a small OpenMetrics encoder. Switching to prometheus-client would provide both natively but means rewriting ~900 lines, including the custom registry that permits identical names with differing label sets.
Exemplar label validation (OpenMetrics: name charset, combined length ≤128 UTF-8 characters) belongs in Core so there is one implementation for all SDKs. Invalid exemplars get dropped with a warning and the observation still recorded — instrumentation must not break the caller.
What I would like decided
- Which internal metrics get automatic exemplars, and carrying which identifier?
- Should automatic
run_id exemplars be implied by exemplars = true, or be a separate opt-in? Some operators will want the mechanism for their own custom metrics only.
- OpenMetrics text, or enable the
prometheus crate's protobuf feature instead? Protobuf avoids hand-writing an encoder but is not widely enabled on scrapers.
Two smaller notes: exemplar storage is still feature-gated on the Prometheus server, so operators must opt in there as well; and OpenMetrics requires counters to end in _total, which conflicts with counters_total_suffix defaulting to false — simplest is to force the suffix on OpenMetrics responses only.
Status: proposal / request for comment
Motivation
Core's Prometheus metrics use a deliberately low-cardinality label set:
namespace,task_queue,workflow_type,activity_type,poller_type. Per-execution identifiers —run_id,workflow_id,activity_id— are absent, correctly, since as labels they would multiply series count by every execution the worker handles.The cost is that latency histograms are unactionable at the tail. When
temporal_workflow_task_execution_latencyp99 jumps, the metrics establish that something was slow and offer no path to what. Exemplars are the OpenMetrics feature for exactly this: a bounded number of high-cardinality sample pointers attached to buckets, stored outside the series and ignored by aggregation.For Core this is unusually cheap, because the identifiers are already in scope at the record sites. In
crates/sdk-core/src/worker/workflow/managed_run.rs, the call recording workflow task latency sits two lines from an existing use ofself.wfm.machines.run_id. No propagation, no changes to the worker/client boundary.Proposal
exemplarsoption onPrometheusExporterOptions, default off. When off, exposition output is byte-identical to today.run_idwhere it already has it — starting with the workflow task and end-to-end latency histograms.CoreMeterimplementations compile unchanged and ignore exemplars.Accept: application/openmetrics-textwhen the option is on; every other request keeps the currentTextEncoderpath.Non-goals:
trace_idexemplars — Core has no distributed tracing,tracing-opentelemetryis not a dependency andSpanContext/trace_idappear nowhere incrates/common/src/telemetry/, so SDKs would supply these via item 3; gauges, which OpenMetrics disallows; and the OTel and buffered meters, which ignore exemplars via the defaulted method.Implementation notes
The
prometheuscrate's histogram type has no exemplar slot, and we depend on it withdefault-features = false, so there is noExemplartype in scope either. Cheapest path is a side table on the customRegistryinprometheus_meter.rs, keyed by family name, label set and bucket index, plus a small OpenMetrics encoder. Switching toprometheus-clientwould provide both natively but means rewriting ~900 lines, including the custom registry that permits identical names with differing label sets.Exemplar label validation (OpenMetrics: name charset, combined length ≤128 UTF-8 characters) belongs in Core so there is one implementation for all SDKs. Invalid exemplars get dropped with a warning and the observation still recorded — instrumentation must not break the caller.
What I would like decided
run_idexemplars be implied byexemplars = true, or be a separate opt-in? Some operators will want the mechanism for their own custom metrics only.prometheuscrate'sprotobuffeature instead? Protobuf avoids hand-writing an encoder but is not widely enabled on scrapers.Two smaller notes: exemplar storage is still feature-gated on the Prometheus server, so operators must opt in there as well; and OpenMetrics requires counters to end in
_total, which conflicts withcounters_total_suffixdefaulting to false — simplest is to force the suffix on OpenMetrics responses only.