Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 23 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,24 @@ 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-test" % Versions.zio % Test,
"dev.zio" %% "zio-test-sbt" % Versions.zio % Test,
"io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetry,
"io.opentelemetry" % "opentelemetry-exporter-otlp" % Versions.openTelemetry,
"io.opentelemetry" % "opentelemetry-exporter-logging-otlp" % Versions.openTelemetry,
"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 +2341,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 +2382,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,115 @@
// {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 io.opentelemetry.semconv:opentelemetry-semconv:1.41.0
//> using dep io.opentelemetry:opentelemetry-sdk:1.62.0
//> using dep io.opentelemetry:opentelemetry-exporter-otlp:1.62.0
//> using dep io.opentelemetry:opentelemetry-exporter-logging-otlp:1.62.0
//> using dep dev.zio::zio-logging:2.5.3
//> using dep dev.zio::zio-logging-slf4j2:2.5.3
//> using dep dev.zio::zio-opentelemetry-zio-logging:3.1.17
//> using dep ch.qos.logback:logback-classic:1.5.32

package sttp.tapir.examples.observability

import io.opentelemetry.api
import io.opentelemetry.api.common.Attributes

import sttp.tapir.server.interceptor.cors.CORSInterceptor
import sttp.tapir.server.ziopentelemetry.*
import sttp.tapir.server.ziohttp.*
import sttp.tapir.ztapir.*

import zio.*
import zio.http.*
import zio.logging.backend.SLF4J
import zio.telemetry.opentelemetry.metrics.Meter
import zio.telemetry.opentelemetry.tracing.Tracing
import sttp.tapir.server.ServerEndpoint

/** 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 ZIOpenTelemetryDefaultExample extends ZIOtelAppDefault("zio-observability-default", Some("1.0.0"), Some("dev")) {
override def consoleLogLayer: ZLayer[Any, Nothing, Unit] = Runtime.removeDefaultLoggers >>> SLF4J.slf4j
override def extraAttributes: Attributes = Attributes.builder().put("stack", "zio").build()

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

given api.OpenTelemetry <- ZIO.service[api.OpenTelemetry]

given Tracing <- ZIO.service[Tracing]

_ <- ZIO.service[Meter]

endpoints = ZIOHttpApiDefault.endpoints

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

override def run =
program

/** 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: api.OpenTelemetry,
tracing: Tracing
): ZioHttpServerOptions[Any] = ZioHttpServerOptions.customiseInterceptors
.prependInterceptor(
ZIOpenTelemetryTracing(tracing)
)
.appendInterceptor(
CORSInterceptor.default
)
.appendInterceptor(otel4zMetricsInterceptor())
.serverLog(
ZioHttpServerOptions.defaultServerLog[Any]
)
.options

class ZIOHttpApiDefault(using tracing: Tracing) {

import tracing.aspects.*

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

}
object ZIOHttpApiDefault {

def endpoints(using tracing: Tracing): List[ServerEndpoint[Any, Task]] =

val api = new ZIOHttpApiDefault(using tracing)

List(api.helloEndpoint)
}

}
Loading
Loading