From 4ecaeaa9cf3896cbc6d844dd9ba21fa0c793b7ff Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Mon, 16 Dec 2024 13:18:32 +0200 Subject: [PATCH] oteljava: refactor Metrics, Traces, and OtelJava API --- docs/oteljava/tracing-java-interop.md | 11 +- docs/tracing-context-propagation.md | 12 +- examples/src/main/scala/KleisliExample.scala | 6 +- .../src/main/scala/PekkoHttpExample.scala | 9 +- .../typelevel/otel4s/oteljava/OtelJava.scala | 121 +++++++++--------- .../otel4s/oteljava/OtelJavaSuite.scala | 2 +- .../otel4s/oteljava/metrics/Metrics.scala | 51 +++++++- .../otel4s/oteljava/trace/Traces.scala | 76 ++++++++--- 8 files changed, 175 insertions(+), 113 deletions(-) diff --git a/docs/oteljava/tracing-java-interop.md b/docs/oteljava/tracing-java-interop.md index 2dda88478..19377c64c 100644 --- a/docs/oteljava/tracing-java-interop.md +++ b/docs/oteljava/tracing-java-interop.md @@ -50,14 +50,8 @@ 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.instances.local._ // brings Local derived from IOLocal import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.oteljava.OtelJava -import io.opentelemetry.api.GlobalOpenTelemetry - -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 @@ -65,8 +59,9 @@ 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].flatMap(otel4s => program(otel4s)) + OtelJava.global[IO].flatMap { otel4s => + implicit val local: Local[IO, Context] = otel4s.localContext + program(otel4s) } ``` diff --git a/docs/tracing-context-propagation.md b/docs/tracing-context-propagation.md index 04d66c395..e401a1cf4 100644 --- a/docs/tracing-context-propagation.md +++ b/docs/tracing-context-propagation.md @@ -32,14 +32,14 @@ You can find both examples below and choose which one suits your requirements. ```scala mdoc:silent:reset import cats.effect._ import cats.mtl.Local -import cats.syntax.functor._ +import cats.syntax.flatMap._ import org.typelevel.otel4s.instances.local._ // brings Local derived from IOLocal import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.oteljava.OtelJava import io.opentelemetry.api.GlobalOpenTelemetry def createOtel4s[F[_]: Async](implicit L: Local[F, Context]): F[OtelJava[F]] = - Async[F].delay(GlobalOpenTelemetry.get).map(OtelJava.local[F]) + Async[F].delay(GlobalOpenTelemetry.get).flatMap(OtelJava.fromJOpenTelemetry[F]) def program[F[_]: Async](otel4s: OtelJava[F]): F[Unit] = { val _ = otel4s @@ -52,7 +52,7 @@ val run: IO[Unit] = } ``` -If you don't need direct access to the `IOLocal` instance, there is also a shortcut `OtelJava.forAsync`: +If you don't need direct access to the `IOLocal` instance, there is also a shortcut `OtelJava.fromJOpenTelemetry`: ```scala mdoc:silent:reset import cats.effect._ @@ -61,7 +61,7 @@ import org.typelevel.otel4s.oteljava.OtelJava import io.opentelemetry.api.GlobalOpenTelemetry def createOtel4s[F[_]: Async: LiftIO]: F[OtelJava[F]] = - Async[F].delay(GlobalOpenTelemetry.get).flatMap(OtelJava.forAsync[F]) + Async[F].delay(GlobalOpenTelemetry.get).flatMap(OtelJava.fromJOpenTelemetry[F]) def program[F[_]: Async](otel4s: OtelJava[F]): F[Unit] = { val _ = otel4s @@ -90,7 +90,7 @@ val run: IO[Unit] = ```scala mdoc:silent:reset import cats.effect._ -import cats.syntax.functor._ +import cats.syntax.flatMap._ import cats.data.Kleisli import cats.mtl.Local import org.typelevel.otel4s.oteljava.context.Context @@ -98,7 +98,7 @@ import org.typelevel.otel4s.oteljava.OtelJava import io.opentelemetry.api.GlobalOpenTelemetry def createOtel4s[F[_]: Async](implicit L: Local[F, Context]): F[OtelJava[F]] = - Async[F].delay(GlobalOpenTelemetry.get).map(OtelJava.local[F]) + Async[F].delay(GlobalOpenTelemetry.get).flatMap(OtelJava.fromJOpenTelemetry[F]) def program[F[_]: Async](otel4s: OtelJava[F]): F[Unit] = { val _ = otel4s diff --git a/examples/src/main/scala/KleisliExample.scala b/examples/src/main/scala/KleisliExample.scala index 58e8c0c08..048284d43 100644 --- a/examples/src/main/scala/KleisliExample.scala +++ b/examples/src/main/scala/KleisliExample.scala @@ -19,7 +19,6 @@ import cats.effect.Async import cats.effect.IO import cats.effect.IOApp import cats.effect.Resource -import io.opentelemetry.api.GlobalOpenTelemetry import org.typelevel.otel4s.oteljava.OtelJava import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.oteljava.context.LocalContext @@ -30,10 +29,7 @@ object KleisliExample extends IOApp.Simple { Tracer[F].span("work").surround(Async[F].delay(println("I'm working"))) private def tracerResource[F[_]: Async: LocalContext]: Resource[F, Tracer[F]] = - Resource - .eval(Async[F].delay(GlobalOpenTelemetry.get)) - .map(OtelJava.local[F]) - .evalMap(_.tracerProvider.get("kleisli-example")) + Resource.eval(OtelJava.global[F]).evalMap(_.tracerProvider.get("kleisli-example")) def run: IO[Unit] = tracerResource[Kleisli[IO, Context, *]] diff --git a/examples/src/main/scala/PekkoHttpExample.scala b/examples/src/main/scala/PekkoHttpExample.scala index d516055ea..14b2919d7 100644 --- a/examples/src/main/scala/PekkoHttpExample.scala +++ b/examples/src/main/scala/PekkoHttpExample.scala @@ -17,7 +17,6 @@ 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 @@ -26,7 +25,6 @@ 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 @@ -37,9 +35,9 @@ 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.instances.local._ import org.typelevel.otel4s.oteljava.OtelJava import org.typelevel.otel4s.oteljava.context.Context +import org.typelevel.otel4s.oteljava.context.LocalContext import org.typelevel.otel4s.trace.Tracer import scala.concurrent.Future @@ -76,9 +74,8 @@ import scala.concurrent.duration._ 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.global[IO].flatMap { otelJava => + implicit val local: LocalContext[IO] = otelJava.localContext otelJava.tracerProvider.get("com.example").flatMap { implicit tracer: Tracer[IO] => createSystem.use { implicit actorSystem: ActorSystem => diff --git a/oteljava/all/src/main/scala/org/typelevel/otel4s/oteljava/OtelJava.scala b/oteljava/all/src/main/scala/org/typelevel/otel4s/oteljava/OtelJava.scala index 371a3f93e..bd996c564 100644 --- a/oteljava/all/src/main/scala/org/typelevel/otel4s/oteljava/OtelJava.scala +++ b/oteljava/all/src/main/scala/org/typelevel/otel4s/oteljava/OtelJava.scala @@ -34,7 +34,6 @@ import org.typelevel.otel4s.metrics.MeterProvider import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.oteljava.context.LocalContext import org.typelevel.otel4s.oteljava.context.LocalContextProvider -import org.typelevel.otel4s.oteljava.context.propagation.PropagatorConverters._ import org.typelevel.otel4s.oteljava.metrics.Metrics import org.typelevel.otel4s.oteljava.trace.Traces import org.typelevel.otel4s.trace.TracerProvider @@ -53,64 +52,6 @@ final class OtelJava[F[_]] private ( object OtelJava { - /** Creates an [[org.typelevel.otel4s.Otel4s]] from a Java OpenTelemetry instance. - * - * @param jOtel - * A Java OpenTelemetry instance. It is the caller's responsibility to shut this down. Failure to do so may result - * in lost metrics and traces. - * - * @return - * An effect of an [[org.typelevel.otel4s.Otel4s]] resource. - */ - def forAsync[F[_]: Async: LocalContextProvider]( - jOtel: JOpenTelemetry - ): F[OtelJava[F]] = - LocalProvider[F, Context].local.map { implicit l => - local[F](jOtel) - } - - def local[F[_]: Async: LocalContext]( - jOtel: JOpenTelemetry - ): OtelJava[F] = { - val contextPropagators = jOtel.getPropagators.asScala - - val metrics = Metrics.forAsync(jOtel) - val traces = Traces.local(jOtel, contextPropagators) - new OtelJava[F]( - jOtel, - contextPropagators, - metrics.meterProvider, - traces.tracerProvider, - ) - } - - /** Creates a no-op implementation of the [[OtelJava]]. - */ - def noop[F[_]: Applicative: LocalContextProvider]: F[OtelJava[F]] = - for { - local <- LocalProvider[F, Context].local - } yield new OtelJava( - JOpenTelemetry.noop(), - ContextPropagators.noop, - MeterProvider.noop, - TracerProvider.noop - )(local) - - /** Lifts the acquisition of a Java OpenTelemetrySdk instance to a Resource. - * - * @param acquire - * OpenTelemetrySdk resource - * - * @return - * An [[org.typelevel.otel4s.Otel4s]] resource. - */ - def resource[F[_]: Async: LocalContextProvider]( - acquire: F[JOpenTelemetrySdk] - ): Resource[F, OtelJava[F]] = - Resource - .make(acquire)(sdk => asyncFromCompletableResultCode(Sync[F].delay(sdk.shutdown()))) - .evalMap(forAsync[F]) - /** Creates a [[cats.effect.Resource `Resource`]] of the automatic configuration of a Java `OpenTelemetrySdk` * instance. * @@ -147,7 +88,67 @@ object OtelJava { * [[autoConfigured]] */ def global[F[_]: Async: LocalContextProvider]: F[OtelJava[F]] = - Sync[F].delay(GlobalOpenTelemetry.get).flatMap(forAsync[F]) + Sync[F].delay(GlobalOpenTelemetry.get).flatMap(fromJOpenTelemetry[F]) + + /** Lifts the acquisition of a Java OpenTelemetrySdk instance to a Resource. The acquired SDK will be shutdown upon + * release. + * + * @param acquire + * OpenTelemetrySdk resource + * + * @return + * An [[org.typelevel.otel4s.Otel4s]] resource. + */ + def resource[F[_]: Async: LocalContextProvider](acquire: F[JOpenTelemetrySdk]): Resource[F, OtelJava[F]] = + Resource + .make(acquire)(sdk => asyncFromCompletableResultCode(Sync[F].delay(sdk.shutdown()))) + .evalMap(fromJOpenTelemetry[F]) + + /** Creates an [[org.typelevel.otel4s.Otel4s]] from a Java OpenTelemetry instance. + * + * @param jOtel + * A Java OpenTelemetry instance. It is the caller's responsibility to shut this down. Failure to do so may result + * in lost metrics and traces. + * + * @return + * An effect of an [[org.typelevel.otel4s.Otel4s]] resource. + */ + def fromJOpenTelemetry[F[_]: Async: LocalContextProvider](jOtel: JOpenTelemetry): F[OtelJava[F]] = + LocalProvider[F, Context].local.map { implicit l => + create[F](jOtel) + } + + /** Creates a no-op implementation of the [[OtelJava]]. + */ + def noop[F[_]: Applicative: LocalContextProvider]: F[OtelJava[F]] = + for { + local <- LocalProvider[F, Context].local + } yield new OtelJava( + JOpenTelemetry.noop(), + ContextPropagators.noop, + MeterProvider.noop, + TracerProvider.noop + )(local) + + /** Creates an [[org.typelevel.otel4s.Otel4s]] from a Java OpenTelemetry instance using the given `Local` instance. + * + * @param jOtel + * A Java OpenTelemetry instance. It is the caller's responsibility to shut this down. Failure to do so may result + * in lost metrics and traces. + * + * @return + * An effect of an [[org.typelevel.otel4s.Otel4s]] resource. + */ + private def create[F[_]: Async: LocalContext](jOtel: JOpenTelemetry): OtelJava[F] = { + val metrics = Metrics.create(jOtel) + val traces = Traces.create(jOtel) + new OtelJava[F]( + jOtel, + traces.propagators, + metrics.meterProvider, + traces.tracerProvider, + ) + } private[this] def asyncFromCompletableResultCode[F[_]]( codeF: F[CompletableResultCode], diff --git a/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/OtelJavaSuite.scala b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/OtelJavaSuite.scala index 667eea93f..a6bddfa7c 100644 --- a/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/OtelJavaSuite.scala +++ b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/OtelJavaSuite.scala @@ -25,7 +25,7 @@ class OtelJavaSuite extends CatsEffectSuite { test("OtelJava toString returns useful info") { val testSdk: JOpenTelemetrySdk = JOpenTelemetrySdk.builder().build() OtelJava - .forAsync[IO](testSdk) + .fromJOpenTelemetry[IO](testSdk) .map(testOtel4s => { val res = testOtel4s.toString() assert(clue(res).contains("OpenTelemetrySdk")) diff --git a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/Metrics.scala b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/Metrics.scala index e63ae8518..44c2ce22e 100644 --- a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/Metrics.scala +++ b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/Metrics.scala @@ -16,21 +16,58 @@ package org.typelevel.otel4s.oteljava.metrics -import cats.effect.kernel.Async +import cats.effect.Async +import cats.mtl.Ask +import cats.syntax.functor._ import io.opentelemetry.api.{OpenTelemetry => JOpenTelemetry} +import io.opentelemetry.api.GlobalOpenTelemetry import org.typelevel.otel4s.metrics.MeterProvider import org.typelevel.otel4s.oteljava.context.AskContext +import org.typelevel.otel4s.oteljava.context.Context -trait Metrics[F[_]] { +/** The configured metrics module. + * + * @tparam F + * the higher-kinded type of a polymorphic effect + */ +sealed trait Metrics[F[_]] { + + /** The [[org.typelevel.otel4s.metrics.MeterProvider MeterProvider]]. + */ def meterProvider: MeterProvider[F] } object Metrics { - def forAsync[F[_]: Async: AskContext](jOtel: JOpenTelemetry): Metrics[F] = - new Metrics[F] { - val meterProvider: MeterProvider[F] = - new MeterProviderImpl[F](jOtel.getMeterProvider) - } + /** Creates a [[org.typelevel.otel4s.oteljava.metrics.Metrics]] from the global Java OpenTelemetry instance. + * + * @note + * the created module is isolated and exemplars won't be collected. Use `OtelJava` if you need to capture + * exemplars. + */ + def global[F[_]: Async]: F[Metrics[F]] = + Async[F].delay(GlobalOpenTelemetry.get).map(fromJOpenTelemetry[F]) + + /** Creates a [[org.typelevel.otel4s.oteljava.metrics.Metrics]] from a Java OpenTelemetry instance. + * + * @note + * the created module is isolated and exemplars won't be collected. Use `OtelJava` if you need to capture + * exemplars. + * + * @param jOtel + * A Java OpenTelemetry instance. It is the caller's responsibility to shut this down. Failure to do so may result + * in lost metrics and traces. + */ + def fromJOpenTelemetry[F[_]: Async](jOtel: JOpenTelemetry): Metrics[F] = { + implicit val askContext: AskContext[F] = Ask.const(Context.root) + create(jOtel) + } + + private[oteljava] def create[F[_]: Async: AskContext](jOtel: JOpenTelemetry): Metrics[F] = + new Impl(new MeterProviderImpl(jOtel.getMeterProvider)) + + private final class Impl[F[_]](val meterProvider: MeterProvider[F]) extends Metrics[F] { + override def toString: String = s"Metrics{meterProvider=$meterProvider}" + } } diff --git a/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/Traces.scala b/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/Traces.scala index a2a7456cd..3675e7084 100644 --- a/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/Traces.scala +++ b/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/Traces.scala @@ -17,43 +17,79 @@ package org.typelevel.otel4s package oteljava.trace -import cats.effect.IOLocal -import cats.effect.LiftIO import cats.effect.Sync +import cats.syntax.flatMap._ +import cats.syntax.functor._ import io.opentelemetry.api.{OpenTelemetry => JOpenTelemetry} +import io.opentelemetry.api.GlobalOpenTelemetry +import org.typelevel.otel4s.context.LocalProvider import org.typelevel.otel4s.context.propagation.ContextPropagators -import org.typelevel.otel4s.instances.local._ import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.oteljava.context.LocalContext +import org.typelevel.otel4s.oteljava.context.LocalContextProvider +import org.typelevel.otel4s.oteljava.context.propagation.PropagatorConverters._ import org.typelevel.otel4s.trace.TracerProvider -trait Traces[F[_]] { +/** The configured tracing module. + * + * @tparam F + * the higher-kinded type of a polymorphic effect + */ +sealed trait Traces[F[_]] { + + /** The [[org.typelevel.otel4s.trace.TracerProvider TracerProvider]]. + */ def tracerProvider: TracerProvider[F] + + /** The propagators used by the [[org.typelevel.otel4s.trace.TracerProvider TracerProvider]]. + */ + def propagators: ContextPropagators[Context] + + /** The [[org.typelevel.otel4s.oteljava.context.LocalContext LocalContext]] used by the + * [[org.typelevel.otel4s.trace.TracerProvider TracerProvider]]. + */ + def localContext: LocalContext[F] + } object Traces { - def local[F[_]: Sync: LocalContext]( - jOtel: JOpenTelemetry, - propagators: ContextPropagators[Context] - ): Traces[F] = { + /** Creates a [[org.typelevel.otel4s.oteljava.trace.Traces]] from the global Java OpenTelemetry instance. + */ + def global[F[_]: Sync: LocalContextProvider]: F[Traces[F]] = + Sync[F].delay(GlobalOpenTelemetry.get).flatMap(fromJOpenTelemetry[F]) + + /** Creates a [[org.typelevel.otel4s.oteljava.trace.Traces]] from a Java OpenTelemetry instance. + * + * @param jOtel + * A Java OpenTelemetry instance. It is the caller's responsibility to shut this down. Failure to do so may result + * in lost metrics and traces. + */ + def fromJOpenTelemetry[F[_]: Sync: LocalContextProvider](jOtel: JOpenTelemetry): F[Traces[F]] = + LocalProvider[F, Context].local.map(implicit l => create[F](jOtel)) + + /** Creates a [[org.typelevel.otel4s.oteljava.trace.Traces]] from a Java OpenTelemetry instance using the given + * `Local` instance. + * + * @param jOtel + * A Java OpenTelemetry instance. It is the caller's responsibility to shut this down. Failure to do so may result + * in lost metrics and traces. + */ + private[oteljava] def create[F[_]: Sync: LocalContext](jOtel: JOpenTelemetry): Traces[F] = { + val propagators = jOtel.getPropagators.asScala val provider = TracerProviderImpl.local( jOtel.getTracerProvider, propagators, TraceScopeImpl.fromLocal[F] ) - new Traces[F] { - def tracerProvider: TracerProvider[F] = provider - } + new Impl(provider, propagators, implicitly) } - def ioLocal[F[_]: LiftIO: Sync]( - jOtel: JOpenTelemetry, - propagators: ContextPropagators[Context] - ): F[Traces[F]] = - IOLocal(Context.root) - .map { implicit ioLocal: IOLocal[Context] => - local(jOtel, propagators) - } - .to[F] + private final class Impl[F[_]]( + val tracerProvider: TracerProvider[F], + val propagators: ContextPropagators[Context], + val localContext: LocalContext[F] + ) extends Traces[F] { + override def toString: String = s"Traces{tracerProvider=$tracerProvider, propagators=$propagators}" + } }