From 925e30c2ecf2cdab80e8c23207fd12b678afd45d Mon Sep 17 00:00:00 2001 From: Jacek Kunicki Date: Thu, 28 Sep 2023 10:40:28 +0200 Subject: [PATCH 1/7] Add OpenTelemetry metrics example --- build.sbt | 3 + .../OpenTelemetryMetricsExample.scala | 93 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala diff --git a/build.sbt b/build.sbt index 7a4548378a..3032e39209 100644 --- a/build.sbt +++ b/build.sbt @@ -2037,6 +2037,9 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples")) "com.github.jwt-scala" %% "jwt-circe" % Versions.jwtScala, "org.mock-server" % "mockserver-netty" % Versions.mockServer, "io.circe" %% "circe-generic-extras" % Versions.circeGenericExtras, + "io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetry, + "io.opentelemetry" % "opentelemetry-sdk-metrics" % Versions.openTelemetry, + "io.opentelemetry" % "opentelemetry-exporter-otlp" % Versions.openTelemetry, scalaTest.value ), libraryDependencies ++= loggerDependencies, diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala new file mode 100644 index 0000000000..93c2845899 --- /dev/null +++ b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala @@ -0,0 +1,93 @@ +package sttp.tapir.examples.observability + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Route +import com.typesafe.scalalogging.StrictLogging +import io.circe.generic.auto._ +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.metrics.SdkMeterProvider +import io.opentelemetry.sdk.metrics.`export`.PeriodicMetricReader +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.json.circe.jsonBody +import sttp.tapir.server.ServerEndpoint +import sttp.tapir.server.akkahttp.{AkkaHttpServerInterpreter, AkkaHttpServerOptions} +import sttp.tapir.server.metrics.opentelemetry.OpenTelemetryMetrics + +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} + +/** This example uses a gRPC exporter to send metrics to a collector, which by + * default is expected to be running on `localhost:4317`. + * + * You can run a collector locally using Docker with the following command: + * {{{ + * docker run -p 4317:4317 otel/opentelemetry-collector:latest + * }}} + * + * Please refer to the exporter + * configuration if you need to use a different host/port. + * + * Once this example app and the collector are running, and after you send some requests to the `/person` endpoint, you should start seeing + * the metrics in the collector logs (look for `InstrumentationScope tapir 1.0.0`), e.g.: + * {{{ + * InstrumentationScope tapir 1.0.0 + * Metric #0 + * Descriptor: + * -> Name: request_active + * -> Description: Active HTTP requests + * -> Unit: 1 + * -> DataType: Sum + * -> IsMonotonic: false + * -> AggregationTemporality: Cumulative + * + * ... + * }}} + */ +object OpenTelemetryMetricsExample extends App with StrictLogging { + implicit val actorSystem: ActorSystem = ActorSystem() + import actorSystem.dispatcher + + case class Person(name: String) + + // Simple endpoint returning 200 or 400 response with string body + val personEndpoint: ServerEndpoint[Any, Future] = + endpoint.post + .in("person") + .in(jsonBody[Person]) + .out(stringBody) + .errorOut(stringBody) + .serverLogic { p => + Thread.sleep(3000) + Future.successful(Either.cond(p.name == "Jacob", "Welcome", "Unauthorized")) + } + + // An exporter that sends metrics to a collector over gRPC + val grpcExporter = OtlpGrpcMetricExporter.builder().build() + + // A metric reader that exports using the gRPC exporter + val metricReader: PeriodicMetricReader = PeriodicMetricReader.builder(grpcExporter).build() + + // A meter registry whose meters are read by the above reader + val meterProvider: SdkMeterProvider = SdkMeterProvider.builder().registerMetricReader(metricReader).build() + + // An instance of OpenTelemetry using the above meter registry + val otel: OpenTelemetry = OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build() + + val openTelemetryMetrics = OpenTelemetryMetrics.default[Future](otel) + + val serverOptions: AkkaHttpServerOptions = + AkkaHttpServerOptions.customiseInterceptors + // Adds an interceptor which collects metrics by executing callbacks + .metricsInterceptor(openTelemetryMetrics.metricsInterceptor()) + .options + + val routes: Route = AkkaHttpServerInterpreter(serverOptions).toRoute(personEndpoint) + + Await.result(Http().newServerAt("localhost", 8080).bindFlow(routes), 1.minute) + + logger.info(s"Server started. POST persons under http://localhost:8080/person.") +} From da9867ddbcd5a943434a44ffd5ba1dc5fe9e19ef Mon Sep 17 00:00:00 2001 From: Jacek Kunicki Date: Mon, 2 Oct 2023 11:43:58 +0200 Subject: [PATCH 2/7] Add required dependencies to OpenTelemetry example instructions --- .../observability/OpenTelemetryMetricsExample.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala index 93c2845899..6546313629 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala @@ -31,6 +31,17 @@ import scala.concurrent.{Await, Future} * Please refer to the exporter * configuration if you need to use a different host/port. * + * The example code requires the following dependencies: + * {{{ + * val openTelemetryVersion = + * + * libraryDependencies ++= Seq( + * "io.opentelemetry" % "opentelemetry-sdk" % openTelemetryVersion, + * "io.opentelemetry" % "opentelemetry-sdk-metrics" % openTelemetryVersion, + * "io.opentelemetry" % "opentelemetry-exporter-otlp" % openTelemetryVersion + * ) + * }}} + * * Once this example app and the collector are running, and after you send some requests to the `/person` endpoint, you should start seeing * the metrics in the collector logs (look for `InstrumentationScope tapir 1.0.0`), e.g.: * {{{ From 00d6ad3b40c966706203e9bc3c65d7430e42d08b Mon Sep 17 00:00:00 2001 From: Jacek Kunicki Date: Mon, 2 Oct 2023 13:28:07 +0200 Subject: [PATCH 3/7] Fix formatting --- .../examples/observability/OpenTelemetryMetricsExample.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala index 6546313629..2fdc9df02a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala @@ -39,7 +39,7 @@ import scala.concurrent.{Await, Future} * "io.opentelemetry" % "opentelemetry-sdk" % openTelemetryVersion, * "io.opentelemetry" % "opentelemetry-sdk-metrics" % openTelemetryVersion, * "io.opentelemetry" % "opentelemetry-exporter-otlp" % openTelemetryVersion - * ) + * ) * }}} * * Once this example app and the collector are running, and after you send some requests to the `/person` endpoint, you should start seeing From ff1c0d706153919d6be0ba0fb7230b570a037738 Mon Sep 17 00:00:00 2001 From: Jacek Kunicki Date: Mon, 2 Oct 2023 13:28:59 +0200 Subject: [PATCH 4/7] Use Netty instead of Akka HTTP --- .../observability/OpenTelemetryMetricsExample.scala | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala index 2fdc9df02a..8faeb4aa5c 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala @@ -1,8 +1,6 @@ package sttp.tapir.examples.observability import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.server.Route import com.typesafe.scalalogging.StrictLogging import io.circe.generic.auto._ import io.opentelemetry.api.OpenTelemetry @@ -14,8 +12,8 @@ import sttp.tapir._ import sttp.tapir.generic.auto._ import sttp.tapir.json.circe.jsonBody import sttp.tapir.server.ServerEndpoint -import sttp.tapir.server.akkahttp.{AkkaHttpServerInterpreter, AkkaHttpServerOptions} import sttp.tapir.server.metrics.opentelemetry.OpenTelemetryMetrics +import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerOptions} import scala.concurrent.duration._ import scala.concurrent.{Await, Future} @@ -90,15 +88,13 @@ object OpenTelemetryMetricsExample extends App with StrictLogging { val openTelemetryMetrics = OpenTelemetryMetrics.default[Future](otel) - val serverOptions: AkkaHttpServerOptions = - AkkaHttpServerOptions.customiseInterceptors + val serverOptions: NettyFutureServerOptions = + NettyFutureServerOptions.customiseInterceptors // Adds an interceptor which collects metrics by executing callbacks .metricsInterceptor(openTelemetryMetrics.metricsInterceptor()) .options - val routes: Route = AkkaHttpServerInterpreter(serverOptions).toRoute(personEndpoint) - - Await.result(Http().newServerAt("localhost", 8080).bindFlow(routes), 1.minute) + Await.ready(NettyFutureServer().port(8080).addEndpoint(personEndpoint, serverOptions).start(), 1.minute) logger.info(s"Server started. POST persons under http://localhost:8080/person.") } From f32af4232c38a8d3595f16e770b704e923f0b3a1 Mon Sep 17 00:00:00 2001 From: Jacek Kunicki Date: Tue, 3 Oct 2023 09:42:01 +0200 Subject: [PATCH 5/7] Remove actor system --- .../examples/observability/OpenTelemetryMetricsExample.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala index 8faeb4aa5c..84a77d9f85 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala @@ -1,6 +1,5 @@ package sttp.tapir.examples.observability -import akka.actor.ActorSystem import com.typesafe.scalalogging.StrictLogging import io.circe.generic.auto._ import io.opentelemetry.api.OpenTelemetry @@ -57,8 +56,6 @@ import scala.concurrent.{Await, Future} * }}} */ object OpenTelemetryMetricsExample extends App with StrictLogging { - implicit val actorSystem: ActorSystem = ActorSystem() - import actorSystem.dispatcher case class Person(name: String) From 57441b9b40d3beb1102dc2586c973bf97263ebbb Mon Sep 17 00:00:00 2001 From: Jacek Kunicki Date: Tue, 3 Oct 2023 09:47:08 +0200 Subject: [PATCH 6/7] Add ExecutionContext --- .../examples/observability/OpenTelemetryMetricsExample.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala index 84a77d9f85..6b8a0de31a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala @@ -14,6 +14,7 @@ import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.metrics.opentelemetry.OpenTelemetryMetrics import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerOptions} +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.concurrent.{Await, Future} From fe19dec09a72a3a5677544267869368f3b937c97 Mon Sep 17 00:00:00 2001 From: Jacek Kunicki Date: Tue, 3 Oct 2023 10:13:42 +0200 Subject: [PATCH 7/7] Add example cURL --- .../examples/observability/OpenTelemetryMetricsExample.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala index 6b8a0de31a..37f3476ac9 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala @@ -94,5 +94,5 @@ object OpenTelemetryMetricsExample extends App with StrictLogging { Await.ready(NettyFutureServer().port(8080).addEndpoint(personEndpoint, serverOptions).start(), 1.minute) - logger.info(s"Server started. POST persons under http://localhost:8080/person.") + logger.info(s"""Server started. Try it with: curl -X POST localhost:8080/person -d '{"name": "Jacob"}'""") }