Skip to main content

ZTracer

ZTracer is the abstraction provided by this library which allows you to seamlessly create traces & spans. For example:

import zio.*
import io.kaizensolutions.trace4cats.zio.extras.ZTracer

val nestedTraceExample: URIO[ZTracer, Unit] =
ZTracer.span("parent") {
ZTracer
.span("child1")(ZTracer.span("grandchild")(ZIO.unit))
.zipParLeft(ZTracer.span("child2")(ZIO.unit))
}

This generates the following structure:

Span structure

Notice that using the ZTracer abstraction cannot fail; meaning if you mis-configure the tracing configuration, your application will continue to function but traces will not be reported.

ZStream integration​

ZTracer can also be used to trace ZStreams in tandem with the Spanned datatype where each element of the ZStream has an associated span (i.e. Kafka messages). For example:

import zio.*
import zio.stream.*
import io.kaizensolutions.trace4cats.zio.extras.*
import trace4cats.*

// create a stream where each element is associated with a trace header
val stream: ZStream[ZTracer, Nothing, (Int, TraceHeaders)] =
ZStream.range(1, 100)
.mapZIO(i =>
ZTracer.withSpan(s"name-$i")(span =>
ZIO.succeed((i, span.extractHeaders(ToHeaders.standard)))
)
)

// create spans for elements in the stream
val tracedOperationStream: ZStream[ZTracer, Throwable, Spanned[Int]] =
stream.traceEachElement(element => /* we name each span based on the element*/ s"in-begin-$element") {
case (_, headers) => headers // we extract the headers from the element
}
.mapThrough(_._1) // we only care about the element, not the headers - traceEachElement will automatically add the headers back
.mapZIOTraced("Plus 1")(e =>
ZTracer.span(s"plus 1 for $e")(Console.printLine(s"Adding ${e} + 1 = ${e + 1}") *> ZIO.succeed(e + 1))
)
.mapZIOParTraced("Plus 2")(8)(e =>
ZTracer.span(s"plus 2 for $e")(
Console.printLine(s"Adding ${e} + 2 = ${e + 2}").delay(500.millis) *>
ZIO.succeed(e + 2)
)
)
.mapZIOParTraced("Plus 4")(3)(e =>
ZTracer.span(s"plus 4 for $e")(
ZTracer.spanSource()(Console.printLine(s"Adding ${e} + 4 = ${e + 4}").delay(1.second)) *>
ZIO.succeed(e + 2)
)
)

Running the stream above, we'd expect to see something along the lines of: Jaegar span

Have a look at the example if you want to learn more.

Log Context Enrichment​

ZTracer automatically enriches the ZIO log context with trace information via LogContextExtractor. This means any ZIO.log* calls within a traced span will include trace/span IDs in the log output without any additional configuration.

By default, ZTracer.layer uses LogContextExtractor.default which adds OpenTelemetry-compatible trace_id and span_id annotations to the log context. You can customize this behavior when constructing the tracer layer:

import io.kaizensolutions.trace4cats.zio.extras.{LogContextExtractor, ZTracer, ZEntryPoint}
import cats.Show
import trace4cats.model.{SpanContext, SpanId, TraceId}
import zio.*

// Use the default (OTel-compatible trace_id and span_id)
val defaultLayer: URLayer[ZEntryPoint, ZTracer] = ZTracer.layer

// Customize the log annotations
val customLayer: URLayer[ZEntryPoint, ZTracer] = ZTracer.layerWith(
logContextExtractor = (spanContext: SpanContext) => Set(
LogAnnotation("myTraceId", Show[TraceId].show(spanContext.traceId)),
LogAnnotation("mySpanId", Show[SpanId].show(spanContext.spanId))
)
)

// Disable log enrichment entirely
val noEnrichmentLayer: URLayer[ZEntryPoint, ZTracer] = ZTracer.layerWith(LogContextExtractor.none)