Skip to content

Commit

Permalink
add Pekko HTTP example
Browse files Browse the repository at this point in the history
  • Loading branch information
iRevive committed Nov 4, 2023
1 parent 11a6717 commit c40c69f
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 33 deletions.
14 changes: 11 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
93 changes: 63 additions & 30 deletions docs/instrumentation/tracing-java-interop.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tracing - interop with Java-instrumented libraries

### Glossary
## Glossary

| Name | Description |
|------------------------------------------|--------------------------------------------------------------|
Expand Down Expand Up @@ -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
Expand All @@ -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

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.
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
Expand All @@ -91,27 +90,55 @@ 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))
```

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 OpenTelemetry Java
Expand All @@ -127,7 +154,7 @@ 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 {
Expand All @@ -148,7 +175,7 @@ Depending on your use case, you may prefer `Sync[F].interruptible` or `Sync[F].b
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")
}
Expand All @@ -162,12 +189,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 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
[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
189 changes: 189 additions & 0 deletions examples/src/main/scala/PekkoHttpExample.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* 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._
import scala.util.Using

/** 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].defer {
Sync[F].fromTry {
val jContext: JContext = ctx.underlying
Using(jContext.makeCurrent())(_ => use(jContext))
}
}
}
}
1 change: 1 addition & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")

0 comments on commit c40c69f

Please sign in to comment.