Skip to content

Commit db4dc59

Browse files
committed
docs(metrics): document experimental bound instruments
1 parent 04b6a6f commit db4dc59

1 file changed

Lines changed: 91 additions & 1 deletion

File tree

docs/metrics.md

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Status: **Work-In-Progress**
1616
instruments](#reporting-measurements-via-synchronous-instruments)
1717
* [Reporting measurements via asynchronous
1818
instruments](#reporting-measurements-via-asynchronous-instruments)
19+
* [Bound Instruments (Experimental)](#bound-instruments-experimental)
1920
* [MeterProvider Management](#meterprovider-management)
2021
* [Memory Management](#memory-management)
2122
* [Example](#example)
@@ -29,7 +30,7 @@ Status: **Work-In-Progress**
2930
* [Metrics Correlation](#metrics-correlation)
3031
* [Modelling Metric Attributes](#modelling-metric-attributes)
3132
* [Common Issues Leading to Missing
32-
Metrics](#common-issues-that-lead-to-missing-metrics)
33+
Metrics](#common-issues-that-lead-to-missing-metrics)a
3334

3435
</details>
3536

@@ -241,6 +242,95 @@ fn setup_metrics(meter: &opentelemetry::metrics::Meter) {
241242
> [!NOTE] The callbacks in the Observable instruments are invoked by the SDK
242243
> during each export cycle.
243244
245+
### Bound Instruments (Experimental)
246+
247+
> [!IMPORTANT] Bound instruments are experimental. Enable
248+
> `experimental_metrics_bound_instruments` on **both** `opentelemetry` and
249+
> `opentelemetry_sdk` — if only the API crate has it enabled, `bind()` calls
250+
> compile but measurements are silently dropped. Enabling it on
251+
> `opentelemetry_sdk` transitively enables it on `opentelemetry`, so for typical
252+
> apps using the SDK, enabling it once on `opentelemetry_sdk` is sufficient.
253+
> The API may change in future releases.
254+
255+
When reporting measurements, the SDK must resolve the provided attributes to
256+
the internal aggregation state (the "tracker") for that attribute combination
257+
on every call. For hot paths that repeatedly emit measurements with the *same*
258+
attribute set, this per-call lookup can be avoided by pre-binding the
259+
attributes once and reusing the resulting bound handle.
260+
261+
Bound instruments are currently supported on `Counter` and `Histogram` via the
262+
`bind` method, which returns a `BoundCounter<T>` or `BoundHistogram<T>`
263+
respectively. Subsequent `add` / `record` calls on the bound handle skip
264+
attribute resolution and write directly to the pre-resolved tracker.
265+
266+
:heavy_check_mark: Use bound instruments on hot paths where the same attribute
267+
set is used repeatedly, and store the bound handle for reuse.
268+
269+
```rust
270+
// Requires the `experimental_metrics_bound_instruments` feature on the
271+
// `opentelemetry_sdk` crate (which transitively enables it on `opentelemetry`).
272+
use opentelemetry::KeyValue;
273+
use opentelemetry::global;
274+
275+
let meter = global::meter("my_company.my_product.my_library");
276+
let packets = meter.u64_counter("packets_processed").build();
277+
278+
// Bind once at initialization, for each well-known protocol.
279+
let tcp_packets = packets.bind(&[KeyValue::new("protocol", "tcp")]);
280+
let udp_packets = packets.bind(&[KeyValue::new("protocol", "udp")]);
281+
282+
// On the hot path, the call site already knows which handle to use —
283+
// no attribute lookup is performed.
284+
tcp_packets.add(1);
285+
udp_packets.add(1);
286+
```
287+
288+
`BoundCounter` and `BoundHistogram` are cheap to clone, so a single bound
289+
handle can be shared across threads or modules without re-binding. The
290+
underlying tracker is reclaimed when the last clone is dropped.
291+
292+
For reference, the SDK's `bound_instruments` benchmark on an Apple M4 Max with
293+
3 attributes (`method`, `status`, `path`) reports:
294+
295+
| Operation | Unbound | Bound | Speedup |
296+
| ----------------------------| --------- | --------- | ------- |
297+
| `Counter::add` (Delta) | ~50 ns | ~1.8 ns | ~28x |
298+
| `Histogram::record` (Delta) | ~58 ns | ~6.5 ns | ~9x |
299+
300+
The exact win depends on attribute count, contention, and the cost of the
301+
lookup being skipped. See
302+
[`opentelemetry-sdk/benches/bound_instruments.rs`](../opentelemetry-sdk/benches/bound_instruments.rs)
303+
to reproduce.
304+
305+
:stop_sign: Do NOT call `bind` on the hot path. A single `bind` + `add` is
306+
*more* expensive than a plain `add` — it does the same attribute lookup, plus
307+
extra allocations for the bound handle. The performance win comes only from
308+
binding once and reusing the returned handle for many measurements.
309+
310+
:stop_sign: Do NOT use bound instruments when the attribute set varies per
311+
measurement. Each unique attribute set requires its own bound handle, and
312+
holding many rarely-used handles wastes memory without any throughput benefit.
313+
314+
:stop_sign: Do NOT bind everything by default. Bound instruments are intended
315+
for a small number of well-known, high-frequency attribute combinations on hot
316+
paths. Binding broadly — especially storing handles in a user-managed map
317+
keyed by attributes — recreates the SDK's per-call lookup in your own code and
318+
just shifts the problem from the SDK to the application, often with worse
319+
results. Stick to the unbound `add` / `record` API unless you have a clear
320+
hot-path case backed by measurements.
321+
322+
> [!NOTE] Many common web-app metrics (RED/SLO style: request count, latency,
323+
> errors broken down by route, method, customer, etc.) involve highly variable
324+
> attribute values and are **not** a good fit for bound instruments. Bound
325+
> instruments shine when each call site already knows its full attribute set
326+
> at compile time / startup, so handles can be stored directly alongside the
327+
> code that uses them — no per-call map lookup needed.
328+
329+
> [!NOTE] Bound instruments do not change the SDK's [cardinality
330+
> limits](#cardinality-limits) or pre-aggregation behavior. They are purely a
331+
> performance optimization that caches the resolved tracker for a fixed
332+
> attribute set.
333+
244334
## MeterProvider Management
245335

246336
Most use-cases require you to create ONLY one instance of MeterProvider. You

0 commit comments

Comments
 (0)