Skip to content

feat: LogSpanner — pluggable span dispatch bridging ZIO.logSpan with OTEL (#1022)#1138

Open
li-nkSN wants to merge 1 commit intozio:v4.0.0-rcfrom
li-nkSN:feature/logspanner-1022
Open

feat: LogSpanner — pluggable span dispatch bridging ZIO.logSpan with OTEL (#1022)#1138
li-nkSN wants to merge 1 commit intozio:v4.0.0-rcfrom
li-nkSN:feature/logspanner-1022

Conversation

@li-nkSN
Copy link
Copy Markdown

@li-nkSN li-nkSN commented Feb 17, 2026

Implements the LogSpanner design proposed in #1022: a FiberRef-based
dispatch mechanism that lets library code mark spans via
effect @@ LogSpanner.span("name") without depending on Tracer.

Three backends:

  • Default: delegates to ZIO.logSpan (zero OTEL overhead)
  • OTEL: creates real OpenTelemetry spans via Tracer.span
  • Hybrid: creates both OTEL and ZIO logSpans

Includes 15 test cases covering default/OTEL/hybrid backends,
scoped installation, and fiber correctness (fork, timeout,
child fiber inheritance).

Design based on the prototype by @thiloplanz in #1022.
Thanks to @grouzen for design guidance and confirming the
ZIO.logAnnotate attribute propagation approach.

@grouzen
Copy link
Copy Markdown
Contributor

grouzen commented Mar 9, 2026

Hello! Great stuff!

I would like to suggest streamlining the resulting design to align with the library's established architecture.
Please have a look at the draft PR I created in here: https://github.com/zio/zio-telemetry/pull/1151/changes.

It basically just moves the constructors and layers around to make it more coherent with the current design.
Also, it adds ZIO's implicit trace: Trace parameter to the methods and layers.

I haven't touched the tests in my PR, so they need to be adjusted accordingly if you are happy with the proposed changes.

Feel free to copy or re-create the suggested changes in your PR so I can remove my draft PR later.

Comment thread docs/logspanner-design-and-usage.md Outdated
…OTEL (zio#1022)

Implements the LogSpanner design proposed in zio#1022: a FiberRef-based
dispatch mechanism that lets library code mark spans via
`effect @@ LogSpanner.span("name")` without depending on Tracer.

Three backends:
- Default: delegates to ZIO.logSpan (zero OTEL overhead)
- OTEL: creates real OpenTelemetry spans via Tracer.span
- Hybrid: creates both OTEL and ZIO logSpans

API surface follows grouzen's suggested restructuring:
- LogSpanner trait with curried logSpan(name)(zio)(implicit trace)
- installOtel/installHybrid on LogSpanner companion
- logSpan + aspects.logSpan on core.OpenTelemetry trait
- installOtelLogSpanner/installHybridLogSpanner layers on
  OpenTelemetry object

OtelLogSpanner folded into LogSpanner companion. Design docs removed
in favor of scaladoc.

14 test cases covering default/OTEL/hybrid backends, scoped
installation, layer factory, and fiber correctness.

Design based on the prototype by @thiloplanz in zio#1022.
Thanks to @grouzen for design guidance and restructuring review.
Comment on lines +98 to +117
/**
* A `ZIOAspect` that wraps an effect with a named span using the currently installed `LogSpanner`.
*
* This is the primary call-site API for library code that should not depend on `Tracer` or `OpenTelemetry`.
*
* Usage:
* {{{
* myEffect @@ LogSpanner.span("operationName")
* }}}
*
* @param spanName
* the span name
* @return
* a ZIOAspect that applies the span
*/
def span(spanName: String): ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] =
new ZIOAspect[Nothing, Any, Nothing, Any, Nothing, Any] {
override def apply[R, E, A](zio: ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
currentLogSpanner.getWith(_.logSpan(spanName)(zio))
}
Copy link
Copy Markdown
Contributor

@grouzen grouzen Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably missed mentioning this one in my PR, but it can be removed since we basically moved it to OpenTelemetry.logSpan. See the next comment below

test("delegates to ZIO.logSpan — no OTEL span created") {
for {
tracerTestkit <- ZIO.service[TracerTestkit]
_ <- ZIO.unit @@ LogSpanner.span("mySpan")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I had to be explicit about this. The idea is to replace this call with OpenTelemetry.logSpan.

This way, we kinda mimic the ZIO.logSpan, using the same naming convention.

@li-nkSN
Copy link
Copy Markdown
Author

li-nkSN commented Mar 24, 2026

@grouzen Thanks for the restructuring feedback — we've incorporated all of it (curried signatures, implicit trace, installOtel/installHybrid on LogSpanner, OpenTelemetry.logSpan + aspects.logSpan on the trait, layer factories on the OpenTelemetry object).

Regarding removing LogSpanner.span — we'd like to keep it alongside OpenTelemetry.aspects.logSpan. The reason is the viral dependency problem that #1022 was designed to solve.

Consider a database module that instruments repository operations:

// database module — depends only on zio-opentelemetry-core
import zio.telemetry.opentelemetry.core.trace.LogSpanner

def findById(id: Long): Task[Option[Entity]] =
repository.findById(id) @@ LogSpanner.span("repo:findById")

No OpenTelemetry instance needed. No environment requirement. The module depends only on zio-opentelemetry-core.

If LogSpanner.span is removed and the call site must use openTelemetry.aspects.logSpan:

// database module — now needs OpenTelemetry instance
def findById(id: Long): Task[Option[Entity]] =
repository.findById(id) @@ openTelemetry.aspects.logSpan("repo:findById")

Where does openTelemetry come from? Either a constructor parameter or ZIO.serviceWithZIO[OpenTelemetry]. Both propagate — every module that calls findById must provide OpenTelemetry, and every module that calls those modules must provide it too. This is the viral Tracer dependency that
#1022's FiberRef design specifically eliminates.

LogSpanner.span and OpenTelemetry.aspects.logSpan are functionally identical — same FiberRef[LogSpanner], same dispatch. They can coexist: OpenTelemetry.aspects.logSpan for application code that already has an instance, LogSpanner.span for library/infrastructure code that shouldn't need
one.

@grouzen
Copy link
Copy Markdown
Contributor

grouzen commented Apr 14, 2026

Hi! Sorry for being silent for so long.

I've thought about it a lot, and I feel like this kind of stuff shouldn't be added to the library in this form.

I think the proper approach would be to add a similar mechanism to the one used for switching between logging backends in ZIO: https://zio.dev/guides/tutorials/create-custom-logger-for-a-zio-application. I believe, this is the conclusion we came to with @thiloplanz in the original issue. The main idea is we want to be able to swap the backends of logSpan and logAnnotate using the non-existent ZIO API.

The main argument against the suggested API is that it feels like we are adding another clunky version of two already existing APIs:

  • zio-telemetry: tracer.span and tracer.addAttribute
  • ZIO: ZIO.logAnnotate and ZIO.logSpan

The better strategy would be to keep this additional layer/API in a separate library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants