# Changelog for hs-opentelemetry-api
## Unreleased
## 1.0.0.0 - 2026-05-29
### Full Spec conformance against 1.55.0
- **`InstrumentationScope` type alias added.**
The OTel spec renamed "Instrumentation Library" to "Instrumentation Scope".
`InstrumentationScope` is now a type alias for `InstrumentationLibrary`, and
`instrumentationScope` is the preferred constructor. The underlying type
retains the old name for backwards compatibility.
Spec: <https://opentelemetry.io/docs/specs/otel/common/instrumentation-scope/>
- **`LogRecordExporter.forceFlush` now returns `IO FlushResult`.**
Previously returned `IO ()`. The spec says ForceFlush SHOULD let the
caller know whether it succeeded, failed, or timed out.
Spec: <https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecordexporter>
- **`shouldSample` now receives the `InstrumentationScope`.**
The `Sampler` type's `CustomSampler` constructor now takes the tracer's
`InstrumentationLibrary` as a final parameter. This is a breaking change
for custom sampler implementations.
Spec: <https://opentelemetry.io/docs/specs/otel/trace/sdk/#shouldsample>
- **`ImmutableLogRecord` internal fields switched to `UMaybe`.**
The `logRecordTimestamp`, `logRecordTracingDetails`, `logRecordSeverityText`,
`logRecordSeverityNumber`, and `logRecordEventName` fields now use unboxed
optionals for lower allocation on the emit hot path. External
`LogRecordArguments` fields remain as `Maybe`.
### Performance
Major performance rework across the tracing and metrics hot paths. The previous
release (`origin/main`) used `System.Random.Stateful` for ID generation,
`IORef ImmutableSpan` with a flat mutable record for span state,
`System.Clock.TimeSpec` for timestamps, Haskell-side hex encoding, and
`http-types`/`case-insensitive`/`binary` as transitive dependencies. All of
these have been replaced:
- **Span representation split**: `ImmutableSpan` identity fields (trace/span ID,
kind, start time) are now immutable and accessed without touching any `IORef`;
only mutable state (`hotName`, `hotEnd`, `hotAttributes`, etc.) goes through a
single `IORef SpanHot`. Eliminates an indirection on every
`getSpanContext`/`isRecording` call.
- **Unboxed TraceId/SpanId**: two `Word64` fields in registers instead of a
heap-allocated pinned `ShortByteString`. Eliminates allocation on every span.
- **Thread-local xoshiro256++ RNG**: replaced `System.Random.Stateful` (Haskell
`random` package) with thread-local xoshiro256++ implemented in C, seeded once
from the platform CSPRNG (`arc4random_buf` / `getrandom`). Zero contention,
zero syscalls, zero Haskell allocation after initial seed. Dropped the `random`
dependency.
- **Timestamp FFI**: `Timestamp` is now `Word64` nanoseconds. Direct
`clock_gettime` C FFI call bypasses the `clock` package's
`alloca`/`errno`/`Storable` overhead. OTLP serialization is zero-cost
(`coerce`). Dropped the `clock` dependency.
- **C hex encoding**: trace/span ID hex via SWAR in C (`hs_otel_hex.c`),
avoiding intermediate `ByteString` allocations from the old Haskell encoder.
- **No-op fast path**: `inSpan` skips `mask`/`bracketError`/context
modification entirely when no processors are registered.
- **`bracketError` elimination**: inlined the `mask $ \restore ->` pattern into
`inSpan`, eliminating a 4-tuple allocation, `uninterruptibleMask_`, and `try`
that the previous generic `bracketError` helper required.
- **Thread-local context rewrite**: the previous `thread-utils-context` V1
stored contexts in 32-stripe `IntMap`s with CAS (`casArray#` + `yield#`
retry) on every write. Every `getContext` call allocated a `ThreadId` box
via `myThreadId#`, crossed the FFI boundary to `rts_getThreadId`, then did
an O(log n) `IntMap.lookup` inside the stripe. Every `adjustContext` /
`attachContext` additionally CAS'd the entire stripe `IntMap`, with
`yield#`-based spin on contention. `lookupSpan` went through
`Data.Vault.Strict` (a `HashMap` keyed by `Data.Unique`).
The new implementation replaces all of this with a flat open-addressed hash table
backed by `MutableByteArray#` keys and `MutableArray#` values, where each
thread gets its own `IORef`. Hot-path reads and writes now go directly through
that `IORef` with zero contention. CAS is only used for
thread registration (once per thread lifetime). Two custom CMM primops
(`stg_getCurrentThreadId`, `stg_probeThreadSlot`) fuse thread ID retrieval
with the table probe in a single STG call, eliminating the `myThreadId#`
box allocation and `rts_getThreadId` FFI call entirely. The OTel wrapper for `Context` itself
now has dedicated unboxed slots for `Span` and `Baggage`,
replacing `Data.Vault.Strict` lookups with O(1) pattern matches.
Result: `getContext` dropped from 17.3 ns to 2.9 ns (6x), `lookupSpan`
from 10.0 ns to 0.6 ns / 0 B (17x).
- **CAS `yield#` removal**: removed `yield#` from the CAS failure path in
`casModifyIORef_`; its presence prevented GHC from optimizing the uncontended
success path.
- **Cached attribute limits**: pre-resolved from `TracerProvider` onto `Tracer`
at `makeTracer` time, eliminating repeated pointer chasing on every attribute
operation.
- **Deferred caller attributes**: source-location attributes passed lazily to
`createSpanHelper`, only forced when the span is actually recorded.
- **INLINE audit**: ~30 hot-path functions annotated; `shouldSample` split into
an inline wrapper + NOINLINE complex path so GHC can perform case-of-case at
call sites.
- **`AttrsBuilder`**: church-encoded attribute builder that folds directly into
the span's `HashMap`, avoiding intermediate list/tuple allocation.
search for histogram bucket index, `AtomicBucketArray` (single
`MutableByteArray#` with `fetchAddIntArray#`), `OptionalDouble` for histogram
min/max.
- **Dependency removals**: dropped `random`, `clock`, `http-types`,
`case-insensitive`, `binary`, `charset`, `regex-tdfa` from the API package.
Current benchmark results (GHC 9.10, `-O1`, aarch64-osx, `-N1 -A32m`):
| Operation | Time | Allocated |
|---|---|---|
| `inSpan` no-op (no processors) | 13.6 ns | 15 B |
| `inSpan` active (skip callerAttrs) | 218 ns | 1.2 KB |
| `inSpan` active | 445 ns | 2.5 KB |
| bare span (create+end) | 209 ns | 1.2 KB |
| HTTP span (3 attrs) | 410 ns | 2.5 KB |
| DB span (5 attrs) | 520 ns | 3.3 KB |
| 3-deep nested spans | 683 ns | 3.7 KB |
| `getContext` | 2.9 ns | 15 B |
| `lookupSpan` | 0.6 ns | 0 B |
| SpanId gen (xoshiro) | 3.0 ns | 0 B |
| TraceId gen (xoshiro) | 5.8 ns | 0 B |
Head-to-head comparison (same benchmark code, same machine, GHC 9.10,
`-O1 -N1 -A32m`):
| Operation | origin/main | Current | Speedup |
|---|---|---|---|
| `createSpan` no-op | 39.7 ns / 191 B | 13.6 ns / 15 B | **2.9x / 12.7x** |
| `inSpan` no-op | 316 ns / 1,678 B | 13.6 ns / 15 B | **23x / 112x** |
| `createSpan+endSpan` no-op | 593 ns / 1,846 B | 441 ns / 1,095 B | **1.3x / 1.7x** |
The `inSpan` no-op improvement is the most representative: it's the path
every instrumented function takes when the SDK is not installed or has no
processors. The old version paid for `mask`, context read/write, and
`System.Random` ID generation even on the no-op path; the new version
short-circuits all of that.
Cross-language comparison (bare span create+end, no attributes, AlwaysSample):
| Language | Time | Source |
|---|---|---|
| **Haskell** | **209 ns** | This release (tasty-bench, aarch64-osx) |
| **Go** | ~279 ns | [open-telemetry/opentelemetry-go#6730](https://github.com/open-telemetry/opentelemetry-go/pull/6730) (StartEndSpan/AlwaysSample, May 2025) |
| **Rust** | ~349 ns | [open-telemetry/opentelemetry-rust#1101](https://github.com/open-telemetry/opentelemetry-rust/pull/1101) (basic span no attrs, always-sample, Jun 2023) |
Haskell's bare span is **1.3x faster than Go** and **1.7x faster than Rust**
on the equivalent workload. The `inSpan` wrapper adds `mask`/`restore` for
async-exception safety and TLS context management, bringing the total to
218 ns without caller attributes or 445 ns with automatic `code.*` source
location attributes (which other SDKs do not include by default).
Note: cross-language numbers are from different machines and compilers, so
ratios are approximate. The Go and Rust numbers are from their own CI /
maintainer benchmarks on x86-64 Linux.
### Bug fixes
- **`addAttributes` now correctly overwrites existing keys.**
`H.union` argument order was reversed, causing existing attribute values to
silently take precedence over new ones. New values now win on key conflict,
matching `addAttribute` behavior and spec intent.
- **`traceIdRatioBased` description now always uses `TraceIdRatioBased{ratio}` format.**
Previously, `traceIdRatioBased 1.0` returned `alwaysOn` whose description was
`"AlwaysOnSampler"`, violating the spec's MUST requirement. The ratio is now
clamped to `[0, 1]` and the description always follows the spec format.
### Spec conformance (SHOULD-level)
- **`LoggerProvider` shutdown suppresses processor dispatch.**
Added `loggerProviderIsShutdown` flag. After `shutdownLoggerProvider`,
`emitLogRecord` still returns a `ReadWriteLogRecord` but skips calling
processors. `loggerIsEnabled` now returns `IO Bool` and accounts for
shutdown state.
- **`Baggage` `insertChecked` enforces W3C size limits.**
New `insertChecked :: Token -> Element -> Baggage -> Either InvalidBaggage Baggage`
enforces the 180-member limit (W3C ABNF grammar) and 8192-byte total
serialized size limit. `InvalidBaggage` now derives `Show, Eq`.
### Breaking changes
- **`createLoggerProvider` is now monadic (`MonadIO m => ... -> m LoggerProvider`).**
Required to safely allocate the internal shutdown `IORef`. Existing `let`
bindings need to become `<-` bindings.
- **`loggerIsEnabled` now returns `IO Bool` instead of `Bool`.**
Checks the provider shutdown flag, which requires reading the `IORef`.
- **`propagatorNames` renamed to `propagatorFields`.**
The `Propagator` record field is now called `propagatorFields` to match the
OpenTelemetry spec's `Fields` method. Values are actual header names (e.g.
`["traceparent", "tracestate"]`), not display names.
- **New `TextMapPropagator` type alias.**
`type TextMapPropagator = Propagator Context RequestHeaders RequestHeaders`
is now exported from `OpenTelemetry.Propagator`.
- **Global `TextMapPropagator` API.**
`getGlobalTextMapPropagator` and `setGlobalTextMapPropagator` provide a
spec-conformant global propagator. Defaults to no-op per spec. The SDK
sets this during initialization. Instrumentation libraries should prefer
the global propagator over `getTracerProviderPropagators`.
- **`SemanticsOptions` is now opaque with generalized stability lookup.**
Instead of a record with `httpOption` and `databaseOption` fields,
`SemanticsOptions` now stores the parsed env var values as a set. Use the new
`lookupStability :: Text -> SemanticsOptions -> StabilityOpt` function to query
any signal key (e.g. `"http"`, `"database"`, `"messaging"`, `"rpc"`). The
convenience functions `httpOption` and `databaseOption` still work as before.
`HttpOption` is now a type alias for the renamed `StabilityOpt` data type.
Third-party instrumentation libraries can now participate in the
`OTEL_SEMCONV_STABILITY_OPT_IN` mechanism without modifying this module.
### Bug fixes
- **Fix: `setStatus` merge semantics.** Previously used `max` on an `Ord SpanStatus`
instance to merge statuses, which broke Error-over-Error (last-writer-wins) regardless
of how `Ord` was defined. Now uses an explicit `mergeStatus` function implementing the
three spec rules: Ok is final, Unset is ignored, everything else is last-writer-wins.
The `Ord` instance is now lawful (EQ for Error/Error) and only represents the class
hierarchy (Ok > Error > Unset), not merge logic.
- **Fix: `forceFlushTracerProvider` leaked async threads on timeout.** Outstanding
processor flush asyncs were never cancelled when the timeout fired. Now calls
`mapM_ cancel jobs` on the `Nothing` (timeout) branch.
- **Fix: `isRecording` returned `True` for `FrozenSpan`.** `FrozenSpan` is an
already-completed immutable span (used for links/export). It is not recording.
Now returns `False`, aligning with `whenSpanIsRecording`.
- **Fix: `setGlobalTracerProvider` used non-atomic `writeIORef`.** Concurrent reads
could see torn state. Now uses `atomicWriteIORef`.
- **Fix: `setGlobalMeterProvider` and `setGlobalLoggerProvider` used non-atomic
`writeIORef`.** Same rationale as the tracer provider; now uses
`atomicWriteIORef`.
- **Fix: noop observable instruments reported `enabled = True`.** The no-op
`Meter` now reports `False` from observable `*Enabled` actions so callers can
skip expensive measurement callbacks when no SDK is installed (aligned with
synchronous noop instruments).
- **Fix: span mutation functions (`addAttribute`, `addAttributes`, `addEvent`,
`addLink`, `setStatus`, `updateName`) used non-atomic `modifyIORef'`.**
Concurrent calls could race and silently drop updates (e.g. lost events
under concurrent `addEvent`). All now use `atomicModifyIORef'`, matching
`endSpan` which was already atomic. Also fixed `withCarryOnProcessor` in
`OpenTelemetry.Contrib.CarryOns`.
- **Fix: log record mutation (`addAttribute`, `addAttributes`) used non-atomic
lazy `modifyIORef`.** Same race condition as span mutations, plus thunk
buildup from the lazy variant. `modifyLogRecord` and `atomicModifyLogRecord`
now both use `atomicModifyIORef'` (strict and atomic).
- **Fix: `forceFlushLoggerProvider` leaked async threads on timeout.** Same
bug as `forceFlushTracerProvider`: processor flush asyncs were never
cancelled when the timeout fired. Now calls `mapM_ cancel jobs`.
- **Fix: `shutdownLoggerProvider` aborted on first processor failure.** Used
`wait` which re-throws on async exception, causing remaining processors to
be skipped. Now uses `waitCatch` so all processors get a chance to shut down.
- **Fix: `Dropped` / no-processor spans discarded parent `TraceState`.** When
creating a child span with a `Dropped` parent, or when no processors are
configured, `traceState` was forced to `TraceState.empty`. Now inherits the
parent's `traceState`, preserving vendor data in W3C `tracestate`.
- **Fix: `shutdownTracerProvider` was sequential.** Each processor shutdown had to
complete before the next started. Now launches all shutdowns concurrently and
waits for all via `waitCatch`.
### ReadableLogRecord true snapshot
- `ReadableLogRecord` is now a `data` type holding a snapshotted `ImmutableLogRecord`,
scope, and resource: instead of a `newtype` wrapper around `ReadWriteLogRecord`.
- `mkReadableLogRecord` is now `IO` (reads the `IORef` at call time to produce a
consistent point-in-time snapshot). Callers must update `let` bindings to `<-`.
### Span lifecycle enforcement
- `setStatus`, `addAttribute`, `addAttributes`, `addEvent`, `addLink`, and
`updateName` now check `spanEnd` and silently skip mutations on ended spans.
This aligns with the OTel spec: "the Span MUST NOT be modified after it ends."
### Exception handlers (Haskell extension)
- New `OpenTelemetry.Trace.ExceptionHandler` module with `ExceptionClassification`, `ExceptionResponse`, `ExceptionHandler` types
- Smart constructors: `ignoreExceptionType`, `ignoreExceptionMatching`, `recordExceptionType`, `recordExceptionMatching`, `classifyException`, `exitSuccessHandler`
- `TracerProvider` now has `tracerProviderExceptionHandlers` field for global exception classification
- `TracerOptions` now has `tracerExceptionHandlerOptions` field for per-library exception classification
- `inSpan''` consults exception handlers before setting Error status / recording events
- **Breaking**: `TracerOptions` changed from `newtype` to `data` (added `tracerExceptionHandlerOptions` field)
### Resource & InstrumentationLibrary ergonomics
- `instrumentationLibrary :: Text -> Text -> InstrumentationLibrary`: smart constructor (name + version)
- `withSchemaUrl :: Text -> InstrumentationLibrary -> InstrumentationLibrary`: composable modifier
- `withLibraryAttributes :: Attributes -> InstrumentationLibrary -> InstrumentationLibrary`: composable modifier
- `materializeResourcesWithSchema :: Maybe String -> Resource schema -> MaterializedResources`: set runtime schema URL
- `setMaterializedResourcesSchema :: Maybe String -> MaterializedResources -> MaterializedResources`: override schema
### Tracing
- `makeTracer` now wires `TracerOptions.tracerSchema` into `InstrumentationLibrary.librarySchemaUrl` (was ignored)
- Add `alwaysRecord` sampler: decorator that upgrades DROP to RECORD_ONLY so span processors see all spans without increasing export volume
- Fix `isValid` to require BOTH TraceId AND SpanId non-zero (was incorrectly valid if either was non-zero)
- Add `TraceState.lookup` for getting a value by key (MUST per spec)
- Add `spanExporterForceFlush` field to `SpanExporter` (MUST per spec); built-in simple/batch processors now call it
### Logs
- Add `logRecordEventName` field to `ImmutableLogRecord` and `eventName` to `LogRecordArguments`
- Add `loggerIsEnabled` function to check if a Logger has registered processors (SHOULD per spec)
### Metrics: full API coverage (new!)
This release introduces complete metrics support to hs-opentelemetry-api,
covering the entire synchronous and asynchronous instrument surface from the
OpenTelemetry specification.
- **Synchronous instruments**: `Counter`, `UpDownCounter`, `Histogram`, `Gauge`
- **Asynchronous (observable) instruments**: `ObservableCounter`,
`ObservableUpDownCounter`, `ObservableGauge`, with `observable*Enabled`
fields so callers can skip expensive measurement callbacks when no SDK is
installed
- **Views**: `name` and `description` override fields on `View`;
`filterAttributesByKeys` for attribute projection
- **Aggregation**: `AggregationTemporality` (delta / cumulative),
`ExponentialHistogramDataPoint`, `MetricExportExponentialHistogram`
- **Exemplars**: `MetricExemplar` type; exemplar fields on all data point types
- **Advisory parameters**: `AdvisoryParameters` with optional
`advisoryHistogramAggregation`; `HistogramAggregation` selects explicit
bucket boundaries or exponential scale
- **Timestamps**: `startTimeUnixNano` on `SumDataPoint`, `HistogramDataPoint`,
`ExponentialHistogramDataPoint`, `GaugeDataPoint`
- **Environment**: `lookupMetricExportIntervalMillis`, `MetricsExemplarFilter`,
`lookupMetricsExemplarFilter`
- **Debug**: `OpenTelemetry.Debug.MetricExport` for human-readable rendering of
metric export batches
## 0.3.1.0
- Add `tracerIsEnabled` function to check if a Tracer is enabled (helps avoid expensive operations when tracing is disabled)
- Fix `spanIdBaseEncodedByteString` error message
## 0.3.0.0
- Export `fromList` from `OpenTelemetry.Trace.TraceState` for creating TraceState from key-value pairs
## 0.2.1.0
- defined and exported `toImmutableSpan` and `FrozenOrDropped` from `OpenTelemetry.Trace.Core`
## 0.2.0.0
- `callerAttributes` and `ownCodeAttributes` now work properly if the call stack has been frozen. Hence most
span-construction functions should now get correct source code attributes in this situation also (#137.
- Added `detectInstrumentationLibrary` for producing `InstrumentationLibrary`s with TH (#2).
- Fixed precedence order of resource merge (#156).
- Added the ability to add links to spans after creation (#152).
- Correctly compute attribute length limits (#151).
- Add helper for reading boolean environment variables correctly (#153).
- Initial scaffolding for logging support. Renamed `Processor` to `SpanProcessor`.
- Export `FlushResult` (#96)
- Use `HashMap Text Attribute` instead of `[(Text, Attribute)]` as attributes
- Improved conformance with semantic conventions.
## 0.0.3.6
- GHC 9.4 support
- Add Show instances to several api types
## 0.0.3.1
- `adjustContext` uses an empty context if one hasn't been created on the current thread yet instead of acting as a no-op.
## 0.0.2.1
- Doc enhancements
## 0.0.2.0
- Separate `Link` and `NewLink` into two different datatypes to improve Link creation interface.
- Add some version bounds
- Catch & print all synchronous exceptions when calling span processor
start and end hooks
## 0.0.1.0
- Initial release