Repository navigation
Add metric exemplar support - #67
Merged
Merged
Conversation
Instruments now record exemplars (a span, its measurement value, and the aggregation attributes) via addExemplar(span:value:attributes:), stored thread-safely alongside their values and drained on snapshotAndReset. Tracer+Metrics records an exemplar in the retire callback where the real measurement value is known. The exporter attaches exemplars to the matching data point (by aggregation key) and, via Exporter.exemplarSamplingDecision, lets a reporter gate attachment on the trace sampling decision. ExampleReporter demonstrates reusing its per-span decision for exemplars. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a DurationUnit enum (seconds, milliseconds, microseconds, nanoseconds) to reportAsDurationHistogramMetric, defaulting to milliseconds. The unit drives both the metric's UCUM symbol (s/ms/µs/ns) and the conversion applied to each recorded span duration. Collapse the four Duration conversion properties into a single wholeUnits helper that derives the half-unit rounding threshold from the per-unit amount, and add asMicroseconds/asSeconds. Also rename the exemplar helper's parameter key -> metricAttributes, and make the duration histogram tests deterministic by asserting against the span's own frozen elapsed (the value is recorded during end(), so wall-clock time can't be cancelled after the fact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ladd
marked this pull request as ready for review
July 17, 2026 19:38
jparise
approved these changes
Jul 17, 2026
Co-authored-by: Jon Parise <jon.parise@airbnb.com>
bachand
reviewed
Jul 20, 2026
| timeUnixNano: timeUnixNano, | ||
| asDouble: doubleValue, | ||
| asInt: intValueString, | ||
| exemplars: nil, // no exemplar support yet |
Comment on lines
+160
to
+171
| // Decision drops everything: no exemplars are attached. | ||
| let noneExporter = Exporter(timeReference: timeReference, exemplarSamplingDecision: { _ in false }) | ||
| #expect(noneExporter.convertToOTLP(exemplars: counter.exemplars, metricAttributes: [:]) == nil) | ||
|
|
||
| // Decision keeps only the sampled span. | ||
| let selectiveExporter = Exporter(timeReference: timeReference, exemplarSamplingDecision: { $0.id == sampledSpan.id }) | ||
| let attached = try #require(selectiveExporter.convertToOTLP(exemplars: counter.exemplars, metricAttributes: [:])) | ||
| #expect(attached.map(\.spanId) == [sampledSpan.id]) | ||
|
|
||
| // Default decision attaches every exemplar. | ||
| let defaultExporter = Exporter(timeReference: timeReference) | ||
| #expect(defaultExporter.convertToOTLP(exemplars: counter.exemplars, metricAttributes: [:])?.count == 2) |
Comment on lines
+113
to
+115
| // Exemplars move to the snapshot; the live instrument is reset. | ||
| #expect(snapshot.exemplarSpans.map(\.id) == [span.id]) | ||
| #expect(counter.exemplarSpans.isEmpty) |
Comment on lines
+13
to
+14
| /// Reporters typically drive this from the current trace sampling decision, so that exemplars — which | ||
| /// link a metric data point back to a trace — are only attached when that trace is sampled. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds OpenTelemetry metric exemplar support — a sampled measurement plus a link back to the trace that produced it.
addExemplar(span:value:attributes:), stored thread-safely (Mutex<[Exemplar<T>]>) alongside their values and drained onsnapshotAndReset().Tracer+Metricsrecords the exemplar inside the span retire callback, where the real measurement value (and collected attributes) is known.asDouble/asInt, span/trace IDs, andfilteredAttributes.Exporter.exemplarSamplingDecisionlets a reporter gate exemplar attachment on the trace sampling decision.ExampleReporterdemonstrates reusing its per-span decision.parentIdpublic for ease of integration testing.addExemplarstays on the concrete generic instruments (likeadd/record/observe);exemplarSpansremains on theInstrumentprotocol for type-erased read access.Testing
swift test— all suites pass, including new coverage for exemplar wiring, attribute matching, snapshot/reset, and the sampling decision.@jparise
@bachand