# ZIO Traces not child of tapir endpoint span

**URL:** <https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540>\
**Category:** tapir\
**Created:** [April 22, 2026, 9:36pm UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540 "2026-04-22T21:36:56Z")\
**Posts on this page:** 17\
**Page:** 1

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [April 22, 2026, 9:36pm UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/1 "2026-04-22T21:36:56Z")

</div>

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.

```auto
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

 ![Screenshot 2026-04-22 at 23.31.47](https://europe1.discourse-cdn.com/flex005/uploads/softwaremill/original/1X/b65eea156f03a8e70aea0e17ffbbb819eab139c7.png)

---

<div class="post-metadata">

**Author:** ![adamw](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/adamw/32/28_2.png) [@adamw](https://softwaremill.community/u/adamw)\
**Post date:** [April 23, 2026, 10:41am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/2 "2026-04-23T10:41:24Z")

</div>

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()`?

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [April 23, 2026, 11:41am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/3 "2026-04-23T11:41:32Z")

</div>

Cool,  
ThreadLocal AFAIU from opentelemetry ([opentelemetry-java/context/src/main/java/io/opentelemetry/context/Context.java at main · open-telemetry/opentelemetry-java · GitHub](https://github.com/open-telemetry/opentelemetry-java/blob/main/context/src/main/java/io/opentelemetry/context/Context.java))

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [April 23, 2026, 9:57pm UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/4 "2026-04-23T21:57:33Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![adamw](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/adamw/32/28_2.png) [@adamw](https://softwaremill.community/u/adamw)\
**Post date:** [April 24, 2026, 6:56am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/5 "2026-04-24T06:56:15Z")

</div>

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?

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [April 24, 2026, 8:21am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/6 "2026-04-24T08:21:09Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![adamw](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/adamw/32/28_2.png) [@adamw](https://softwaremill.community/u/adamw)\
**Post date:** [April 24, 2026, 8:44am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/7 "2026-04-24T08:44:08Z")

</div>

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](https://github.com/softwaremill/tapir/tree/master/tracing/opentelemetry-tracing/src) which I suspect you’re using now?) so that it reads the context from `ContextStorage`. Plus another one for parsing the dist tracing headers.

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [April 24, 2026, 4:14pm UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/8 "2026-04-24T16:14:05Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [April 30, 2026, 6:40am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/9 "2026-04-30T06:40:49Z")

</div>

Hi again @adamw,

I’ve adapted the [otel4s interceptor](https://tapir.softwaremill.com/en/latest/server/observability.html#otel4s-opentelemetry-tracing) 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](https://github.com/cheleb/zio-laminar-tapir/blob/opentelemetry-baggage/modules/server/src/main/scala/dev/cheleb/ziotapir/server/otel/ZIOpenTelemtryTracing.scala)

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

---

<div class="post-metadata">

**Author:** ![adamw](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/adamw/32/28_2.png) [@adamw](https://softwaremill.community/u/adamw)\
**Post date:** [April 30, 2026, 8:23am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/10 "2026-04-30T08:23:25Z")

</div>

Of course 🙂 PRs always welcome

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [May 3, 2026, 11:22am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/11 "2026-05-03T11:22:58Z")

</div>

> <https://github.com/softwaremill/tapir/pull/5203>

Early draft 😇 !

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 😢

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

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [May 10, 2026, 12:25pm UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/12 "2026-05-10T12:25:15Z")

</div>

Less early draft: [5201 zio interceptor by cheleb · Pull Request #5203 · softwaremill/tapir · GitHub](https://github.com/softwaremill/tapir/pull/5203)

If put a sample: [tapir/examples/src/main/scala/sttp/tapir/examples/observability/ZIOTracingExample.scala at 9de40d93eb5c066a71448e1cf4cc0a03c50c18a5 · cheleb/tapir · GitHub](https://github.com/cheleb/tapir/blob/9de40d93eb5c066a71448e1cf4cc0a03c50c18a5/examples/src/main/scala/sttp/tapir/examples/observability/ZIOTracingExample.scala)

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 🙏 ?

---

<div class="post-metadata">

**Author:** ![adamw](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/adamw/32/28_2.png) [@adamw](https://softwaremill.community/u/adamw)\
**Post date:** [May 12, 2026, 8:54am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/13 "2026-05-12T08:54:24Z")

</div>

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 ;).

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [May 12, 2026, 12:00pm UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/14 "2026-05-12T12:00:05Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [May 19, 2026, 7:19am UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/15 "2026-05-19T07:19:41Z")

</div>

Covered all the claude issues AFAIU.

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

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [May 27, 2026, 9:33pm UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/16 "2026-05-27T21:33:09Z")

</div>

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

> <https://github.com/softwaremill/tapir/pull/5256>
>
> Provides ZIO OpenTelementry Tracing interceptor.
> 
> ZIO OpenTelemetry example wi…th OpenTelemetry setup is provided with:
> 
> \- Trace
> \- Logging
> \- Metric
> 
> As ZIO OpenTelemetry provides them.

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.

---

<div class="post-metadata">

**Author:** ![oNouguier](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/onouguier/32/81_2.png) [@oNouguier](https://softwaremill.community/u/oNouguier)\
**Post date:** [May 30, 2026, 12:31pm UTC](https://softwaremill.community/t/zio-traces-not-child-of-tapir-endpoint-span/540/17 "2026-05-30T12:31:23Z")

</div>

I’ve finally added some convenience method, IMHO this ready to review.

The result is rather compact: [tapir/examples/src/main/scala/sttp/tapir/examples/observability/ZIOpenTelemetryDefaultExample.skip at d9bdecd4b26b76e84861eeeac80aab5ee80adbc5 · cheleb/tapir · GitHub](https://github.com/cheleb/tapir/blob/d9bdecd4b26b76e84861eeeac80aab5ee80adbc5/examples/src/main/scala/sttp/tapir/examples/observability/ZIOpenTelemetryDefaultExample.skip#L42)

Thx @adamw for the previous review.
