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 ?

Less early draft: 5201 zio interceptor by cheleb · Pull Request #5203 · softwaremill/tapir · GitHub

If put a sample: tapir/examples/src/main/scala/sttp/tapir/examples/observability/ZIOTracingExample.scala at 9de40d93eb5c066a71448e1cf4cc0a03c50c18a5 · cheleb/tapir · GitHub

I quite happy with this, but:

  • tapir support many scala version, and I’m stuck with the CI ;( (work on my machine syndrome)
  • ZIO OpenTelemetry pulls more than pure tracing deps
  • I provides some helpers that also pull dependencies ( zio-http-server, Runtime metrics)

WDTY @adamw :folded_hands: ?

Regarding Scala versions: it’s totally fine to support only Scala 3. Unless you need support for Scala 2 at your project, then it worth spending the additional time making that work. The only thing I’d prefer to avoid is having a Scala2-only dependency.

But if you already have a Scala2/3 project, having cross-compile Scala2 sources is of course fine.

We could also rename the module to zio-observability. Maybe having it under tracing/ is not perfect, but also not total nonsense ;).

My bad it was the scala-cli compile, that I missed … Adding scala-cli headers helped !

Covered all the claude issues AFAIU.

I pushed also the module as observability/tapir-otel4z and used o11y as scala package (just a suggestion :innocent: ).

Ok, I closed le previous PR and provide a new one much more limited.

This PR provides only ZIO OpenTelementry Tracing interceptor.

And a ZIO OpenTelemetry example with OpenTelemetry setup is provided with:

  • Trace

  • Logging

  • Metric

As ZIO OpenTelemetry provides them.

@adamw, Sorry for the noise of the first attempt that was an extraction on my usage.

I am not sure that scala-cli is the more convenient way to provide a full example.