From 11a6717d2e175a2fd99af20a60e867be72e80167 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Sat, 4 Nov 2023 10:56:06 +0200 Subject: [PATCH 1/2] site: add `Tracing - interop with Java-instrumented libraries` page --- docs/instrumentation/directory.conf | 1 + docs/instrumentation/tracing-java-interop.md | 173 +++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 docs/instrumentation/tracing-java-interop.md diff --git a/docs/instrumentation/directory.conf b/docs/instrumentation/directory.conf index 4503b4d76..aa495bb29 100644 --- a/docs/instrumentation/directory.conf +++ b/docs/instrumentation/directory.conf @@ -2,4 +2,5 @@ laika.title = Instrumentation laika.navigationOrder = [ tracing + tracing-java-interop ] diff --git a/docs/instrumentation/tracing-java-interop.md b/docs/instrumentation/tracing-java-interop.md new file mode 100644 index 000000000..ee01cf299 --- /dev/null +++ b/docs/instrumentation/tracing-java-interop.md @@ -0,0 +1,173 @@ +# Tracing - interop with Java-instrumented libraries + +### Glossary + +| Name | Description | +|------------------------------------------|--------------------------------------------------------------| +| [Context][otel4s-context] | otel4s context that carries tracing information (spans, etc) | +| [Local{F, Context}][cats-mtl-local] | The context carrier tool within the effect environment | +| [OpenTelemetry Java][opentelemetry-java] | The OpenTelemetry library for Java | +| [JContext][opentelemetry-java-context] | Alias for `io.opentelemetry.context.Context` | +| [JSpan][opentelemetry-java-span] | Alias for `io.opentelemetry.api.trace.Span` | + +## The problem + +[OpenTelemetry Java][opentelemetry-java] and otel4s rely on different context manipulation approaches, +which aren't interoperable out of the box. +OpenTelemetry Java utilizes ThreadLocal variables to share tracing information, +otel4s, on the other hand, uses [Local][cats-mtl-local]. + +Let's take a look at example below: +```scala mdoc:silent +import cats.effect.IO +import org.typelevel.otel4s.trace.Tracer +import io.opentelemetry.api.trace.{Span => JSpan} + +def test(implicit tracer: Tracer[IO]): IO[Unit] = + tracer.span("test").use { span => // start 'test' span using otel4s + val jSpanContext = JSpan.current().getSpanContext // get a span from a ThreadLocal var + IO.println(s"Java ctx: $jSpanContext") >> IO.println(s"Otel4s ctx: ${span.context}") + } +``` + +The output will be: +``` +Java ctx: {traceId=00000000000000000000000000000000, spanId=0000000000000000, ...} +Otel4s ctx: {traceId=318854a5bd6ac0dd7b0a926f89c97ecb, spanId=925ad3a126cec272, ...} +``` + +Here we try to get the current `JSpan` within the effect. +Unfortunately, due to different context manipulation approaches, +the context operated by otel4s isn't visible to the OpenTelemetry Java. + +To mitigate this limitation, the context must be shared manually. + +## Before we start + +Since we need to manually modify the context we need direct access to `Local[F, Context]`. +It can be constructed in the following way: + +```scala mdoc:silent +import cats.effect._ +import cats.mtl.Local +import org.typelevel.otel4s.java.context.Context +import org.typelevel.otel4s.java.OtelJava +import org.typelevel.otel4s.java.instances._ // brings Local derived from IOLocal +import io.opentelemetry.api.GlobalOpenTelemetry + +def createOtel4s[F[_]: Async](implicit L: Local[F, Context]): Resource[F, OtelJava[F]] = + Resource + .eval(Async[F].delay(GlobalOpenTelemetry.get)) + .map(OtelJava.local[F]) + +def program[F[_]: Async](otel4s: OtelJava[F])(implicit L: Local[F, Context]): F[Unit] = { + val _ = (otel4s, L) // both OtelJava and Local[F, Context] are available here + Async[F].unit +} + +val run: IO[Unit] = + IOLocal(Context.root).flatMap { implicit ioLocal: IOLocal[Context] => + createOtel4s[IO].use(otel4s => program(otel4s)) + } +``` + +## How to use OpenTelemetry Java context with otel4s + +There are several scenarios when you want to run an effect with an explicit OpenTelemetry Java context. +For example, when you need to materialize an effect inside [Play Framework][play-framework] request handler. + +To make it work, we can define a utility method: +```scala mdoc:silent:reset +import cats.mtl.Local +import org.typelevel.otel4s.java.context.Context +import io.opentelemetry.context.{Context => JContext} + +def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context]): F[A] = + Local[F, Context].scope(fa)(Context.wrap(ctx)) +``` + +1) `Context.wrap(ctx)` - creates otel4s context from the `JContext` +2) `Local[F, Context].scope` - sets the given context as an active environment for the effect `fa` + +_____ + +Let's say you use [Play Framework][play-framework] and want to materialize an `IO` using the current tracing context: +```scala +class Application @Inject() (implicit + cc: ControllerComponents, + tracer: Tracer[IO], + local: Local[IO, Cointext] +) extends AbstractController(cc) { + + def findUser(userId: Long) = Action { + val current = JContext.current() // get current JContext + withJContext(current)(search(userId)).unsafeRunSync() // materialize IO + } + + def search(userId: Long): IO[String] = + tracer.span("find-user").surround { + IO.pure("the-result") + } + + def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context]): F[A] = + Local[F, Context].scope(fa)(Context.wrap(ctx)) +} +``` + +## How to use otel4s context with OpenTelemetry Java + +To interoperate with Java libraries that rely on the OpenTelemetry Java context, you need to activate the context manually. +The following utility method allows you to extract the current otel4s context and set it into the ThreadLocal variable: + +```scala mdoc:silent:reset +import cats.effect.Sync +import cats.mtl.Local +import cats.syntax.flatMap._ +import org.typelevel.otel4s.java.context.Context +import io.opentelemetry.context.{Context => JContext} +import scala.util.Using + +def withJContext[F[_]: Sync, A](use: JContext => A)(implicit L: Local[F, Context]): F[A] = + Local[F, Context].ask.flatMap { ctx => // <1> + Sync[F].defer { + Sync[F].fromTry { + val jContext: JContext = ctx.underlying // <2> + Using(jContext.makeCurrent())(_ => use(jContext)) // <3> + } + } + } +``` + +1) `Local[F, Context].ask` - get the current otel4s context +2) `ctx.underlying` - unwrap otel4s context and get `JContext` +3) `Using(jContext.makeCurrent())` - activate `JContext` within the current thread and run `use` afterward + +**Note:** here we use `Sync[F].defer` and `Sync[F].fromTry` to handle the side effects. +Depending on your use case, you may prefer `Sync[F].interruptible` or `Sync[F].blocking`. + +Now we can run a slightly modified original 'problematic' example: +```scala +tracer.span("test").use { span => // start 'test' span using otel4s + IO.println(s"Otel4s ctx: ${span.context}") >> withJContext[IO, Unit] { _ => + val jSpanContext = JSpan.current().getSpanContext // get a span from the ThreadLocal variable + println(s"Java ctx: $jSpanContext") + } +} +``` + +The output will be: +``` +Java ctx: {traceId=06f5d9112efbe711947ebbded1287a30, spanId=26ed80c398cc039f, ...} +Otel4s ctx: {traceId=06f5d9112efbe711947ebbded1287a30, spanId=26ed80c398cc039f, ...} +``` + +As we can see, the tracing information is in sync now, +and you can use Java-instrumented libraries within the `withJContext` block. + +[opentelemetry-java]: https://github.com/open-telemetry/opentelemetry-java +[opentelemetry-java-autoconfigure]: https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md +[opentelemetry-java-context]: https://github.com/open-telemetry/opentelemetry-java/blob/main/context/src/main/java/io/opentelemetry/context/Context.java +[opentelemetry-java-span]: https://github.com/open-telemetry/opentelemetry-java/blob/main/api/all/src/main/java/io/opentelemetry/api/trace/Span.java +[otel4s-context]: https://github.com/typelevel/otel4s/blob/main/java/common/src/main/scala/org/typelevel/otel4s/java/context/Context.scala +[cats-mtl-local]: https://typelevel.org/cats-mtl/mtl-classes/local.html +[play-framework]: https://github.com/playframework/playframework From eb16ba7efa07d7f0f8a442935ec6cc73c64e0d51 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Sat, 4 Nov 2023 14:13:44 +0200 Subject: [PATCH 2/2] add Pekko HTTP example --- build.sbt | 14 +- .../histogram-custom-buckets/README.md | 4 +- docs/instrumentation/tracing-java-interop.md | 143 ++++++++----- .../src/main/scala/PekkoHttpExample.scala | 191 ++++++++++++++++++ project/plugins.sbt | 1 + 5 files changed, 294 insertions(+), 59 deletions(-) create mode 100644 examples/src/main/scala/PekkoHttpExample.scala diff --git a/build.sbt b/build.sbt index fb88a8105..cfe302235 100644 --- a/build.sbt +++ b/build.sbt @@ -37,6 +37,8 @@ val MUnitCatsEffectVersion = "2.0.0-M3" val MUnitDisciplineVersion = "2.0.0-M3" val OpenTelemetryVersion = "1.31.0" val OpenTelemetrySemConvVersion = "1.21.0-alpha" +val PekkoStreamVersion = "1.0.1" +val PekkoHttpVersion = "1.0.0" val PlatformVersion = "1.0.2" val ScodecVersion = "1.1.38" val VaultVersion = "3.5.0" @@ -346,17 +348,21 @@ lazy val benchmarks = project .settings(scalafixSettings) lazy val examples = project - .enablePlugins(NoPublishPlugin) + .enablePlugins(NoPublishPlugin, JavaAgent) .in(file("examples")) .dependsOn(core.jvm, java) .settings( name := "otel4s-examples", libraryDependencies ++= Seq( + "org.apache.pekko" %% "pekko-stream" % PekkoStreamVersion, + "org.apache.pekko" %% "pekko-http" % PekkoHttpVersion, "io.opentelemetry" % "opentelemetry-exporter-otlp" % OpenTelemetryVersion, "io.opentelemetry" % "opentelemetry-sdk" % OpenTelemetryVersion, "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % OpenTelemetryVersion, - "io.opentelemetry" % "opentelemetry-extension-trace-propagators" % OpenTelemetryVersion % Runtime + "io.opentelemetry" % "opentelemetry-extension-trace-propagators" % OpenTelemetryVersion % Runtime, + "io.opentelemetry.instrumentation" % "opentelemetry-instrumentation-annotations" % OpenTelemetryVersion ), + javaAgents += "io.opentelemetry.javaagent" % "opentelemetry-javaagent" % OpenTelemetryVersion % Runtime, run / fork := true, javaOptions += "-Dotel.java.global-autoconfigure.enabled=true", envVars ++= Map( @@ -372,7 +378,9 @@ lazy val docs = project .dependsOn(java) .settings( libraryDependencies ++= Seq( - "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % OpenTelemetryVersion + "org.apache.pekko" %% "pekko-http" % PekkoHttpVersion, + "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % OpenTelemetryVersion, + "io.opentelemetry.instrumentation" % "opentelemetry-instrumentation-annotations" % OpenTelemetryVersion ), mdocVariables ++= Map( "OPEN_TELEMETRY_VERSION" -> OpenTelemetryVersion diff --git a/docs/customization/histogram-custom-buckets/README.md b/docs/customization/histogram-custom-buckets/README.md index fab84723e..eb4f27db6 100644 --- a/docs/customization/histogram-custom-buckets/README.md +++ b/docs/customization/histogram-custom-buckets/README.md @@ -1,7 +1,7 @@ # Histogram custom buckets By default, OpenTelemetry use the following boundary values for histogram -bucketing: {0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000}. +bucketing:[0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000]. In some cases, these boundaries don't represent the distribution of the values. For example, we expect that HTTP server latency should be somewhere between 100ms and 1s. Therefore, 2.5, 5, 7.5, and 10 seconds buckets are redundant. @@ -88,7 +88,7 @@ To select multiple instruments, a wildcard pattern can be used: `service.*.durat The view determines how the selected instruments should be changed or aggregated. -In our particular case, we create a histogram view with custom buckets: {.005, .01, .025, .05, .075, .1, .25, .5}. +In our particular case, we create a histogram view with custom buckets:[.005, .01, .025, .05, .075, .1, .25, .5]. ```scala mdoc:silent import io.opentelemetry.sdk.metrics.{Aggregation, View} diff --git a/docs/instrumentation/tracing-java-interop.md b/docs/instrumentation/tracing-java-interop.md index ee01cf299..3cc0fad29 100644 --- a/docs/instrumentation/tracing-java-interop.md +++ b/docs/instrumentation/tracing-java-interop.md @@ -1,20 +1,20 @@ # Tracing - interop with Java-instrumented libraries -### Glossary +## Glossary -| Name | Description | -|------------------------------------------|--------------------------------------------------------------| -| [Context][otel4s-context] | otel4s context that carries tracing information (spans, etc) | -| [Local{F, Context}][cats-mtl-local] | The context carrier tool within the effect environment | -| [OpenTelemetry Java][opentelemetry-java] | The OpenTelemetry library for Java | -| [JContext][opentelemetry-java-context] | Alias for `io.opentelemetry.context.Context` | -| [JSpan][opentelemetry-java-span] | Alias for `io.opentelemetry.api.trace.Span` | +| Name | Description | +|----------------------------------------|--------------------------------------------------------------| +| [Context][otel4s-context] | otel4s context that carries tracing information (spans, etc) | +| [Local[F, Context]][cats-mtl-local] | The context carrier tool within the effect environment | +| [Java SDK][opentelemetry-java] | The OpenTelemetry library for Java | +| [JContext][opentelemetry-java-context] | Alias for `io.opentelemetry.context.Context` | +| [JSpan][opentelemetry-java-span] | Alias for `io.opentelemetry.api.trace.Span` | ## The problem -[OpenTelemetry Java][opentelemetry-java] and otel4s rely on different context manipulation approaches, +[OpenTelemetry Java SDK][opentelemetry-java] and otel4s rely on different context manipulation approaches, which aren't interoperable out of the box. -OpenTelemetry Java utilizes ThreadLocal variables to share tracing information, +Java SDK utilizes ThreadLocal variables to share tracing information, otel4s, on the other hand, uses [Local][cats-mtl-local]. Let's take a look at example below: @@ -38,7 +38,7 @@ Otel4s ctx: {traceId=318854a5bd6ac0dd7b0a926f89c97ecb, spanId=925ad3a126cec272, Here we try to get the current `JSpan` within the effect. Unfortunately, due to different context manipulation approaches, -the context operated by otel4s isn't visible to the OpenTelemetry Java. +the context operated by otel4s isn't visible to the Java SDK. To mitigate this limitation, the context must be shared manually. @@ -50,15 +50,14 @@ It can be constructed in the following way: ```scala mdoc:silent import cats.effect._ import cats.mtl.Local +import cats.syntax.functor._ import org.typelevel.otel4s.java.context.Context import org.typelevel.otel4s.java.OtelJava import org.typelevel.otel4s.java.instances._ // brings Local derived from IOLocal import io.opentelemetry.api.GlobalOpenTelemetry -def createOtel4s[F[_]: Async](implicit L: Local[F, Context]): Resource[F, OtelJava[F]] = - Resource - .eval(Async[F].delay(GlobalOpenTelemetry.get)) - .map(OtelJava.local[F]) +def createOtel4s[F[_]: Async](implicit L: Local[F, Context]): F[OtelJava[F]] = + Async[F].delay(GlobalOpenTelemetry.get).map(OtelJava.local[F]) def program[F[_]: Async](otel4s: OtelJava[F])(implicit L: Local[F, Context]): F[Unit] = { val _ = (otel4s, L) // both OtelJava and Local[F, Context] are available here @@ -67,14 +66,14 @@ def program[F[_]: Async](otel4s: OtelJava[F])(implicit L: Local[F, Context]): F[ val run: IO[Unit] = IOLocal(Context.root).flatMap { implicit ioLocal: IOLocal[Context] => - createOtel4s[IO].use(otel4s => program(otel4s)) + createOtel4s[IO].flatMap(otel4s => program(otel4s)) } ``` -## How to use OpenTelemetry Java context with otel4s +## How to use Java SDK context with otel4s -There are several scenarios when you want to run an effect with an explicit OpenTelemetry Java context. -For example, when you need to materialize an effect inside [Play Framework][play-framework] request handler. +There are several scenarios when you want to run an effect with an explicit Java SDK context. +For example, when you need to materialize an effect inside [Pekko HTTP][pekko-http] request handler. To make it work, we can define a utility method: ```scala mdoc:silent:reset @@ -91,32 +90,60 @@ def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context] _____ -Let's say you use [Play Framework][play-framework] and want to materialize an `IO` using the current tracing context: -```scala -class Application @Inject() (implicit - cc: ControllerComponents, - tracer: Tracer[IO], - local: Local[IO, Cointext] -) extends AbstractController(cc) { - - def findUser(userId: Long) = Action { - val current = JContext.current() // get current JContext - withJContext(current)(search(userId)).unsafeRunSync() // materialize IO - } - - def search(userId: Long): IO[String] = - tracer.span("find-user").surround { - IO.pure("the-result") +Let's say you use [Pekko HTTP][pekko-http] and want to materialize an `IO` using the current tracing context: +```scala mdoc:silent:reset +import cats.effect.{Async, IO} +import cats.effect.std.Random +import cats.effect.syntax.temporal._ +import cats.effect.unsafe.implicits.global +import cats.mtl.Local +import cats.syntax.all._ +import org.apache.pekko.http.scaladsl.model.StatusCodes.OK +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server.Route +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.trace.Tracer +import org.typelevel.otel4s.java.context.Context +import io.opentelemetry.instrumentation.annotations.WithSpan +import io.opentelemetry.context.{Context => JContext} +import scala.concurrent.duration._ + +def route(implicit T: Tracer[IO], L: Local[IO, Context]): Route = + path("gen-random-name") { + get { + complete { + OK -> generateRandomName(length = 10) + } } + } - def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context]): F[A] = - Local[F, Context].scope(fa)(Context.wrap(ctx)) -} +@WithSpan("generate-random-name") +def generateRandomName(length: Int)(implicit T: Tracer[IO], L: Local[IO, Context]): String = + withJContext(JContext.current())(generate[IO](length)).unsafeRunSync() + +def generate[F[_]: Async: Tracer](length: Int): F[String] = + Tracer[F].span("generate", Attribute("length", length.toLong)).surround { + for { + random <- Random.scalaUtilRandom[F] + delay <- random.betweenInt(100, 2000) + chars <- random.nextAlphaNumeric.replicateA(length).delayBy(delay.millis) + } yield chars.mkString + } + +def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context]): F[A] = + Local[F, Context].scope(fa)(Context.wrap(ctx)) ``` -## How to use otel4s context with OpenTelemetry Java +When you invoke the `gen-random-name` endpoint, the spans will be structured in the following way: +``` +> GET { http.method = GET, http.target = /gen-random-name, ... } + > generate-random-name + > generate { length = 10 } +``` + +## How to use otel4s context with Java SDK -To interoperate with Java libraries that rely on the OpenTelemetry Java context, you need to activate the context manually. +To interoperate with Java libraries that rely on the Java SDK context, you need to activate the context manually. The following utility method allows you to extract the current otel4s context and set it into the ThreadLocal variable: ```scala mdoc:silent:reset @@ -125,14 +152,16 @@ import cats.mtl.Local import cats.syntax.flatMap._ import org.typelevel.otel4s.java.context.Context import io.opentelemetry.context.{Context => JContext} -import scala.util.Using -def withJContext[F[_]: Sync, A](use: JContext => A)(implicit L: Local[F, Context]): F[A] = +def useJContext[F[_]: Sync, A](use: JContext => A)(implicit L: Local[F, Context]): F[A] = Local[F, Context].ask.flatMap { ctx => // <1> - Sync[F].defer { - Sync[F].fromTry { - val jContext: JContext = ctx.underlying // <2> - Using(jContext.makeCurrent())(_ => use(jContext)) // <3> + Sync[F].delay { + val jContext: JContext = ctx.underlying // <2> + val scope = jContext.makeCurrent() // <3> + try { + use(jContext) + } finally { + scope.close() } } } @@ -140,15 +169,15 @@ def withJContext[F[_]: Sync, A](use: JContext => A)(implicit L: Local[F, Context 1) `Local[F, Context].ask` - get the current otel4s context 2) `ctx.underlying` - unwrap otel4s context and get `JContext` -3) `Using(jContext.makeCurrent())` - activate `JContext` within the current thread and run `use` afterward +3) `jContext.makeCurrent()` - activate `JContext` within the current thread -**Note:** here we use `Sync[F].defer` and `Sync[F].fromTry` to handle the side effects. +**Note:** we use `Sync[F].delay` to handle the side effects. Depending on your use case, you may prefer `Sync[F].interruptible` or `Sync[F].blocking`. Now we can run a slightly modified original 'problematic' example: ```scala tracer.span("test").use { span => // start 'test' span using otel4s - IO.println(s"Otel4s ctx: ${span.context}") >> withJContext[IO, Unit] { _ => + IO.println(s"Otel4s ctx: ${span.context}") >> useJContext[IO, Unit] { _ => val jSpanContext = JSpan.current().getSpanContext // get a span from the ThreadLocal variable println(s"Java ctx: $jSpanContext") } @@ -162,12 +191,18 @@ Otel4s ctx: {traceId=06f5d9112efbe711947ebbded1287a30, spanId=26ed80c398cc039f, ``` As we can see, the tracing information is in sync now, -and you can use Java-instrumented libraries within the `withJContext` block. +and you can use Java-instrumented libraries within the `useJContext` block. + +## Pekko HTTP example + +[PekkoHttpExample][pekko-http-example] is a complete example that shows how to use otel4s +with OpenTelemetry Java SDK instrumented libraries. [opentelemetry-java]: https://github.com/open-telemetry/opentelemetry-java -[opentelemetry-java-autoconfigure]: https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md -[opentelemetry-java-context]: https://github.com/open-telemetry/opentelemetry-java/blob/main/context/src/main/java/io/opentelemetry/context/Context.java -[opentelemetry-java-span]: https://github.com/open-telemetry/opentelemetry-java/blob/main/api/all/src/main/java/io/opentelemetry/api/trace/Span.java +[opentelemetry-java-autoconfigure]: https://github.com/open-telemetry/opentelemetry-java/blob/v1.31.0/sdk-extensions/autoconfigure/README.md +[opentelemetry-java-context]: https://github.com/open-telemetry/opentelemetry-java/blob/v1.31.0/context/src/main/java/io/opentelemetry/context/Context.java +[opentelemetry-java-span]: https://github.com/open-telemetry/opentelemetry-java/blob/v1.31.0/api/all/src/main/java/io/opentelemetry/api/trace/Span.java [otel4s-context]: https://github.com/typelevel/otel4s/blob/main/java/common/src/main/scala/org/typelevel/otel4s/java/context/Context.scala [cats-mtl-local]: https://typelevel.org/cats-mtl/mtl-classes/local.html -[play-framework]: https://github.com/playframework/playframework +[pekko-http]: https://pekko.apache.org/docs/pekko-http/current +[pekko-http-example]: https://github.com/typelevel/otel4s/blob/main/examples/src/main/scala/PekkoHttpExample.scala diff --git a/examples/src/main/scala/PekkoHttpExample.scala b/examples/src/main/scala/PekkoHttpExample.scala new file mode 100644 index 000000000..dc00b928e --- /dev/null +++ b/examples/src/main/scala/PekkoHttpExample.scala @@ -0,0 +1,191 @@ +/* + * Copyright 2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cats.effect.Async +import cats.effect.IO +import cats.effect.IOApp +import cats.effect.IOLocal +import cats.effect.Resource +import cats.effect.Sync +import cats.effect.std.Random +import cats.effect.syntax.temporal._ +import cats.mtl.Local +import cats.syntax.applicative._ +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.context.{Context => JContext} +import io.opentelemetry.instrumentation.annotations.WithSpan +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.model.HttpRequest +import org.apache.pekko.http.scaladsl.model.StatusCodes.OK +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.util.ByteString +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.java.OtelJava +import org.typelevel.otel4s.java.context.Context +import org.typelevel.otel4s.java.instances._ +import org.typelevel.otel4s.trace.Tracer + +import scala.concurrent.Future +import scala.concurrent.duration._ + +/** This example relies on the OpenTelemetry Java agent. To make it work, add + * the following settings to your build: + * + * add `sbt-javaagent` dependency to the `plugins.sbt`: + * + * {{{ + * addSbtPlugin("com.github.sbt" % "sbt-javaagent" % "0.1.8") + * }}} + * + * update definition of a project in the `build.sbt`: + * + * {{{ + * .enablePlugins(JavaAgent) + * .settings( + * libraryDependencies ++= Seq( + * "org.typelevel" %% "otel4s-java" % "0.3.0-RC2", + * "org.apache.pekko" %% "pekko-stream" % "1.0.1", + * "org.apache.pekko" %% "pekko-http" % "1.0.0", + * "io.opentelemetry.instrumentation" % "opentelemetry-instrumentation-annotations" % "1.31.0", + * "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.31.0" % Runtime, + * "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.31.0" % Runtime + * ) + * run / fork := true, + * javaOptions += "-Dotel.java.global-autoconfigure.enabled=true", + * javaOptions += "-Dotel.service.name=pekko-otel4s", + * javaAgents += "io.opentelemetry.javaagent" % "opentelemetry-javaagent" % "1.31.0" % Runtime + * ) + * }}} + */ +object PekkoHttpExample extends IOApp.Simple { + + def run: IO[Unit] = + IOLocal(Context.root).flatMap { implicit ioLocal: IOLocal[Context] => + implicit val local: Local[IO, Context] = localForIoLocal + val otelJava: OtelJava[IO] = OtelJava.local(GlobalOpenTelemetry.get()) + + otelJava.tracerProvider.get("com.example").flatMap { + implicit tracer: Tracer[IO] => + createSystem.use { implicit actorSystem: ActorSystem => + def bind: Future[Http.ServerBinding] = + Http().newServerAt("127.0.0.1", 9000).bindFlow(routes) + + Resource + .make(IO.fromFuture(IO.delay(bind))) { b => + IO.fromFuture(IO.delay(b.unbind())).void + } + .use(_ => IO.never) + } + } + } + + private def createSystem: Resource[IO, ActorSystem] = + Resource.make(IO.delay(ActorSystem()))(system => + IO.fromFuture(IO.delay(system.terminate())).void + ) + + private def routes(implicit + T: Tracer[IO], + L: Local[IO, Context], + S: ActorSystem + ): Route = + concat( + path("gen-random-name") { + get { + complete { + OK -> generateRandomName(length = 10) + } + } + }, + path("get-ip") { + get { + complete { + OK -> getIP() + } + } + } + ) + + @WithSpan("generate-random-name") + private def generateRandomName( + length: Int + )(implicit T: Tracer[IO], L: Local[IO, Context]): String = + withJContext(JContext.current())(generate[IO](length)) + .unsafeRunSync()(runtime) + + @WithSpan("get-ip") + private def getIP()(implicit + T: Tracer[IO], + L: Local[IO, Context], + A: ActorSystem + ): String = + withJContext(JContext.current())(resolveIP[IO]).unsafeRunSync()(runtime) + + private def generate[F[_]: Async: Tracer](length: Int): F[String] = + Tracer[F].span("generate", Attribute("length", length.toLong)).surround { + for { + random <- Random.scalaUtilRandom[F] + delay <- random.betweenInt(100, 2000) + chars <- random.nextAlphaNumeric + .replicateA(length) + .delayBy(delay.millis) + } yield chars.mkString + } + + private def resolveIP[F[_]: Async: Tracer](implicit + L: Local[F, Context], + A: ActorSystem + ): F[String] = + Tracer[F].span("resolve-ip").surround { + Async[F].executionContext.flatMap { implicit ec => + Async[F].fromFuture { + useJContext[F, Future[String]] { _ => + for { + response <- Http().singleRequest( + HttpRequest(uri = "https://checkip.amazonaws.com") + ) + body <- response.entity.dataBytes + .runFold(ByteString.empty)(_ ++ _) + } yield new String(body.toArray) + } + } + } + } + + private def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit + L: Local[F, Context] + ): F[A] = + Local[F, Context].scope(fa)(Context.wrap(ctx)) + + private def useJContext[F[_]: Sync, A](use: JContext => A)(implicit + L: Local[F, Context] + ): F[A] = + Local[F, Context].ask.flatMap { ctx => + Sync[F].delay { + val jContext: JContext = ctx.underlying + val scope = jContext.makeCurrent() + try { + use(jContext) + } finally { + scope.close() + } + } + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 184e39d82..32fbb05e1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,3 +6,4 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") +addSbtPlugin("com.github.sbt" % "sbt-javaagent" % "0.1.8")