otel4s
repository·main·Indexed 42 days ago
https://github.com/typelevel/otel4sAn OpenTelemetry implementation for Scala that leverages Cats Effect to provide an idiomatic, high-performance API for metrics and tracing. It includes support for the OpenTelemetry Java SDK via otel4s-oteljava and integrates with backends such as Dash0, Honeycomb, Jaeger, and the Grafana LGTM stack.
What's inside otel4s
- otel4s is an OpenTelemetry implementation for Scala designed to faithfully implement the OpenTelemetry Specification atop Cats Effect. It provides an idiomatic and user-friendly API for metrics, tracing, and context propagation, following Typelevel ecosystem best practices.
Explore the otel4s ecosystem and integrations
mainotel4s integrates with various Typelevel and functional Scala libraries to provide observability (tracing and metrics) across different layers of your application.
Available Integrations:
Web & HTTP:
http4s-otel4s-middleware: Middleware forhttp4sto integrate tracing and metrics seamlessly.tapir: Supports server traces viaotel4s.sttp: sttp 4.x providesmeteredandtracedbackends.
Database:
otel4s-doobie: Integration fordoobieproviding traces of database queries (supports HikariCP).
Messaging & Streaming:
fs2-queues-otel4s: Provides traces and metrics forfs2-queuesmessaging systems.fs2-kafka-otel4s: Provides consumer and producer traces forfs2-kafka.fs2-grpc: Thefs2-grpc-otel4s-tracemodule provides server and client tracing capability.
Experimental Features:
otel4s-experimental: Includes instrumentedcats.effect.std.Queue, runtime metrics, and@spanannotations.otel4s-opentelemetry-java: An experimental distribution of the OpenTelemetry Java agent with instrumentation for Cats Effect andotel4s.
Use the `otel4s-oteljava` backend for JVM applications
mainThe
otel4s-oteljavamodule is the JVM backend forotel4s. It implements theotel4sAPIs on top of the OpenTelemetry Java SDK.Use this backend when you need:
- OpenTelemetry Java exporters and SDK extensions.
- Interoperability with Java libraries that expect OpenTelemetry Java types or the global SDK.
- Integration with the OpenTelemetry Java agent or the
otel4sJava agent distribution. - The
otel4s-oteljava-testkitmodule for asserting exported telemetry in tests.
Note: This backend is JVM-only. For Scala.js or Scala Native, use the
otel4s-sdkproject instead.Common tracing tasks in otel4s
mainThe following tasks are supported via specific guides:
- Effectful Code: Create spans around effectful code.
- No-op Tracing: Provide a no-op tracer for testing or environments where tracing is disabled.
- Unmanaged Spans: Use unmanaged spans when a span must end outside its immediate scope.
- Stream Tracing: Trace
Resourceandfs2.Streamcode. - Baggage: Work with OpenTelemetry baggage to carry metadata.
- Context Propagation: Propagate trace context across service boundaries.
- Java Interop: Use
otel4swith Java-instrumented libraries or Pekko HTTP instrumentation.
Identify the main entry points for otel4s
mainMost development starts with one of these two families:
otel4s-core*: Use these when you are instrumenting a library or writing backend-agnostic code. It defines core APIs likeTracer,Meter, and logging interfaces.otel4s-oteljava*: Use these when building a JVM application that needs a production telemetry backend. This is the main backend in this repository and is JVM-only.
Re-enter an unmanaged span scope using `childScope`
mainCalling
startUnmanageddoes not automatically make the returned span the 'current' span for subsequent operations. If you perform work later that you want to be a child of the unmanaged span, you must explicitly re-enter its context usingchildScope(span.context).Without
childScope, any new spans created will attach to the previous parent in the trace hierarchy instead of the unmanaged span.import cats.Monad import cats.syntax.flatMap._ import org.typelevel.otel4s.trace.Tracer def propagated[F[_]: Monad: Tracer]: F[Unit] = Tracer[F].span("auto").surround { Tracer[F].span("unmanaged").startUnmanaged.flatMap { unmanaged => Tracer[F].childScope(unmanaged.context) { Tracer[F].span("child-1").use_ >> unmanaged.end } } }Set explicit parents with `childScope` and `withParent`
mainWhen you need to bypass the automatic context discovery and specify a parent manually, you have two primary options depending on the scope of the change:
childScope(parent): Use this when you want multiple spans created within a block to all inherit the same explicitSpanContext. It changes the tracing context for the entire duration of the block.withParent(parent): Use this when you want to attach only one specific span to an explicit parent without affecting the tracing context for any other spans in the surrounding scope.
Use
childScopefor grouping related operations under a known parent, andwithParentfor one-off attachments (e.g., attaching a specific span to an outer span even if other spans were created in between).// Using childScope to group multiple spans under one parent def continueMany(parent: SpanContext)(implicit tracer: Tracer[IO]): IO[Unit] = Tracer[IO].childScope(parent) { for { _ <- Tracer[IO].span("step-1").use_ _ <- Tracer[IO].span("step-2").use_ } yield () } // Using withParent to attach a single span to a specific parent def attachOneToOuter(implicit tracer: Tracer[IO]): IO[Unit] = Tracer[IO].span("span").use { outer => Tracer[IO].span("span-2").use_ >> Tracer[IO].spanBuilder("span-3").withParent(outer.context).build.use_ }Choose between `OtelJava.autoConfigured` and `OtelJava.global`
mainWhen using the
otel4s-oteljavabackend, you must choose how to access the SDK based on who owns the SDK lifecycle:OtelJava.autoConfiguredUse this when your application is responsible for creating the SDK. It creates an isolated, non-global SDK instance using OpenTelemetry Java autoconfiguration.
Common scenarios:
- Your application starts the SDK itself.
- You configure exporters and SDK settings via environment variables or JVM properties.
- You do not need to reuse an SDK already created by another library, framework, or agent.
Warning: If a global SDK already exists in the process, using
autoConfiguredcreates an isolated instance which may make telemetry behavior harder to reason about.OtelJava.globalUse this when the SDK is already configured elsewhere and
otel4sneeds to join that existing setup. It reads the current process-wide OpenTelemetry instance.Common scenarios:
- An agent (like the OpenTelemetry Java agent) configures the global SDK.
- A framework or bootstrap layer initializes OpenTelemetry before your
otel4scode runs. - You need
otel4sand Java libraries to observe the same process-wide SDK state.
Perform partial matching with `MetricExpectation`
mainMetric expectations in the testkit are partial by design. You only need to specify the properties you want to assert; all other properties are ignored. This allows for flexible tests that focus only on relevant data.
Examples:
- Name only:
MetricExpectation.name("service.counter")matches any metric with that name, regardless of type, points, scope, or resource. - Value only:
MetricExpectation.sum[Long]("service.counter").value(1L)matches a long sum metric namedservice.counterthat has at least one point with the value1L.
// Matches any metric named "service.counter" MetricExpectation.name("service.counter") // Matches a long sum metric named "service.counter" with at least one point of value 1L MetricExpectation.sum[Long]("service.counter").value(1L)- Name only:
How point-set expectations work in MetricsTestkit
mainIn
otel4smetrics testing,MetricExpectationdoes not check points individually in isolation. Instead, it evaluates aPointSetExpectationagainst the entire collection of points for a metric. This allows you to assert presence, absence, cardinality, and collection-wide invariants.All typed metric expectations provide several shorthand methods for composing
PointSetExpectations:points(...): For rawPointSetExpectationcomposition.containsPoints(...): Shorthand forPointSetExpectation.contains(...)(requires distinct matching points, but allows extras).exactlyPoints(...): Shorthand forPointSetExpectation.exactly(...)(requires exactly the expected points and no extras).pointCount(...): Shorthand forPointSetExpectation.count(...)(checks total count).withoutPointsMatching(...): Shorthand forPointSetExpectation.none(...)(asserts no points match the criteria).pointsWhere(...): Shorthand forPointSetExpectation.predicate(...)(allows custom collection-wide assertions).
// Example of using shorthand methods on a MetricExpectation MetricExpectation .sum[Long]("service.requests") .containsPoints( PointExpectation.numeric(1L).attributesSubset(Attribute("region", "eu")), PointExpectation.numeric(1L).attributesSubset(Attribute("region", "us")) )Use TraceExpectation and TraceForestExpectation for structural matching
mainStructural matching allows you to define the shape of your traces using trees and forests.
Tree Expectations (
TraceExpectation)Used to describe a single subtree:
TraceExpectation.leaf(...): A span with no expected children.TraceExpectation.ordered(...): A subtree whose direct children must appear in a specific order.TraceExpectation.unordered(...): A subtree whose direct children may appear in any order.
Forest Expectations (
TraceForestExpectation)Used to describe the full set of exported spans:
TraceForestExpectation.ordered(...): Requires root spans to appear in a specific order.TraceForestExpectation.unordered(...): Ignores the order of root spans.TraceForestExpectation.empty: Requires no finished root spans.
Note: Both ordered and unordered modes still require the exact number of direct children or roots.
// A leaf span TraceExpectation.leaf(SpanExpectation.name("db.query")) // An ordered subtree TraceExpectation.ordered( SpanExpectation.name("request").noParentSpanContext, TraceExpectation.leaf(SpanExpectation.name("decode")), TraceExpectation.leaf(SpanExpectation.name("persist")) )Understand semantic attribute requirements
mainWhile
otel4s-semconvprovides typed keys, it does not enforce the following OpenTelemetry semantic convention rules:- Whether an attribute is required, recommended, conditionally required, or opt-in.
- Cardinality limits (e.g., preventing too many unique values).
- Allowed values (e.g., specific string enums).
Always consult the Scaladoc for the specific generated key and the official OpenTelemetry semantic conventions to ensure your instrumentation follows the correct requirements.