Skip to content

Commit dd88b4e

Browse files
committed
add Pekko HTTP example
1 parent 11a6717 commit dd88b4e

File tree

4 files changed

+260
-28
lines changed

4 files changed

+260
-28
lines changed

build.sbt

+11-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ val MUnitCatsEffectVersion = "2.0.0-M3"
3737
val MUnitDisciplineVersion = "2.0.0-M3"
3838
val OpenTelemetryVersion = "1.31.0"
3939
val OpenTelemetrySemConvVersion = "1.21.0-alpha"
40+
val PekkoStreamVersion = "1.0.1"
41+
val PekkoHttpVersion = "1.0.0"
4042
val PlatformVersion = "1.0.2"
4143
val ScodecVersion = "1.1.38"
4244
val VaultVersion = "3.5.0"
@@ -346,17 +348,21 @@ lazy val benchmarks = project
346348
.settings(scalafixSettings)
347349

348350
lazy val examples = project
349-
.enablePlugins(NoPublishPlugin)
351+
.enablePlugins(NoPublishPlugin, JavaAgent)
350352
.in(file("examples"))
351353
.dependsOn(core.jvm, java)
352354
.settings(
353355
name := "otel4s-examples",
354356
libraryDependencies ++= Seq(
357+
"org.apache.pekko" %% "pekko-stream" % PekkoStreamVersion,
358+
"org.apache.pekko" %% "pekko-http" % PekkoHttpVersion,
355359
"io.opentelemetry" % "opentelemetry-exporter-otlp" % OpenTelemetryVersion,
356360
"io.opentelemetry" % "opentelemetry-sdk" % OpenTelemetryVersion,
357361
"io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % OpenTelemetryVersion,
358-
"io.opentelemetry" % "opentelemetry-extension-trace-propagators" % OpenTelemetryVersion % Runtime
362+
"io.opentelemetry" % "opentelemetry-extension-trace-propagators" % OpenTelemetryVersion % Runtime,
363+
"io.opentelemetry.instrumentation" % "opentelemetry-instrumentation-annotations" % OpenTelemetryVersion
359364
),
365+
javaAgents += "io.opentelemetry.javaagent" % "opentelemetry-javaagent" % OpenTelemetryVersion % Runtime,
360366
run / fork := true,
361367
javaOptions += "-Dotel.java.global-autoconfigure.enabled=true",
362368
envVars ++= Map(
@@ -372,7 +378,9 @@ lazy val docs = project
372378
.dependsOn(java)
373379
.settings(
374380
libraryDependencies ++= Seq(
375-
"io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % OpenTelemetryVersion
381+
"org.apache.pekko" %% "pekko-http" % PekkoHttpVersion,
382+
"io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % OpenTelemetryVersion,
383+
"io.opentelemetry.instrumentation" % "opentelemetry-instrumentation-annotations" % OpenTelemetryVersion
376384
),
377385
mdocVariables ++= Map(
378386
"OPEN_TELEMETRY_VERSION" -> OpenTelemetryVersion

docs/instrumentation/tracing-java-interop.md

+59-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Tracing - interop with Java-instrumented libraries
22

3-
### Glossary
3+
## Glossary
44

55
| Name | Description |
66
|------------------------------------------|--------------------------------------------------------------|
@@ -74,7 +74,7 @@ val run: IO[Unit] =
7474
## How to use OpenTelemetry Java context with otel4s
7575

7676
There are several scenarios when you want to run an effect with an explicit OpenTelemetry Java context.
77-
For example, when you need to materialize an effect inside [Play Framework][play-framework] request handler.
77+
For example, when you need to materialize an effect inside [Pekko HTTP][pekko-http] request handler.
7878

7979
To make it work, we can define a utility method:
8080
```scala mdoc:silent:reset
@@ -91,27 +91,55 @@ def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context]
9191

9292
_____
9393

94-
Let's say you use [Play Framework][play-framework] and want to materialize an `IO` using the current tracing context:
95-
```scala
96-
class Application @Inject() (implicit
97-
cc: ControllerComponents,
98-
tracer: Tracer[IO],
99-
local: Local[IO, Cointext]
100-
) extends AbstractController(cc) {
101-
102-
def findUser(userId: Long) = Action {
103-
val current = JContext.current() // get current JContext
104-
withJContext(current)(search(userId)).unsafeRunSync() // materialize IO
105-
}
106-
107-
def search(userId: Long): IO[String] =
108-
tracer.span("find-user").surround {
109-
IO.pure("the-result")
94+
Let's say you use [Pekko HTTP][pekko-http] and want to materialize an `IO` using the current tracing context:
95+
```scala mdoc:silent:reset
96+
import cats.effect.{Async, IO}
97+
import cats.effect.std.Random
98+
import cats.effect.syntax.temporal._
99+
import cats.effect.unsafe.implicits.global
100+
import cats.mtl.Local
101+
import cats.syntax.all._
102+
import org.apache.pekko.http.scaladsl.model.StatusCodes.OK
103+
import org.apache.pekko.http.scaladsl.server.Directives._
104+
import org.apache.pekko.http.scaladsl.server.Route
105+
import org.typelevel.otel4s.Attribute
106+
import org.typelevel.otel4s.trace.Tracer
107+
import org.typelevel.otel4s.java.context.Context
108+
import io.opentelemetry.instrumentation.annotations.WithSpan
109+
import io.opentelemetry.context.{Context => JContext}
110+
import scala.concurrent.duration._
111+
112+
def route(implicit T: Tracer[IO], L: Local[IO, Context]): Route =
113+
path("gen-random-name") {
114+
get {
115+
complete {
116+
OK -> generateRandomName(length = 10)
117+
}
110118
}
119+
}
111120

112-
def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context]): F[A] =
113-
Local[F, Context].scope(fa)(Context.wrap(ctx))
114-
}
121+
@WithSpan("generate-random-name")
122+
def generateRandomName(length: Int)(implicit T: Tracer[IO], L: Local[IO, Context]): String =
123+
withJContext(JContext.current())(generate[IO](length)).unsafeRunSync()
124+
125+
def generate[F[_]: Async: Tracer](length: Int): F[String] =
126+
Tracer[F].span("generate", Attribute("length", length.toLong)).surround {
127+
for {
128+
random <- Random.scalaUtilRandom[F]
129+
delay <- random.betweenInt(100, 2000)
130+
chars <- random.nextAlphaNumeric.replicateA(length).delayBy(delay.millis)
131+
} yield chars.mkString
132+
}
133+
134+
def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context]): F[A] =
135+
Local[F, Context].scope(fa)(Context.wrap(ctx))
136+
```
137+
138+
When you invoke the `gen-random-name` endpoint, the spans will be structured in the following way:
139+
```
140+
> GET { http.method = GET, http.target = /gen-random-name, ... }
141+
> generate-random-name
142+
> generate { length = 10 }
115143
```
116144

117145
## How to use otel4s context with OpenTelemetry Java
@@ -127,7 +155,7 @@ import org.typelevel.otel4s.java.context.Context
127155
import io.opentelemetry.context.{Context => JContext}
128156
import scala.util.Using
129157

130-
def withJContext[F[_]: Sync, A](use: JContext => A)(implicit L: Local[F, Context]): F[A] =
158+
def useJContext[F[_]: Sync, A](use: JContext => A)(implicit L: Local[F, Context]): F[A] =
131159
Local[F, Context].ask.flatMap { ctx => // <1>
132160
Sync[F].defer {
133161
Sync[F].fromTry {
@@ -148,7 +176,7 @@ Depending on your use case, you may prefer `Sync[F].interruptible` or `Sync[F].b
148176
Now we can run a slightly modified original 'problematic' example:
149177
```scala
150178
tracer.span("test").use { span => // start 'test' span using otel4s
151-
IO.println(s"Otel4s ctx: ${span.context}") >> withJContext[IO, Unit] { _ =>
179+
IO.println(s"Otel4s ctx: ${span.context}") >> useJContext[IO, Unit] { _ =>
152180
val jSpanContext = JSpan.current().getSpanContext // get a span from the ThreadLocal variable
153181
println(s"Java ctx: $jSpanContext")
154182
}
@@ -162,12 +190,18 @@ Otel4s ctx: {traceId=06f5d9112efbe711947ebbded1287a30, spanId=26ed80c398cc039f,
162190
```
163191

164192
As we can see, the tracing information is in sync now,
165-
and you can use Java-instrumented libraries within the `withJContext` block.
193+
and you can use Java-instrumented libraries within the `useJContext` block.
194+
195+
## The example
196+
197+
[PekkoHttpExample][pekko-http-example] is a complete example that shows how to use otel4s
198+
with OpenTelemetry Java instrumented libraries.
166199

167200
[opentelemetry-java]: https://github.com/open-telemetry/opentelemetry-java
168201
[opentelemetry-java-autoconfigure]: https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md
169202
[opentelemetry-java-context]: https://github.com/open-telemetry/opentelemetry-java/blob/main/context/src/main/java/io/opentelemetry/context/Context.java
170203
[opentelemetry-java-span]: https://github.com/open-telemetry/opentelemetry-java/blob/main/api/all/src/main/java/io/opentelemetry/api/trace/Span.java
171204
[otel4s-context]: https://github.com/typelevel/otel4s/blob/main/java/common/src/main/scala/org/typelevel/otel4s/java/context/Context.scala
172205
[cats-mtl-local]: https://typelevel.org/cats-mtl/mtl-classes/local.html
173-
[play-framework]: https://github.com/playframework/playframework
206+
[pekko-http]: https://pekko.apache.org/docs/pekko-http/current
207+
[pekko-http-example]: https://github.com/typelevel/otel4s/blob/main/examples/src/main/scala/PekkoHttpExample.scala
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright 2022 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import cats.effect.Async
18+
import cats.effect.IO
19+
import cats.effect.IOApp
20+
import cats.effect.IOLocal
21+
import cats.effect.Resource
22+
import cats.effect.Sync
23+
import cats.effect.std.Random
24+
import cats.effect.syntax.temporal._
25+
import cats.mtl.Local
26+
import cats.syntax.applicative._
27+
import cats.syntax.flatMap._
28+
import cats.syntax.functor._
29+
import io.opentelemetry.api.GlobalOpenTelemetry
30+
import io.opentelemetry.context.{Context => JContext}
31+
import io.opentelemetry.instrumentation.annotations.WithSpan
32+
import org.apache.pekko.actor.ActorSystem
33+
import org.apache.pekko.http.scaladsl.Http
34+
import org.apache.pekko.http.scaladsl.model.HttpRequest
35+
import org.apache.pekko.http.scaladsl.model.StatusCodes.OK
36+
import org.apache.pekko.http.scaladsl.server.Directives._
37+
import org.apache.pekko.http.scaladsl.server.Route
38+
import org.apache.pekko.util.ByteString
39+
import org.typelevel.otel4s.Attribute
40+
import org.typelevel.otel4s.java.OtelJava
41+
import org.typelevel.otel4s.java.context.Context
42+
import org.typelevel.otel4s.java.instances._
43+
import org.typelevel.otel4s.trace.Tracer
44+
45+
import scala.concurrent.Future
46+
import scala.concurrent.duration._
47+
import scala.util.Using
48+
49+
/** This example relies on the OpenTelemetry Java agent. To make it work, add
50+
* the following settings to your build:
51+
*
52+
* add `sbt-javaagent` dependency to the `plugins.sbt`:
53+
*
54+
* {{{
55+
* addSbtPlugin("com.github.sbt" % "sbt-javaagent" % "0.1.8")
56+
* }}}
57+
*
58+
* update definition of a project in the `build.sbt`:
59+
*
60+
* {{{
61+
* .enablePlugins(JavaAgent)
62+
* .settings(
63+
* libraryDependencies ++= Seq(
64+
* "org.typelevel" %% "otel4s-java" % "0.3.0-RC2",
65+
* "org.apache.pekko" %% "pekko-stream" % "1.0.1",
66+
* "org.apache.pekko" %% "pekko-http" % "1.0.0",
67+
* "io.opentelemetry.instrumentation" % "opentelemetry-instrumentation-annotations" % "1.31.0",
68+
* "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.31.0" % Runtime,
69+
* "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.31.0" % Runtime
70+
* )
71+
* run / fork := true,
72+
* javaOptions += "-Dotel.java.global-autoconfigure.enabled=true",
73+
* javaOptions += "-Dotel.service.name=pekko-otel4s",
74+
* javaAgents += "io.opentelemetry.javaagent" % "opentelemetry-javaagent" % "1.31.0" % Runtime
75+
* )
76+
* }}}
77+
*/
78+
object PekkoHttpExample extends IOApp.Simple {
79+
80+
def run: IO[Unit] =
81+
IOLocal(Context.root).flatMap { implicit ioLocal: IOLocal[Context] =>
82+
implicit val local: Local[IO, Context] = localForIoLocal
83+
val otelJava: OtelJava[IO] = OtelJava.local(GlobalOpenTelemetry.get())
84+
85+
otelJava.tracerProvider.get("com.example").flatMap {
86+
implicit tracer: Tracer[IO] =>
87+
createSystem.use { implicit actorSystem: ActorSystem =>
88+
def bind: Future[Http.ServerBinding] =
89+
Http().newServerAt("127.0.0.1", 9000).bindFlow(routes)
90+
91+
Resource
92+
.make(IO.fromFuture(IO.delay(bind))) { b =>
93+
IO.fromFuture(IO.delay(b.unbind())).void
94+
}
95+
.use(_ => IO.never)
96+
}
97+
}
98+
}
99+
100+
private def createSystem: Resource[IO, ActorSystem] =
101+
Resource.make(IO.delay(ActorSystem()))(system =>
102+
IO.fromFuture(IO.delay(system.terminate())).void
103+
)
104+
105+
private def routes(implicit
106+
T: Tracer[IO],
107+
L: Local[IO, Context],
108+
S: ActorSystem
109+
): Route =
110+
concat(
111+
path("gen-random-name") {
112+
get {
113+
complete {
114+
OK -> generateRandomName(length = 10)
115+
}
116+
}
117+
},
118+
path("get-ip") {
119+
get {
120+
complete {
121+
OK -> getIP()
122+
}
123+
}
124+
}
125+
)
126+
127+
@WithSpan("generate-random-name")
128+
private def generateRandomName(
129+
length: Int
130+
)(implicit T: Tracer[IO], L: Local[IO, Context]): String =
131+
withJContext(JContext.current())(generate[IO](length))
132+
.unsafeRunSync()(runtime)
133+
134+
@WithSpan("get-ip")
135+
private def getIP()(implicit
136+
T: Tracer[IO],
137+
L: Local[IO, Context],
138+
A: ActorSystem
139+
): String =
140+
withJContext(JContext.current())(resolveIP[IO]).unsafeRunSync()(runtime)
141+
142+
private def generate[F[_]: Async: Tracer](length: Int): F[String] =
143+
Tracer[F].span("generate", Attribute("length", length.toLong)).surround {
144+
for {
145+
random <- Random.scalaUtilRandom[F]
146+
delay <- random.betweenInt(100, 2000)
147+
chars <- random.nextAlphaNumeric
148+
.replicateA(length)
149+
.delayBy(delay.millis)
150+
} yield chars.mkString
151+
}
152+
153+
private def resolveIP[F[_]: Async: Tracer](implicit
154+
L: Local[F, Context],
155+
A: ActorSystem
156+
): F[String] =
157+
Tracer[F].span("resolve-ip").surround {
158+
Async[F].executionContext.flatMap { implicit ec =>
159+
Async[F].fromFuture {
160+
useJContext[F, Future[String]] { _ =>
161+
for {
162+
response <- Http().singleRequest(
163+
HttpRequest(uri = "https://checkip.amazonaws.com")
164+
)
165+
body <- response.entity.dataBytes
166+
.runFold(ByteString.empty)(_ ++ _)
167+
} yield new String(body.toArray)
168+
}
169+
}
170+
}
171+
}
172+
173+
private def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit
174+
L: Local[F, Context]
175+
): F[A] =
176+
Local[F, Context].scope(fa)(Context.wrap(ctx))
177+
178+
private def useJContext[F[_]: Sync, A](use: JContext => A)(implicit
179+
L: Local[F, Context]
180+
): F[A] =
181+
Local[F, Context].ask.flatMap { ctx =>
182+
Sync[F].defer {
183+
Sync[F].fromTry {
184+
val jContext: JContext = ctx.underlying
185+
Using(jContext.makeCurrent())(_ => use(jContext))
186+
}
187+
}
188+
}
189+
}

project/plugins.sbt

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16")
66
addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2")
77
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0")
88
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6")
9+
addSbtPlugin("com.github.sbt" % "sbt-javaagent" % "0.1.8")

0 commit comments

Comments
 (0)