SDK implementation of the Otel.API.Metrics.Meter
behaviour (metrics/sdk.md §Meter L870-L943).
Handles instrument creation with case-insensitive duplicate detection. Instruments are stored in a shared ETS table owned by the MeterProvider.
All functions are safe for concurrent use, satisfying spec
metrics/sdk.md L1351-L1352 — "Instrument — synchronous and
asynchronous instrument operations MUST be safe to be called
concurrently."
Public API
| Callback | Role |
|---|---|
create_* (counter, histogram, gauge, updowncounter, observable*) | SDK (OTel API MUST) — metrics/api.md §Instruments |
record/3 | SDK (OTel API MUST) — synchronous instrument measurement |
register_callback/5 | SDK (OTel API MUST) — async instrument registration |
enabled?/2 | SDK (OTel API MUST) — metrics/api.md §Enabled (Stable, #4787) |
Design notes
Duplicate registration
register_instrument/4 keys instruments by
{scope, downcased_name} and uses :ets.insert_new/2 so the
first registration wins. Subsequent create_* calls for the same
key return the already-stored struct unchanged. This satisfies the
case-insensitive identity requirement of
metrics/sdk.md L945-L958 "The name of an Instrument is defined to
be case-insensitive.".
The spec's SHOULD-log clauses for identifying-field conflicts
(sdk.md L904-L958, L990) are not currently emitted; a caller
that re-creates an instrument with a different kind, unit,
or description gets the original instrument back silently. This
matches the project's happy-path policy and will be revisited in
the finalization error-handling pass.
View vs advisory precedence
When both a matching View and instrument advisory parameters influence the same stream aspect, the View wins. Concretely:
- Aggregation / bucket boundaries. If a View explicitly
specifies an aggregation (e.g.
ExplicitBucketHistogram), advisory:explicit_bucket_boundariesare ignored entirely — even when the View does not supply custom boundaries (metrics/sdk.mdL1003-L1005). Advisory boundaries only apply when no View matched or the matching View uses default aggregation (stream.aggregation == nil), resolved inStream.from_view/2→Stream.resolve/1andStream.from_instrument/1. - Attribute keys.
Stream.from_view/2falls back to advisory:attributesonly when the View has no:attribute_keys.
enabled?/2 with Drop aggregation
enabled?/2 returns false only when every resolved stream (for
a registered instrument) or every matching View (for an unregistered
instrument name) uses Drop aggregation. Any non-Drop stream/view
makes the instrument enabled. This matches metrics/sdk.md L1029
and L1037 and lets user code skip measurement computation cheaply
when the pipeline would discard the value anyway.
Async cardinality — first-observed across temporalities
Spec metrics/sdk.md §"Asynchronous instrument cardinality
limits" L864-L866 SHOULD —
"Aggregators of asynchronous instruments SHOULD prefer the
first-observed attributes in the callback when limiting
cardinality, regardless of temporality."
Sync overflow (maybe_overflow/3) inspects metrics_tab,
which works as first-observed only under cumulative because
delta-temporality readers clear the table on each collect.
Async overflow (maybe_overflow_async/3) instead consults
the dedicated observed_attrs_tab ETS set, which records
every (stream, reader, attrs) triple ever observed for an
async stream. Entries survive delta resets, so the first N
attribute sets ever observed are pinned to the original
key forever; subsequent sets route to the overflow
attribute regardless of whether the metrics table has been
cleared.
The :ets.member + count_stream_keys + :ets.insert
sequence in maybe_overflow_async/3 is non-atomic. Two
callbacks racing on different new attribute sets at the
boundary current = limit - 1 could both pass the count
check and both insert, briefly exceeding the limit by one.
In practice MetricReader.collect/1 serialises callbacks
per reader, so this race only fires across multiple readers
collecting simultaneously — a corner the spec MUST at L840
(no overflow when distinct sets ≤ limit) does not strictly
bind, since at the boundary the SHOULD is already best-
effort.
Deferred Development-status features
- MeterConfig (
enabledflag). Specmetrics/sdk.mdL1029-L1037 (Status: Development on theMeterConfig.enabled=falsebullet) — when set, the Meter's instruments MUST reportenabled?/2asfalse. Not implemented — there is noMeterConfiganalogue on the meter today, so the disabled-Meter leg cannot be expressed. The Drop-aggregation leg ofenabled?/2IS honoured (above). Waits for spec stabilisation.
References
- OTel Metrics SDK §Meter:
opentelemetry-specification/specification/metrics/sdk.mdL870-L943 - OTel Metrics API §Meter:
opentelemetry-specification/specification/metrics/api.mdL156-L499
Summary
Functions
Executes all registered callbacks for the given meter config and aggregates the observations into the metrics pipeline.
Removes a previously registered callback.
Functions
@spec run_callbacks(config :: map()) :: :ok
Executes all registered callbacks for the given meter config and aggregates the observations into the metrics pipeline.
Called by MetricReader during collection. Two callback shapes are supported, distinguished by the shape marker stored in each ETS entry:
:single— inline callback registered viacreate_observable_*/5→store_callback/4. Callback returns[Measurement.t()]permetrics/api.mdL441-L442.:multi— callback registered viaregister_callback/5. Callback returns[{Instrument.t(), Measurement.t()}]permetrics/api.mdL1302-L1303 + L452-L453 MUST (multi-instrument callbacks MUST distinguish the instrument for each observation).
Internally both shapes are normalised to the multi-shape
(a list of pairs) so apply_observations/2 has a single
code path that looks up streams per-instrument.
@spec unregister_callback(state :: {reference(), :ets.table()}) :: :ok
Removes a previously registered callback.
Called indirectly via Otel.API.Metrics.Meter.unregister_callback/1,
which unwraps the opaque {module, state} handle and passes the
inner state {ref, callbacks_tab} here.