Problem
Tracer.span uses a callback pattern that manages context via ZIO.acquireReleaseWith internally. This works well for standard ZIO effects, but breaks down when integrating with libraries that have their own resource lifecycle — most notably Caliban's ZQuery.
Caliban's FieldWrapper operates on ZQuery, which provides ZQuery.acquireReleaseWith(acquire)(release)(use) with separate acquire/release effects. To create field-level spans that properly nest repository-level spans underneath them, you need to:
- Start a span and push its context (acquire)
- Run the wrapped query, which may internally call
Tracer.span for repo-level work (use)
- End the span and restore the parent context (release)
The callback pattern cannot express this because ZQuery.acquireReleaseWith requires acquire: ZIO[R, E, A] and release: A => ZIO[R, Nothing, Any] as separate values — there is no way to thread a callback-scoped span through them.
Without this, field-level spans in a Caliban schema tracer cannot act as parents for the spans created by downstream resolvers, resulting in a flat span hierarchy instead of the expected field → repository nesting.
Relationship to #1140 (LogSpanner.setAttribute)
spanUnsafe and setAttribute (#1140) are complementary pieces of the same Caliban tracing story:
In a typical Caliban GraphQL stack, the flow is:
FieldWrapper (spanUnsafe creates the field span)
└─ DataSource batch resolver (setAttribute enriches with batch.size, batch.request_count)
└─ Repository (LogSpanner.span creates the repo span)
setAttribute dispatches through the same FiberRef mechanism as LogSpanner.span, so it writes to the innermost span — which is the field span created by spanUnsafe when called from a DataSource, or the repo span when called from deeper code.
Proposed API
def spanUnsafe(
spanName: String,
spanKind: SpanKind = SpanKind.INTERNAL,
attributes: Attributes = Attributes.empty(),
links: Seq[SpanContext] = Seq.empty
)(implicit trace: Trace): ZIO[Any, Nothing, (Span, UIO[Unit])]
Returns (span, endEffect) where:
span is the started child span with its context already pushed to ContextStorage
endEffect ends the span and restores the previous context
This also requires a ContextStorage.set(ctx: Context): UIO[Unit] method to push context without callback scoping.
This is the ZIO equivalent of OpenTelemetry Java SDK's Span.makeCurrent(), which returns a Scope for manual lifecycle management.
Example: Caliban FieldWrapper
class FieldTracer(tracer: Tracer) extends FieldWrapper[Any](wrapPureValues = false) {
def wrap[R](
query: ZQuery[R, CalibanError.ExecutionError, ResponseValue],
info: FieldInfo
): ZQuery[R, CalibanError.ExecutionError, ResponseValue] =
ZQuery.acquireReleaseWith(tracer.spanUnsafe(info.name)) {
case (_, end) => end
} { case (span, _) =>
query.foldCauseQuery(
cause => { span.setStatus(StatusCode.ERROR, cause.prettyPrint); ZQuery.failCause(cause) },
value => ZQuery.succeed(value)
)
}
}
With this, any Tracer.span or LogSpanner.span calls inside the query's resolvers will correctly appear as children of the field span, and LogSpanner.setAttribute calls will enrich the correct span in the hierarchy.
Related
Problem
Tracer.spanuses a callback pattern that manages context viaZIO.acquireReleaseWithinternally. This works well for standard ZIO effects, but breaks down when integrating with libraries that have their own resource lifecycle — most notably Caliban'sZQuery.Caliban's
FieldWrapperoperates onZQuery, which providesZQuery.acquireReleaseWith(acquire)(release)(use)with separate acquire/release effects. To create field-level spans that properly nest repository-level spans underneath them, you need to:Tracer.spanfor repo-level work (use)The callback pattern cannot express this because
ZQuery.acquireReleaseWithrequiresacquire: ZIO[R, E, A]andrelease: A => ZIO[R, Nothing, Any]as separate values — there is no way to thread a callback-scoped span through them.Without this, field-level spans in a Caliban schema tracer cannot act as parents for the spans created by downstream resolvers, resulting in a flat span hierarchy instead of the expected
field → repositorynesting.Relationship to #1140 (LogSpanner.setAttribute)
spanUnsafeandsetAttribute(#1140) are complementary pieces of the same Caliban tracing story:spanUnsafesolves the structural problem: creating spans where the callback pattern doesn't fit (FieldWrapper → ZQuery lifecycle)setAttribute(LogSpanner: add setAttribute for attribute dispatch alongside span dispatch (#1022 follow-up) #1140) solves the enrichment problem: writing attributes to the current span from library code (DataSources, business services) without depending onTracerorSpanIn a typical Caliban GraphQL stack, the flow is:
setAttributedispatches through the sameFiberRefmechanism asLogSpanner.span, so it writes to the innermost span — which is the field span created byspanUnsafewhen called from a DataSource, or the repo span when called from deeper code.Proposed API
Returns
(span, endEffect)where:spanis the started child span with its context already pushed toContextStorageendEffectends the span and restores the previous contextThis also requires a
ContextStorage.set(ctx: Context): UIO[Unit]method to push context without callback scoping.This is the ZIO equivalent of OpenTelemetry Java SDK's
Span.makeCurrent(), which returns aScopefor manual lifecycle management.Example: Caliban FieldWrapper
With this, any
Tracer.spanorLogSpanner.spancalls inside the query's resolvers will correctly appear as children of the field span, andLogSpanner.setAttributecalls will enrich the correct span in the hierarchy.Related
LogSpanner.setAttributefor span enrichment withoutSpandependencyLogSpanneraddition, part of the same broader tracing integration effort