ZIO Traces not child of tapir endpoint span

Hi all,

Using ZIO and open telemetry IMHO I have to provide an Interceptor propagates OpenTelemetry’s `io.opentelemetry.context.Context` (ThreadLocal) into zio-opentelemetry’s `ContextStorage` (FiberRef) for each Tapir request, so that `zio.telemetry.opentelemetry.tracing.Tracing` spans (e.g. `tracing.span`) are children of the HTTP sttp.tapir.server.tracing.opentelemetry.OpenTelemetryTracing `SERVER` span.

object ZioOtelContextBridge {

  def tapirRequestInterceptor(storage: ContextStorage): RequestInterceptor[Task] =
    RequestInterceptor.transformResultEffect[Task](
      new RequestInterceptor.RequestResultEffectTransform[Task] {
        def apply[B](
            request: ServerRequest,
            result: Task[RequestResult[B]]
        ): Task[RequestResult[B]] = {
          // Captured while `OpenTelemetryTracing` still has the server `Span` in `Context` (synchronous transform hook).
          val ctx: Context = Context.current()
          storage.locally(ctx)(result)
        }
      }
    )
}

WDYT, is seem to works like a charm, I this is useful would you be interested by a contrib ?

Attachmenent bellow, without the Interceptor the inner span are not child of the entry point span.

Thx

Yes, that’s exactly the kind of integration that is needed to make otel work with libs like ZIO. Though, where does the context originally come from? That is - what is setting the context that’s being read by Context.current()?

Cool,
ThreadLocal AFAIU from opentelemetry (opentelemetry-java/context/src/main/java/io/opentelemetry/context/Context.java at main · open-telemetry/opentelemetry-java · GitHub)

Then @adamw do you see any issue with this interceptor ?

Yes, but the context needs to be populated - typically by parsing incoming tracing headers. And this has to be done by ZIO, as that’s the only component that knows about the headers :). Unless you’re setting an empty context, but then the interceptor could do just that, set an empty one?

You mean in case of distributed tracing (baggage & carrier) ?
My need right now was just to link tapir span and ZIO ones, distributed tracing is my next objective.

Thanks Adam.

Ah yes, well in the end for a full ZIO integration I think you’ll need a custom zio-tracing interceptor (instead of tapir/tracing/opentelemetry-tracing/src at master · softwaremill/tapir · GitHub which I suspect you’re using now?) so that it reads the context from ContextStorage. Plus another one for parsing the dist tracing headers.

1 Like

You suspicion are legit ;p

Right now I chain the 2:

ZioHttpServerOptions.customiseInterceptors
.prependInterceptor(
OpenTelemetryTracing(otel)
)
.prependInterceptor(
ZioOtelContextBridge.tapirRequestInterceptor(contextStorage)
)

AFAIU it works for me need (short term).

I will focus on implementing your solution asap.

Thanks again and good week end

1 Like

Hi again @adamw,

I’ve adapted the otel4s interceptor with zio-telemetry as zio-laminar-tapir/modules/server/src/main/scala/dev/cheleb/ziotapir/server/otel/ZIOpenTelemtryTracing.scala at opentelemetry-baggage · cheleb/zio-laminar-tapir · GitHub

Seems to be OK, any chance I could contribute with a tapir-otel4z-tracing module being part of Tapir ?

Of course :slight_smile: PRs always welcome

Early draft :innocent: !

But it make me realize that ZIO OpenTelemetry pulls all observability dependencies (log, tracing and metrics), hence instead of tracing/zio-tracing module is not the best name :cry:

Also, what is the best/prefered tactic to support scala 2/3 ? Stick to Scala 2 or rely on Scala2 and Scala3 sources ?