Documentation
¶
Overview ¶
Package telemetry is the OpenTelemetry seam every other package instruments through: a Config carrying the providers, a Vocabulary that bounds an attribute to a closed set, and a RoundTripper that measures an outbound call.
Why ¶
Phase 9 of the plan of record needs the library to be observable without deciding anything for the consumer. Two rules follow from that, and this package exists to hold both in one place rather than repeat them in the eight Configs that will embed it.
The first is that a library instruments through the API and never the SDK. OpenTelemetry's own guidance for instrumentation authors is that they "MUST NOT directly reference any SDK package of any kind, only the API", because the SDK is the application's choice: readers, exporters, samplers, resource attributes and cardinality limits all belong to whoever runs the process. So the dependency is exactly three stable v1.x modules — otel, otel/metric and otel/trace — and a nil provider means no-op rather than "use the global", because reading the global would let an unrelated dependency of the consumer switch this library on. Configure nothing and this library records nothing (decision record 0040).
The second is that no string a device sent may become a metric attribute. A MessageType, a RequestType or a declaration reason code all arrive from the wire, and an attribute built straight from one is a time series count that a single malformed enrollment can grow without limit; an SDK answers that by discarding the overflow into a synthetic series, so the metric stops being trustworthy before anyone notices. Vocabulary is the bound: it is built from a set fixed at compile time, usually one of the generated registries in schema/, and maps everything else to OtherValue. Values that cannot be bounded — a UDID, a serial, a declaration identifier, an ErrorChain description — belong on a span, never on a metric.
RoundTripper applies both rules to outbound HTTP. It records only the method, the server address and port, the status and the error type, and deliberately nothing from the URL path, query or body: an APNs push is a POST to /3/device/<device token>, so a path recorded anywhere in telemetry publishes the credential that wakes a device.
The logs bridge is deliberately absent. Metrics and traces are stable v1.x; otel/log is v0.x with a policy that "anything MAY change at any time", so taking it into library packages would put a permanently unstable dependency in front of every consumer. The library keeps emitting slog with the Context variants it already uses, and internal/app — the reference server, which is allowed to be opinionated — wires the bridge.
Fakes that record what was emitted are telemetry/telemetrytest.
References ¶
- Decision record 0040: docs/research/decisions/0040-opentelemetry-seam.md
- Decision record 0037: docs/research/decisions/0037-event-sinks-and-redaction.md
- Plan of record: docs/research/implementation_plan.md (phase 9)
- OpenTelemetry: https://opentelemetry.io/docs/languages/go/libraries/
- OpenTelemetry: https://opentelemetry.io/docs/specs/semconv/http/http-metrics/
- OpenTelemetry: https://opentelemetry.io/docs/specs/otel/versioning-and-stability/
- Apple: https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns
Index ¶
Constants ¶
const ( AttrHTTPRequestMethod = "http.request.method" AttrHTTPResponseStatusCode = "http.response.status_code" AttrServerAddress = "server.address" AttrServerPort = "server.port" AttrErrorType = "error.type" )
Attribute keys from OpenTelemetry's HTTP semantic conventions.
Only the stable subset is used. They are named here rather than imported from a semconv package: those are published one import path per specification release (.../semconv/v1.42.0, v1.43.0, and so on), so an import pins a spec version in our source and every otel upgrade is then a choice between editing the path and going stale. It saves no dependency — otel/trace already links a semconv package — but the five names below are in the stable set, whose spelling cannot change without a major version of the specification, so pinning buys nothing.
const MetricHTTPClientDuration = "http.client.request.duration"
MetricHTTPClientDuration is the stable OpenTelemetry instrument for an outbound request, measured in seconds.
const OtherValue = "_OTHER"
OtherValue replaces any value outside a Vocabulary. It matches the convention OpenTelemetry's own HTTP semantic conventions use for an unrecognised request method, and the leading underscore keeps it from colliding with anything Apple sends.
const ScopeRoot = "github.com/deploymenttheory/go-apple-dm"
ScopeRoot is the module path every instrumentation scope is rooted at.
Variables ¶
This section is empty.
Functions ¶
func RoundTripper ¶
func RoundTripper(next http.RoundTripper, cfg Config, opts ...Option) http.RoundTripper
RoundTripper wraps next so every outbound request records http.client.request.duration and, when a TracerProvider is configured, one span. A nil next uses http.DefaultTransport.
It records the request method, the server address and port, the response status and the error type — and deliberately nothing drawn from the URL path, query or body, on the span as much as on the metric. That is not only a cardinality rule: an APNs push is a POST to /3/device/<device token>, so a path recorded anywhere in telemetry publishes the credential that wakes a device to every backend the traces reach.
Errors are returned unchanged, so the wrapper is transparent to callers.
func Scope ¶
Scope returns the instrumentation scope name for a package, which OpenTelemetry defines as the fully qualified name of the instrumenting library: Scope("dep") is the scope for package dep.
An empty pkg returns ScopeRoot, so a caller that has no sub-package to name still produces a valid scope rather than a trailing slash.
Types ¶
type Config ¶
type Config struct {
// MeterProvider supplies meters. Nil records no metrics.
MeterProvider metric.MeterProvider
// TracerProvider supplies tracers. Nil records no spans.
TracerProvider trace.TracerProvider
}
Config carries the OpenTelemetry providers a package should instrument through. It is embedded in each package's own Config beside Logger, Clock and Bus, and every field is optional.
A nil provider means no-op, not "use the global". Reading otel.GetMeterProvider() would let a consumer's unrelated global configuration silently switch this library on, which is the opposite of the contract: unless a consumer passes a provider here, this library records nothing and allocates nothing.
func (Config) Metrics ¶
Metrics reports whether a consumer asked for metrics. Instrument construction is cheap but not free, so a package that would build many instruments may skip the work entirely.
type Option ¶
type Option func(*rtOptions)
Option configures a RoundTripper.
func WithAttributes ¶
WithAttributes adds constant attributes to every measurement. They must be fixed at configuration time: an attribute whose value varies per request multiplies the series count by its range, and one that varies per device is unbounded.
type Vocabulary ¶
type Vocabulary struct {
// contains filtered or unexported fields
}
Vocabulary is a closed set of values for one metric attribute.
It exists because almost every interesting label on this library's metrics starts life as a string a device sent us: a MessageType, a RequestType, a declaration reason code. A metric attribute built straight from one of those is an unbounded time series that a single malformed or hostile enrollment can grow without limit, and OpenTelemetry's SDK answers that by discarding the overflow into a synthetic series — so the metric stops being trustworthy long before anyone notices.
A Vocabulary is built once from a set fixed at compile time, usually one of the generated registries in schema/, and maps everything else to OtherValue. A caller can then label freely without auditing the call site.
The zero value is not usable; build one with NewVocabulary. A Vocabulary is immutable after construction and safe for concurrent use.
func NewVocabulary ¶
func NewVocabulary(key string, values []string) *Vocabulary
NewVocabulary returns a Vocabulary for an attribute key over a closed set of values. Duplicate values are collapsed; an empty set maps everything to OtherValue, which is a usable if uninformative attribute rather than an error, because a generated registry that turns out to be empty should not take a server down.
func (*Vocabulary) Allows ¶
func (v *Vocabulary) Allows(value string) bool
Allows reports whether a value is in the vocabulary. Use it to decide whether a value is worth putting on a span, where cardinality is not a concern but a fabricated value is still worth knowing about.
func (*Vocabulary) Attr ¶
func (v *Vocabulary) Attr(value string) attribute.KeyValue
Attr returns the attribute for a value, replacing anything outside the vocabulary with OtherValue.
func (*Vocabulary) Cardinality ¶
func (v *Vocabulary) Cardinality() int
Cardinality is the number of series one Vocabulary can produce for its key, which is its size plus OtherValue. A caller multiplying the cardinality of every attribute on an instrument gets that instrument's worst case, which is the number worth checking against a backend's limit before shipping.
func (*Vocabulary) Values ¶
func (v *Vocabulary) Values() []string
Values returns the vocabulary in sorted order, excluding OtherValue. It is a copy: a caller cannot widen the set through it.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package telemetrytest provides recording OpenTelemetry providers, so a test can assert what an instrument emitted.
|
Package telemetrytest provides recording OpenTelemetry providers, so a test can assert what an instrument emitted. |