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 10, 2023
1 parent 11a6717 commit eb16ba7
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 59 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
4 changes: 2 additions & 2 deletions docs/customization/histogram-custom-buckets/README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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}
Expand Down
143 changes: 89 additions & 54 deletions docs/instrumentation/tracing-java-interop.md
Original file line number Diff line number Diff line change
@@ -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) |
| [LocalF, 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:
Expand All @@ -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.

Expand All @@ -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
## 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
Expand All @@ -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
Expand All @@ -125,30 +152,32 @@ 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()
}
}
}
```

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")
}
Expand All @@ -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
Loading

0 comments on commit eb16ba7

Please sign in to comment.