otel4s

repository·main·Indexed 42 days ago

https://github.com/typelevel/otel4s

An 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.

Tokens
71.1K
Snippets
159
Records
229
Agent score
50%

What's inside otel4s

  1. Overview of otel4s

    main
    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.
  2. Explore the otel4s ecosystem and integrations

    main

    otel4s 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 for http4s to integrate tracing and metrics seamlessly.
      • tapir: Supports server traces via otel4s.
      • sttp: sttp 4.x provides metered and traced backends.
    • Database:

      • otel4s-doobie: Integration for doobie providing traces of database queries (supports HikariCP).
    • Messaging & Streaming:

      • fs2-queues-otel4s: Provides traces and metrics for fs2-queues messaging systems.
      • fs2-kafka-otel4s: Provides consumer and producer traces for fs2-kafka.
      • fs2-grpc: The fs2-grpc-otel4s-trace module provides server and client tracing capability.
    • Experimental Features:

      • otel4s-experimental: Includes instrumented cats.effect.std.Queue, runtime metrics, and @span annotations.
      • otel4s-opentelemetry-java: An experimental distribution of the OpenTelemetry Java agent with instrumentation for Cats Effect and otel4s.
  3. Use the `otel4s-oteljava` backend for JVM applications

    main

    The otel4s-oteljava module is the JVM backend for otel4s. It implements the otel4s APIs 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 otel4s Java agent distribution.
    • The otel4s-oteljava-testkit module for asserting exported telemetry in tests.

    Note: This backend is JVM-only. For Scala.js or Scala Native, use the otel4s-sdk project instead.

  4. Common tracing tasks in otel4s

    main

    The 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 Resource and fs2.Stream code.
    • Baggage: Work with OpenTelemetry baggage to carry metadata.
    • Context Propagation: Propagate trace context across service boundaries.
    • Java Interop: Use otel4s with Java-instrumented libraries or Pekko HTTP instrumentation.
  5. Identify the main entry points for otel4s

    main

    Most 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 like Tracer, 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.
  6. Re-enter an unmanaged span scope using `childScope`

    main

    Calling startUnmanaged does 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 using childScope(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
          }
        }
      }
  7. Set explicit parents with `childScope` and `withParent`

    main

    When 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:

    1. childScope(parent): Use this when you want multiple spans created within a block to all inherit the same explicit SpanContext. It changes the tracing context for the entire duration of the block.
    2. 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 childScope for grouping related operations under a known parent, and withParent for 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_
      }
  8. Choose between `OtelJava.autoConfigured` and `OtelJava.global`

    main

    When using the otel4s-oteljava backend, you must choose how to access the SDK based on who owns the SDK lifecycle:

    OtelJava.autoConfigured

    Use 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 autoConfigured creates an isolated instance which may make telemetry behavior harder to reason about.

    OtelJava.global

    Use this when the SDK is already configured elsewhere and otel4s needs 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 otel4s code runs.
    • You need otel4s and Java libraries to observe the same process-wide SDK state.
  9. Perform partial matching with `MetricExpectation`

    main

    Metric 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 named service.counter that has at least one point with the value 1L.
    // 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)
  10. How point-set expectations work in MetricsTestkit

    main

    In otel4s metrics testing, MetricExpectation does not check points individually in isolation. Instead, it evaluates a PointSetExpectation against 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 raw PointSetExpectation composition.
    • containsPoints(...): Shorthand for PointSetExpectation.contains(...) (requires distinct matching points, but allows extras).
    • exactlyPoints(...): Shorthand for PointSetExpectation.exactly(...) (requires exactly the expected points and no extras).
    • pointCount(...): Shorthand for PointSetExpectation.count(...) (checks total count).
    • withoutPointsMatching(...): Shorthand for PointSetExpectation.none(...) (asserts no points match the criteria).
    • pointsWhere(...): Shorthand for PointSetExpectation.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"))
      )
  11. Use TraceExpectation and TraceForestExpectation for structural matching

    main

    Structural 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"))
    )
  12. Understand semantic attribute requirements

    main

    While otel4s-semconv provides 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.