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
29 changes: 28 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 ++
otel4z.projectRefs ++
json4s.projectRefs ++
playJson.projectRefs ++
play29Json.projectRefs ++
Expand Down Expand Up @@ -1179,6 +1181,30 @@ lazy val otel4sMetrics: ProjectMatrix = (projectMatrix in file("metrics/otel4s-m
.jvmPlatform(scalaVersions = scala2_13And3Versions, settings = commonJvmSettings)
.dependsOn(serverCore % CompileAndTest, catsEffect % Test)

lazy val otel4z: ProjectMatrix = (projectMatrix in file("observability/otel4z"))
.dependsOn(zio, zioHttpServer, opentelemetryMetrics)
.settings(commonSettings)
.settings(
name := "tapir-otel4z",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-logging" % Versions.zioLogging,
"dev.zio" %% "zio-logging-slf4j2" % Versions.zioLogging,
"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,
"io.opentelemetry.instrumentation" % "opentelemetry-runtime-telemetry-java17" % Versions.openTelemetryRuntime,
"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 @@ -2361,7 +2387,8 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
vertxServer,
zioHttpServer,
zioJson,
zioMetrics
zioMetrics,
otel4z
)

//TODO this should be invoked by compilation process, see #https://github.com/scalameta/mdoc/issues/355
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// {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.18
//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.13.18
//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.13.18
//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.18
//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.13.18
//> using dep com.softwaremill.sttp.tapir::tapir-otel4z:1.13.18

package sttp.tapir.examples.observability

import io.opentelemetry.api.OpenTelemetry

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

import zio._
import zio.http._
import zio.telemetry.opentelemetry.metrics.Meter
import zio.telemetry.opentelemetry.tracing.Tracing
import sttp.tapir.server.interceptor.cors.CORSInterceptor




/** 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") 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.
*/



// 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](
Scope.default,
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),

ZIOpenTelemetry.runtimeTelemetry

)


/** 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(
ZIOtelTracing(tracing)
)
.appendInterceptor(
CORSInterceptor.default
)
.appendInterceptor(metricsInterceptor)
.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 ()
)
}

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,16 @@
package sttp.tapir.server.o11y.otel4z

import zio.ZIOApp

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

trait ZIOpenTelemetryFull extends ZIOtelBase with Metrics with Traces {
this: ZIOApp =>
}

object ZIOpenTelemetry extends ZIOpenTelemetryBase
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package sttp.tapir.server.o11y.otel4z

import zio.ZIOApp

/** ZIOpenTelemetry is a trait that provides a ZIO layer for OpenTelemetry.
* @param name
*/
trait ZIOpenTelemetry(val resourceName: String) extends ZIOtelBase {
this: ZIOApp =>
}

trait ZIOpenTelemetryFull(val resourceName: String) extends ZIOtelBase with Metrics with Traces {
this: ZIOApp =>
}


object ZIOpenTelemetry extends ZIOpenTelemetryBase
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package sttp.tapir.server.o11y.otel4z

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.semconv.ServiceAttributes
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter

object LoggerProvider extends OtlpEndpoint {

/** gRPC exporter that sends logs to the endpoint specified in the environment variable.
*
* If the environment variable is not set, will fallback OTEL_EXPORTER_OTLP_ENDPOINT env var or "http://localhost:4317".
*/
def grpc(resourceName: String): URIO[Scope, Option[SdkLoggerProvider]] =
for {
logRecordExporter <-
ZIO.fromAutoCloseable(
ZIO.succeed(
OtlpGrpcLogRecordExporter
.builder()
.setEndpoint(getEndpoint("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"))
.build()
)
)
logRecordProcessor <-
ZIO.fromAutoCloseable(
ZIO.succeed(SimpleLogRecordProcessor.create(logRecordExporter))
)
loggerProvider <-
ZIO.fromAutoCloseable(
ZIO.succeed(
SdkLoggerProvider
.builder()
.setResource(
Resource.create(
Attributes.of(ServiceAttributes.SERVICE_NAME, resourceName)
)
)
.addLogRecordProcessor(logRecordProcessor)
.build()
)
)
} yield Some(loggerProvider)

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

import zio._
import io.opentelemetry.sdk.metrics.SdkMeterProvider
import io.opentelemetry.sdk.metrics.`export`.PeriodicMetricReader
import io.opentelemetry.sdk.resources.Resource
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.semconv.ServiceAttributes
import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingMetricExporter
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter

object MeterProvider extends OtlpEndpoint {

/** Prints to stdout in OTLP Json format
*/
def stdout(resourceName: String): RIO[Scope, SdkMeterProvider] =
for {
metricExporter <- ZIO.fromAutoCloseable(
ZIO.succeed(OtlpJsonLoggingMetricExporter.create())
)
metricReader <-
ZIO.fromAutoCloseable(
ZIO.succeed(
PeriodicMetricReader
.builder(metricExporter)
.setInterval(5.second)
.build()
)
)
meterProvider <-
ZIO.fromAutoCloseable(
ZIO.succeed(
SdkMeterProvider
.builder()
.registerMetricReader(metricReader)
.setResource(
Resource.create(
Attributes.of(ServiceAttributes.SERVICE_NAME, resourceName)
)
)
.build()
)
)
} yield meterProvider

/** gRPC exporter that sends metrics to the endpoint specified in the environment variable.
*
* If the environment variable is not set, will fallback OTEL_EXPORTER_OTLP_ENDPOINT env var or "http://localhost:4317".
*/
def grpc(resourceName: String): URIO[Scope, Option[SdkMeterProvider]] =
for {
metricExporter <- ZIO.fromAutoCloseable(
ZIO.succeed(OtlpGrpcMetricExporter.builder().setEndpoint(getEndpoint("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT")).build())
)
metricReader <-
ZIO.fromAutoCloseable(
ZIO.succeed(
PeriodicMetricReader
.builder(metricExporter)
.setInterval(5.second)
.build()
)
)
meterProvider <-
ZIO.fromAutoCloseable(
ZIO.succeed(
SdkMeterProvider
.builder()
.registerMetricReader(metricReader)
.setResource(
Resource.create(
Attributes.of(ServiceAttributes.SERVICE_NAME, resourceName)
)
)
.build()
)
)
} yield Some(meterProvider)

}
Loading
Loading