Work with baggage

Use this page when you want to attach request-scoped key-value pairs to the current OpenTelemetry context and read them later in the same flow.

In OpenTelemetry, baggage is a set of key-value pairs that travels with the current context. Use it for request-scoped metadata that should be available across service boundaries, such as request IDs, tenant IDs, or debug flags.

Prerequisites

1. Get BaggageManager from OtelJava

BaggageManager[F] gives you access to the current baggage and scoped baggage updates. The main operations are:

import cats.effect.{IO, IOApp}
import org.typelevel.otel4s.baggage.BaggageManager
import org.typelevel.otel4s.oteljava.OtelJava

object Main extends IOApp.Simple {
  def run: IO[Unit] =
    OtelJava.autoConfigured[IO]().use { otel4s =>
      implicit val baggageManager: BaggageManager[IO] = otel4s.baggageManager
      program
    }

  def program(implicit baggageManager: BaggageManager[IO]): IO[Unit] = {
    val _ = baggageManager
    IO.unit
  }
}

2. Add baggage around the effect that needs it

Baggage is immutable. To change it, wrap the effect in scope(...) or local(...).

import org.typelevel.otel4s.baggage.Baggage

def withRequestMetadata[A](fa: IO[A])(implicit baggageManager: BaggageManager[IO]): IO[A] =
  baggageManager.scope(
    Baggage.empty
      .updated("request.id", "req-123")
      .updated("user.id", "user-42")
  )(fa)

If you want to update the current baggage instead of replacing it, use local(...):

def withDebugFlag[A](fa: IO[A])(implicit baggageManager: BaggageManager[IO]): IO[A] =
  baggageManager.local(_.updated("debug", "true"))(fa)

3. Read baggage values downstream

def handleRequest(implicit baggageManager: BaggageManager[IO]): IO[Unit] =
  withRequestMetadata {
    for {
      current <- baggageManager.current
      userId <- baggageManager.getValue("user.id")
      _ <- IO.println(s"baggage: $current")
      _ <- IO.println(s"user.id: ${userId.getOrElse("missing")}")
    } yield ()
  }

What's next