Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
66758e0
Add ZIO OpenTelemetry tracing support with new providers and configur…
cheleb May 1, 2026
3e4ebb6
Refactor formatting and indentation in Providers.scala for improved r…
cheleb May 15, 2026
cc5dc25
Renaming
cheleb May 16, 2026
94ddded
Remove unnecessary Scope.default reference in ZIOpenTelemetryExample …
cheleb May 16, 2026
5f41449
docs(observability): add ZIO OpenTelemetry documentation
cheleb May 16, 2026
69b4c1e
feat(observability): add ZIO OpenTelemetry tracing example and test e…
cheleb May 16, 2026
61c5895
fix(otel4z): ensure Java 11 compatibility in ZIOtelTracingTest
cheleb May 16, 2026
506aaa8
refactor(otel4z): extract buildExporter method for cleaner metric exp…
onouguier-ledger May 19, 2026
effcf8e
Refactor observability module: Replace otel4z with zio-opentelemetry
onouguier-ledger May 21, 2026
df8b422
refactor(ziopentelemetry): replace newCarrier method with extractCarr…
cheleb May 21, 2026
1d7545d
feat(observability): add ZIO OpenTelemetry tracing tests and remove d…
cheleb May 21, 2026
cd202de
refactor(ziopentelemetry): scala 2.12 compat, extractCarrier Mutable …
cheleb May 22, 2026
bd1bdd0
refactor(observability): remove zio-logging dependencies and update l…
cheleb May 22, 2026
8fa5ddf
chore: update Tapir and ZIO dependencies to version 1.13.19 and 2.5.3
cheleb May 22, 2026
86cfaf5
refactor(observability): update documentation for Logging and Metrics…
cheleb May 23, 2026
3185f7a
refactor(observability): change otelProviders to return ULayer
cheleb May 23, 2026
1847909
build(deps): add zio-logging-slf4j2 dependency
cheleb May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Versions.zioHttp
import com.softwaremill.Publish.{ossPublishSettings, updateDocs}
import com.softwaremill.SbtSoftwareMillBrowserTestJS._
import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings
Expand Down Expand Up @@ -192,6 +193,7 @@ lazy val rawAllAggregates = core.projectRefs ++
opentelemetryTracing.projectRefs ++
otel4sMetrics.projectRefs ++
otel4sTracing.projectRefs ++
zioOpenTelemetry.projectRefs ++
json4s.projectRefs ++
playJson.projectRefs ++
play29Json.projectRefs ++
Expand Down Expand Up @@ -1179,6 +1181,26 @@ lazy val otel4sMetrics: ProjectMatrix = (projectMatrix in file("metrics/otel4s-m
.jvmPlatform(scalaVersions = scala2_13And3Versions, settings = commonJvmSettings)
.dependsOn(serverCore % CompileAndTest, catsEffect % Test)

lazy val zioOpenTelemetry: ProjectMatrix = (projectMatrix in file("observability/zio-opentelemetry"))
.dependsOn(zio, zioHttpServer, opentelemetryMetrics)
.settings(commonSettings)
.settings(
name := "tapir-zio-opentelemetry",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-opentelemetry" % Versions.zioOpenTelemetry,
"dev.zio" %% "zio-opentelemetry-zio-logging" % Versions.zioOpenTelemetry,
"io.opentelemetry.semconv" % "opentelemetry-semconv" % Versions.openTelemetrySemconvVersion,
"io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetry,
"io.opentelemetry" % "opentelemetry-exporter-otlp" % Versions.openTelemetry,
"io.opentelemetry" % "opentelemetry-exporter-logging-otlp" % Versions.openTelemetry,
"dev.zio" %% "zio-test" % Versions.zio % Test,
"dev.zio" %% "zio-test-sbt" % Versions.zio % Test,
"io.opentelemetry" % "opentelemetry-sdk-testing" % Versions.openTelemetry % Test
)
)
.jvmPlatform(scalaVersions = scala2And3Versions, settings = commonJvmSettings)
.dependsOn(serverCore % CompileAndTest)

// docs

lazy val apispecDocs: ProjectMatrix = (projectMatrix in file("docs/apispec-docs"))
Expand Down Expand Up @@ -2321,6 +2343,7 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
"io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % Versions.openTelemetry,
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % Versions.jsoniter,
"org.typelevel" %% "otel4s-oteljava" % Versions.otel4s,
"dev.zio" %% "zio-logging-slf4j2" % Versions.zioLogging,
scalaTest.value,
logback
),
Expand Down Expand Up @@ -2361,7 +2384,8 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
vertxServer,
zioHttpServer,
zioJson,
zioMetrics
zioMetrics,
zioOpenTelemetry
)

//TODO this should be invoked by compilation process, see #https://github.com/scalameta/mdoc/issues/355
Expand Down
27 changes: 26 additions & 1 deletion doc/server/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,4 +522,29 @@ might still serve the request.

If a default response (e.g. a `404 Not Found`) should be produced, this should be enabled using the
[reject interceptor](errors.md). Such a setup assumes that there are no other routes in the server, after the Tapir
server interpreter is invoked.
server interpreter is invoked.

## ZIO OpenTelemetry

ZIO OpenTelemetry integration is provided by the `otel4z` module, which uses the otel4s library under the hood. It provides both logging, tracing and metrics capabilities, as well as a runtime telemetry service for ZIO applications.


Add the following dependency:

```scala
"com.softwaremill.sttp.tapir" %% "tapir-otel4z" % "@VERSION@"
```

The `otel4z` module provides integration with the [ZIO OpenTelemetry](https://zio.dev/zio-opentelemetry/) library, which is built on top of the [OpenTelemetry](https://opentelemetry.io/) allowing you to create traces and metrics for your tapir endpoints using a purely functional API.

This module provides the following layers helpers:
- `otel4zLogging` - a layer that provides the OpenTelemetry logging interceptor, which logs incoming requests and other operations.
- `otel4zMetrics` - a layer that provides the OpenTelemetry metrics interceptor, which records metrics for incoming requests and other operations.
- `otel4zTracing` - a layer that provides the OpenTelemetry tracing interceptor, which creates spans for incoming requests and other operations.

All of these layers require an OpenTelemetry instance to be provided, but this layer to works with Zio runtime metrics must be provided during the application startup (aka bootstrap).

The ZIOpenTelemetry trait provide this bootstrap layer, which is used to create the OpenTelemetry instance and provide it to the application.

Full example of using the `otel4z` module can be found in the [ZIO OpenTelemetry example](https://tapir.softwaremill.com/en/latest/observability/ZIOpenTelemetryExample.scala)

Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// {cat=Hello, World!; effects=ZIO; server=ZIO HTTP; json=zio; docs=Swagger UI}: ZIO OpenTelemetry tracing example

//> using option -Xkind-projector
//> using dep com.softwaremill.sttp.tapir::tapir-core::1.13.19
//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.13.19
//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.13.19
//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.19
//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.13.19
//> using dep com.softwaremill.sttp.tapir::tapir-zio-opentelemetry:1.13.19
//> using dep dev.zio::zio-logging:2.5.3
//> using dep dev.zio::zio-logging-slf4j2:2.5.3
//> using dep ch.qos.logback:logback-classic:1.5.32


package sttp.tapir.examples.observability

import io.opentelemetry.api.OpenTelemetry

import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.o11y.ziopentelemetry._
import sttp.tapir.server.ziohttp._
import sttp.tapir.ztapir._

import zio._
import zio.logging.backend.SLF4J
import zio.http._
import zio.telemetry.opentelemetry.metrics.Meter
import zio.telemetry.opentelemetry.tracing.Tracing
import sttp.tapir.server.interceptor.cors.CORSInterceptor
import io.opentelemetry.api.common.Attributes





/** This example demonstrates how to use ZIO with Tapir and OpenTelemetry for tracing. It sets up a simple HTTP server with a single
* endpoint that returns "Hello, World!" and includes tracing for incoming requests.
*
* To enable tracing, we use the ZIOpenTelemetry trait, which provides a Tracing service.
*
* To effectively produce traces, you need to set the OTEL_EXPORTER_OTLP_ENDPOINT environment variable to the address of your
* OpenTelemetry.
*/
object ZIOpenTelemetryExample extends ZIOApp with ZIOpenTelemetry("zio-observability-example", Some("1.0.0"), Some("dev")) with Logging with Metrics with Traces {

/** The server options for the ZIOOpenTelemetry trait.
*
* This is a separate method pulled by the bootstrap layer, as it is used to provide the OpenTelemetry layer to the server options, which
* are provided by the ZIO application itself. This allows the OpenTelemetry layer to be used for both the server options and the main
* program.
*/

override def consoleLogLayer: ZLayer[Any, Nothing, Unit] = Runtime.removeDefaultLoggers >>> SLF4J.slf4j

override def extraAttributes: Attributes = Attributes.builder().put("stack", "zio").build()


// The main program - start the server on port 8080
val program = for
_ <- Console.printLine("Starting server on http://localhost:8080")

given OpenTelemetry <- ZIO.service[OpenTelemetry]

given Tracing <- ZIO.service[Tracing]

// m <- ZIO.service[Meter]

httpApi = ZIOHttpApi()

endpoints = httpApi.endpoints

httpApp = ZioHttpInterpreter(serverOptions).toHttp(
endpoints
)
_ <- Server.serve(httpApp)
yield ()

/** Run the program.
*
* Provide the necessary layers for the program, including the ZIOOpenTelemetry layer and the server layer.
*
* Note that if not metric are exposed by a service, the meter layer will not be used, hence provideSomeLayer have to be used to ignore
* the meter layer (part of bootstrap layer Environment):
* {{{
* override def run =
* program.provideSome[Environment](
* Server.default
* )
* }}}
*/
override def run =
program.provideSome[Environment](

Server.default,

// This layers provides sample custom metric, which will be visible in the OpenTelemetry collector and can be used to verify that the metrics are working.
TickCounter.tickRefLayer,
TickCounter.tickCounterLayer,
// This layer provides the OpenTelemetry Metrics service.
// Can be used to create custom metrics.
// Note this will be different Meter instance than the one used by the ZIO runtime or Tapir.
otel4zMetrics(resourceName),
// This layer publishes ZIO logs to OpenTelemetry, which will be correlated with traces and metrics.
otel4zLogging(resourceName),
// This layer provides the OpenTelemetry Tracing service,
// which is used to create spans for incoming requests and other operations.
otel4zTracing(resourceName),

// This layer provides the OpenTelemetry Runtime Metrics service, which is used to expose ZIO runtime metrics.
// Scope.default,
// RuntimeMetrics.otel4zRuntimeTelemetry

)




/** The server options for the ZIOpenTelemetry trait.
*
* This is the server options that will be used to run the ZIO application, hence provided by bootstrap. It includes the OpenTelemetry
* instance and the ContextStorage.
*/
private def serverOptions(implicit
otel: OpenTelemetry,
tracing: Tracing
): ZioHttpServerOptions[Any] = ZioHttpServerOptions.customiseInterceptors
.prependInterceptor(
ZIOpenTelemetryTracing(tracing)
)
.appendInterceptor(
CORSInterceptor.default
)
.appendInterceptor(otel4zMetricsInterceptor())
.serverLog(
ZioHttpServerOptions.defaultServerLog[Any]
)
.options
}



/**
* A simple counter that increments every second and is exposed as an OpenTelemetry metric.
*
* This is used to demonstrate how to create custom metrics
*/
object TickCounter {
val tickRefLayer: ULayer[Ref[Long]] =
ZLayer(
for {
ref <- Ref.make(0L)
_ <- ref
.update(_ + 1)
.repeat[Any, Long](Schedule.spaced(1.second))
.forkDaemon
} yield ref
)

// Records the number of seconds elapsed since the application startup
val tickCounterLayer: RLayer[Meter & Ref[Long], Unit] =
ZLayer.scoped(
for {
meter <- ZIO.service[Meter]
ref <- ZIO.service[Ref[Long]]
// Initialize observable counter instrument
_ <- meter.observableCounter("tick_counter") { om =>
for {
tick <- ref.get
_ <- om.record(tick)
} yield ()
}
} yield ()
)
}

/*

To provide runtime metrics, you can use the OpenTelemetry Runtime Telemetry module, which is available as a separate dependency.
It provides a RuntimeTelemetry class that can be instantiated with an OpenTelemetry instance and will automatically collect and export runtime metrics.

"io.opentelemetry.instrumentation" % "opentelemetry-runtime-telemetry-java17" % <version>

object RuntimeMetrics {
def otel4zRuntimeTelemetry = ZLayer.fromZIO(
for {
openTelemetry <- ZIO.service[io.opentelemetry.api.OpenTelemetry]
_ <- ZIO.fromAutoCloseable(ZIO.succeed(RuntimeTelemetry.create(openTelemetry)))
} yield ()
)
}
*/

class ZIOHttpApi(using tracing: Tracing) {

val helloEndpoint: ServerEndpoint[Any, Task] = sttp.tapir.endpoint.get
.in("hello")
.out(stringBody)
.zServerLogic(_ =>
ZIO.logInfo("Handling /hello request") *>
ZIO.succeed("Hello, World!") @@ tracing.aspects.span("hello-logic"))

val endpoints: List[ServerEndpoint[Any, Task]] = List(helloEndpoint)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package sttp.tapir.server.o11y.ziopentelemetry

import zio.ZIOApp

/** ZIOpenTelemetry is a trait that provides a ZIO layer for OpenTelemetry.
* @param name
*/
trait ZIOpenTelemetry extends ZIOpenTelemetryBase {
this: ZIOApp =>

def version: Option[String] = None

def environment: Option[String] = None
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package sttp.tapir.server.o11y.ziopentelemetry

import zio.ZIOApp
import sttp.tapir.server.o11y.ziopentelemetry.ZIOpenTelemetryBase

/** ZIOpenTelemetry is a trait that provides a ZIO layer for OpenTelemetry.
* @param name
*/
trait ZIOpenTelemetry(val resourceName: String, val version: Option[String] = None, val environment: Option[String] = None)
extends ZIOpenTelemetryBase {
this: ZIOApp =>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package sttp.tapir.server.o11y.ziopentelemetry

import io.opentelemetry.api.common.Attributes

import io.opentelemetry.sdk.logs.SdkLoggerProvider
import io.opentelemetry.sdk.logs.`export`.SimpleLogRecordProcessor
import io.opentelemetry.sdk.resources.Resource
import zio._

import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter

/** Provides a logger provider for OpenTelemetry.
*/
object LoggerProvider {

/** Provides a logger provider for OpenTelemetry, which logs in OTLP Json format as gRPC if either of the following environment variables
* is set:
* - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`
* - `OTEL_EXPORTER_OTLP_ENDPOINT`
*/
def grpc(attributes: Attributes): URIO[Scope, Option[SdkLoggerProvider]] = OtlpEndpoint("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") match {
case None =>
ZIO.logInfo(
"No OTLP logs endpoint configured, skipping OpenTelemetry logging setup. To enable it, set either OTEL_EXPORTER_OTLP_LOGS_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT environment variable."
) *> ZIO.succeed(None)

case Some(endpoint) =>
for {
_ <- ZIO.logInfo(s"Configuring OpenTelemetry logging to $endpoint")
logRecordExporter <-
ZIO.fromAutoCloseable(
ZIO.succeed(
OtlpGrpcLogRecordExporter
.builder()
.setEndpoint(endpoint)
.build()
)
)
logRecordProcessor <-
ZIO.fromAutoCloseable(
ZIO.succeed(SimpleLogRecordProcessor.create(logRecordExporter))
)
loggerProvider <-
ZIO.fromAutoCloseable(
ZIO.succeed(
SdkLoggerProvider
.builder()
.setResource(
Resource.create(
attributes
)
)
.addLogRecordProcessor(logRecordProcessor)
.build()
)
)
} yield Some(loggerProvider)

}
}
Loading
Loading