hs-opentelemetry-api
OpenTelemetry API for libraries to instrument code or build wrappers.
https://github.com/iand675/hs-opentelemetry#readme
| Stackage Nightly 2026-06-01: | 1.0.0.0 |
| Latest on Hackage: | 1.0.0.0 |
hs-opentelemetry-api-1.0.0.0@sha256:9faf9d6abf556b7dbe393353eaca1088039593902dd05b859e3505ade80afd77,5993Module documentation for 1.0.0.0
- OpenTelemetry
- OpenTelemetry.Attributes
- OpenTelemetry.Baggage
- OpenTelemetry.Common
- OpenTelemetry.Context
- OpenTelemetry.Contrib
- OpenTelemetry.Debug
- OpenTelemetry.Environment
- OpenTelemetry.Exporter
- OpenTelemetry.Internal
- OpenTelemetry.Internal.AtomicBucketArray
- OpenTelemetry.Internal.AtomicCounter
- OpenTelemetry.Internal.Common
- OpenTelemetry.Internal.Log
- OpenTelemetry.Internal.Logging
- OpenTelemetry.Internal.Trace
- OpenTelemetry.Log
- OpenTelemetry.LogAttributes
- OpenTelemetry.Metric
- OpenTelemetry.Processor
- OpenTelemetry.Propagator
- OpenTelemetry.Registry
- OpenTelemetry.Resource
- OpenTelemetry.Resource.Cloud
- OpenTelemetry.Resource.Container
- OpenTelemetry.Resource.DeploymentEnvironment
- OpenTelemetry.Resource.Device
- OpenTelemetry.Resource.FaaS
- OpenTelemetry.Resource.Host
- OpenTelemetry.Resource.Kubernetes
- OpenTelemetry.Resource.OperatingSystem
- OpenTelemetry.Resource.Process
- OpenTelemetry.Resource.Service
- OpenTelemetry.Resource.Telemetry
- OpenTelemetry.Resource.Webengine
- OpenTelemetry.SemanticsConfig
- OpenTelemetry.Trace
- OpenTelemetry.Util
OpenTelemetry API for Haskell
This package provides an interface for instrumentors to use when instrumenting a library directly or implementing a wrapper API around an existing project.
The methods in this package can be safely called by libraries or end-user applications regardless of whether the application has registered an OpenTelemetry SDK configuration or not. When the OpenTelemetry SDK has not registered a tracer provider with any span processors, there API incurs minimal performance overhead, as most of the core interface performs no-ops.
In order to generate and export telemetry data, you will also need to use the OpenTelemetry Haskell SDK.
The inspiration of the OpenTelemetry project is to make every library and application observable out of the box by having them call the OpenTelemetry API directly. Until that happens, there is a need for a separate library which can inject this information. A library that enables observability for another library is called an instrumentation library. In the case of Haskell, instrumentation is currently entirely manual.
Visit the GitHub project for a list of provided instrumentation libraries.
Install Dependencies
Add hs-opentelemetry-api to your package.yaml or Cabal file.
Useful Links
- For more information on OpenTelemetry, visit: https://opentelemetry.io/
- For more about the Haskell OpenTelemetry project, visit: https://github.com/iand675/hs-opentelemetry
Changes
Changelog for hs-opentelemetry-api
Unreleased
1.0.0.0 - 2026-05-29
Full Spec conformance against 1.55.0
InstrumentationScopetype alias added. The OTel spec renamed “Instrumentation Library” to “Instrumentation Scope”.InstrumentationScopeis now a type alias forInstrumentationLibrary, andinstrumentationScopeis the preferred constructor. The underlying type retains the old name for backwards compatibility. Spec: https://opentelemetry.io/docs/specs/otel/common/instrumentation-scope/LogRecordExporter.forceFlushnow returnsIO FlushResult. Previously returnedIO (). 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/#logrecordexportershouldSamplenow receives theInstrumentationScope. TheSamplertype’sCustomSamplerconstructor now takes the tracer’sInstrumentationLibraryas a final parameter. This is a breaking change for custom sampler implementations. Spec: https://opentelemetry.io/docs/specs/otel/trace/sdk/#shouldsampleImmutableLogRecordinternal fields switched toUMaybe. ThelogRecordTimestamp,logRecordTracingDetails,logRecordSeverityText,logRecordSeverityNumber, andlogRecordEventNamefields now use unboxed optionals for lower allocation on the emit hot path. ExternalLogRecordArgumentsfields remain asMaybe.
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:
ImmutableSpanidentity fields (trace/span ID, kind, start time) are now immutable and accessed without touching anyIORef; only mutable state (hotName,hotEnd,hotAttributes, etc.) goes through a singleIORef SpanHot. Eliminates an indirection on everygetSpanContext/isRecordingcall. - Unboxed TraceId/SpanId: two
Word64fields in registers instead of a heap-allocated pinnedShortByteString. Eliminates allocation on every span. - Thread-local xoshiro256++ RNG: replaced
System.Random.Stateful(Haskellrandompackage) 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 therandomdependency. - Timestamp FFI:
Timestampis nowWord64nanoseconds. Directclock_gettimeC FFI call bypasses theclockpackage’salloca/errno/Storableoverhead. OTLP serialization is zero-cost (coerce). Dropped theclockdependency. - C hex encoding: trace/span ID hex via SWAR in C (
hs_otel_hex.c), avoiding intermediateByteStringallocations from the old Haskell encoder. - No-op fast path:
inSpanskipsmask/bracketError/context modification entirely when no processors are registered. bracketErrorelimination: inlined themask $ \restore ->pattern intoinSpan, eliminating a 4-tuple allocation,uninterruptibleMask_, andtrythat the previous genericbracketErrorhelper required.- Thread-local context rewrite: the previous
thread-utils-contextV1 stored contexts in 32-stripeIntMaps with CAS (casArray#+yield#retry) on every write. EverygetContextcall allocated aThreadIdbox viamyThreadId#, crossed the FFI boundary torts_getThreadId, then did an O(log n)IntMap.lookupinside the stripe. EveryadjustContext/attachContextadditionally CAS’d the entire stripeIntMap, withyield#-based spin on contention.lookupSpanwent throughData.Vault.Strict(aHashMapkeyed byData.Unique). The new implementation replaces all of this with a flat open-addressed hash table backed byMutableByteArray#keys andMutableArray#values, where each thread gets its ownIORef. Hot-path reads and writes now go directly through thatIORefwith 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 themyThreadId#box allocation andrts_getThreadIdFFI call entirely. The OTel wrapper forContextitself now has dedicated unboxed slots forSpanandBaggage, replacingData.Vault.Strictlookups with O(1) pattern matches. Result:getContextdropped from 17.3 ns to 2.9 ns (6x),lookupSpanfrom 10.0 ns to 0.6 ns / 0 B (17x). - CAS
yield#removal: removedyield#from the CAS failure path incasModifyIORef_; its presence prevented GHC from optimizing the uncontended success path. - Cached attribute limits: pre-resolved from
TracerProviderontoTraceratmakeTracertime, 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;
shouldSamplesplit 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’sHashMap, avoiding intermediate list/tuple allocation. search for histogram bucket index,AtomicBucketArray(singleMutableByteArray#withfetchAddIntArray#),OptionalDoublefor histogram min/max.- Dependency removals: dropped
random,clock,http-types,case-insensitive,binary,charset,regex-tdfafrom 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 (StartEndSpan/AlwaysSample, May 2025) |
| Rust | ~349 ns | open-telemetry/opentelemetry-rust#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
addAttributesnow correctly overwrites existing keys.H.unionargument order was reversed, causing existing attribute values to silently take precedence over new ones. New values now win on key conflict, matchingaddAttributebehavior and spec intent.traceIdRatioBaseddescription now always usesTraceIdRatioBased{ratio}format. Previously,traceIdRatioBased 1.0returnedalwaysOnwhose 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)
LoggerProvidershutdown suppresses processor dispatch. AddedloggerProviderIsShutdownflag. AftershutdownLoggerProvider,emitLogRecordstill returns aReadWriteLogRecordbut skips calling processors.loggerIsEnablednow returnsIO Booland accounts for shutdown state.BaggageinsertCheckedenforces W3C size limits. NewinsertChecked :: Token -> Element -> Baggage -> Either InvalidBaggage Baggageenforces the 180-member limit (W3C ABNF grammar) and 8192-byte total serialized size limit.InvalidBaggagenow derivesShow, Eq.
Breaking changes
createLoggerProvideris now monadic (MonadIO m => ... -> m LoggerProvider). Required to safely allocate the internal shutdownIORef. Existingletbindings need to become<-bindings.loggerIsEnablednow returnsIO Boolinstead ofBool. Checks the provider shutdown flag, which requires reading theIORef.propagatorNamesrenamed topropagatorFields. ThePropagatorrecord field is now calledpropagatorFieldsto match the OpenTelemetry spec’sFieldsmethod. Values are actual header names (e.g.["traceparent", "tracestate"]), not display names.- New
TextMapPropagatortype alias.type TextMapPropagator = Propagator Context RequestHeaders RequestHeadersis now exported fromOpenTelemetry.Propagator. - Global
TextMapPropagatorAPI.getGlobalTextMapPropagatorandsetGlobalTextMapPropagatorprovide a spec-conformant global propagator. Defaults to no-op per spec. The SDK sets this during initialization. Instrumentation libraries should prefer the global propagator overgetTracerProviderPropagators. SemanticsOptionsis now opaque with generalized stability lookup. Instead of a record withhttpOptionanddatabaseOptionfields,SemanticsOptionsnow stores the parsed env var values as a set. Use the newlookupStability :: Text -> SemanticsOptions -> StabilityOptfunction to query any signal key (e.g."http","database","messaging","rpc"). The convenience functionshttpOptionanddatabaseOptionstill work as before.HttpOptionis now a type alias for the renamedStabilityOptdata type. Third-party instrumentation libraries can now participate in theOTEL_SEMCONV_STABILITY_OPT_INmechanism without modifying this module.
Bug fixes
- Fix:
setStatusmerge semantics. Previously usedmaxon anOrd SpanStatusinstance to merge statuses, which broke Error-over-Error (last-writer-wins) regardless of howOrdwas defined. Now uses an explicitmergeStatusfunction implementing the three spec rules: Ok is final, Unset is ignored, everything else is last-writer-wins. TheOrdinstance is now lawful (EQ for Error/Error) and only represents the class hierarchy (Ok > Error > Unset), not merge logic. - Fix:
forceFlushTracerProviderleaked async threads on timeout. Outstanding processor flush asyncs were never cancelled when the timeout fired. Now callsmapM_ cancel jobson theNothing(timeout) branch. - Fix:
isRecordingreturnedTrueforFrozenSpan.FrozenSpanis an already-completed immutable span (used for links/export). It is not recording. Now returnsFalse, aligning withwhenSpanIsRecording. - Fix:
setGlobalTracerProviderused non-atomicwriteIORef. Concurrent reads could see torn state. Now usesatomicWriteIORef. - Fix:
setGlobalMeterProviderandsetGlobalLoggerProviderused non-atomicwriteIORef. Same rationale as the tracer provider; now usesatomicWriteIORef. - Fix: noop observable instruments reported
enabled = True. The no-opMeternow reportsFalsefrom observable*Enabledactions 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-atomicmodifyIORef'. Concurrent calls could race and silently drop updates (e.g. lost events under concurrentaddEvent). All now useatomicModifyIORef', matchingendSpanwhich was already atomic. Also fixedwithCarryOnProcessorinOpenTelemetry.Contrib.CarryOns. - Fix: log record mutation (
addAttribute,addAttributes) used non-atomic lazymodifyIORef. Same race condition as span mutations, plus thunk buildup from the lazy variant.modifyLogRecordandatomicModifyLogRecordnow both useatomicModifyIORef'(strict and atomic). - Fix:
forceFlushLoggerProviderleaked async threads on timeout. Same bug asforceFlushTracerProvider: processor flush asyncs were never cancelled when the timeout fired. Now callsmapM_ cancel jobs. - Fix:
shutdownLoggerProvideraborted on first processor failure. Usedwaitwhich re-throws on async exception, causing remaining processors to be skipped. Now useswaitCatchso all processors get a chance to shut down. - Fix:
Dropped/ no-processor spans discarded parentTraceState. When creating a child span with aDroppedparent, or when no processors are configured,traceStatewas forced toTraceState.empty. Now inherits the parent’straceState, preserving vendor data in W3Ctracestate. - Fix:
shutdownTracerProviderwas sequential. Each processor shutdown had to complete before the next started. Now launches all shutdowns concurrently and waits for all viawaitCatch.
ReadableLogRecord true snapshot
ReadableLogRecordis now adatatype holding a snapshottedImmutableLogRecord, scope, and resource: instead of anewtypewrapper aroundReadWriteLogRecord.mkReadableLogRecordis nowIO(reads theIORefat call time to produce a consistent point-in-time snapshot). Callers must updateletbindings to<-.
Span lifecycle enforcement
setStatus,addAttribute,addAttributes,addEvent,addLink, andupdateNamenow checkspanEndand 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.ExceptionHandlermodule withExceptionClassification,ExceptionResponse,ExceptionHandlertypes - Smart constructors:
ignoreExceptionType,ignoreExceptionMatching,recordExceptionType,recordExceptionMatching,classifyException,exitSuccessHandler TracerProvidernow hastracerProviderExceptionHandlersfield for global exception classificationTracerOptionsnow hastracerExceptionHandlerOptionsfield for per-library exception classificationinSpan''consults exception handlers before setting Error status / recording events- Breaking:
TracerOptionschanged fromnewtypetodata(addedtracerExceptionHandlerOptionsfield)
Resource & InstrumentationLibrary ergonomics
instrumentationLibrary :: Text -> Text -> InstrumentationLibrary: smart constructor (name + version)withSchemaUrl :: Text -> InstrumentationLibrary -> InstrumentationLibrary: composable modifierwithLibraryAttributes :: Attributes -> InstrumentationLibrary -> InstrumentationLibrary: composable modifiermaterializeResourcesWithSchema :: Maybe String -> Resource schema -> MaterializedResources: set runtime schema URLsetMaterializedResourcesSchema :: Maybe String -> MaterializedResources -> MaterializedResources: override schema
Tracing
makeTracernow wiresTracerOptions.tracerSchemaintoInstrumentationLibrary.librarySchemaUrl(was ignored)- Add
alwaysRecordsampler: decorator that upgrades DROP to RECORD_ONLY so span processors see all spans without increasing export volume - Fix
isValidto require BOTH TraceId AND SpanId non-zero (was incorrectly valid if either was non-zero) - Add
TraceState.lookupfor getting a value by key (MUST per spec) - Add
spanExporterForceFlushfield toSpanExporter(MUST per spec); built-in simple/batch processors now call it
Logs
- Add
logRecordEventNamefield toImmutableLogRecordandeventNametoLogRecordArguments - Add
loggerIsEnabledfunction 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, withobservable*Enabledfields so callers can skip expensive measurement callbacks when no SDK is installed - Views:
nameanddescriptionoverride fields onView;filterAttributesByKeysfor attribute projection - Aggregation:
AggregationTemporality(delta / cumulative),ExponentialHistogramDataPoint,MetricExportExponentialHistogram - Exemplars:
MetricExemplartype; exemplar fields on all data point types - Advisory parameters:
AdvisoryParameterswith optionaladvisoryHistogramAggregation;HistogramAggregationselects explicit bucket boundaries or exponential scale - Timestamps:
startTimeUnixNanoonSumDataPoint,HistogramDataPoint,ExponentialHistogramDataPoint,GaugeDataPoint - Environment:
lookupMetricExportIntervalMillis,MetricsExemplarFilter,lookupMetricsExemplarFilter - Debug:
OpenTelemetry.Debug.MetricExportfor human-readable rendering of metric export batches
0.3.1.0
- Add
tracerIsEnabledfunction to check if a Tracer is enabled (helps avoid expensive operations when tracing is disabled) - Fix
spanIdBaseEncodedByteStringerror message
0.3.0.0
- Export
fromListfromOpenTelemetry.Trace.TraceStatefor creating TraceState from key-value pairs
0.2.1.0
- defined and exported
toImmutableSpanandFrozenOrDroppedfromOpenTelemetry.Trace.Core
0.2.0.0
callerAttributesandownCodeAttributesnow 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
detectInstrumentationLibraryfor producingInstrumentationLibrarys 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
ProcessortoSpanProcessor. - Export
FlushResult(#96) - Use
HashMap Text Attributeinstead 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
adjustContextuses 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
LinkandNewLinkinto 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