-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
An exploration of cats.mtl.Local tracing #102
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ ThisBuild / scalaVersion := Scala213 // the default Scala | |
|
||
val CatsVersion = "2.9.0" | ||
val CatsEffectVersion = "3.4.5" | ||
val CatsMtlVersion = "1.3.0" | ||
val FS2Version = "3.5.0" | ||
val MUnitVersion = "1.0.0-M7" | ||
val MUnitCatsEffectVersion = "2.0.0-M3" | ||
|
@@ -96,6 +97,7 @@ lazy val `core-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform) | |
name := "otel4s-core-trace", | ||
libraryDependencies ++= Seq( | ||
"org.typelevel" %%% "cats-effect-kernel" % CatsEffectVersion, | ||
"org.typelevel" %%% "cats-mtl" % CatsMtlVersion, | ||
"org.scodec" %%% "scodec-bits" % ScodecVersion | ||
) | ||
) | ||
|
@@ -175,7 +177,7 @@ lazy val `java-trace` = project | |
.settings( | ||
name := "otel4s-java-trace", | ||
libraryDependencies ++= Seq( | ||
"org.typelevel" %%% "cats-effect" % CatsEffectVersion, | ||
"org.typelevel" %%% "cats-mtl" % CatsMtlVersion, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bonus: the hard IO dependency is gone. |
||
"io.opentelemetry" % "opentelemetry-sdk-testing" % OpenTelemetryVersion % Test, | ||
"org.typelevel" %%% "cats-effect-testkit" % CatsEffectVersion % Test, | ||
"co.fs2" %% "fs2-core" % FS2Version % Test | ||
|
@@ -196,6 +198,7 @@ lazy val examples = project | |
.settings( | ||
name := "otel4s-examples", | ||
libraryDependencies ++= Seq( | ||
"org.typelevel" %%% "cats-effect" % CatsEffectVersion, | ||
"io.opentelemetry" % "opentelemetry-exporter-otlp" % OpenTelemetryVersion, | ||
"io.opentelemetry" % "opentelemetry-sdk" % OpenTelemetryVersion, | ||
"io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % s"${OpenTelemetryVersion}-alpha" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,11 +14,14 @@ | |
* limitations under the License. | ||
*/ | ||
|
||
import cats.Applicative | ||
import cats.effect.IO | ||
import cats.effect.IOApp | ||
import cats.effect.IOLocal | ||
import cats.effect.MonadCancelThrow | ||
import cats.effect.Resource | ||
import cats.effect.std.Console | ||
import cats.mtl.Local | ||
import cats.syntax.all._ | ||
import io.opentelemetry.api.GlobalOpenTelemetry | ||
import org.typelevel.otel4s.java.OtelJava | ||
|
@@ -44,11 +47,35 @@ object Work { | |
} | ||
|
||
object TracingExample extends IOApp.Simple { | ||
def tracerResource: Resource[IO, Tracer[IO]] = | ||
Resource | ||
.eval(IO(GlobalOpenTelemetry.get)) | ||
.evalMap(OtelJava.forSync[IO]) | ||
.evalMap(_.tracerProvider.tracer("Example").get) | ||
|
||
// This would be a library function | ||
def local[E](a: E): IO[Local[IO, E]] = | ||
IOLocal(a).map { ioLocal => | ||
new Local[IO, E] { | ||
override def local[A](fa: IO[A])(f: E => E): IO[A] = | ||
ioLocal.get.flatMap(prev => | ||
ioLocal | ||
.set(f(prev)) | ||
.bracket(Function.const(fa))(Function.const(ioLocal.set(prev))) | ||
) | ||
override val applicative: Applicative[IO] = | ||
Applicative[IO] | ||
override def ask[E2 >: E]: IO[E2] = | ||
ioLocal.get | ||
} | ||
} | ||
Comment on lines
+51
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is my interpretation of typelevel/cats-effect#3385, but I haven't tested it. |
||
|
||
def tracerResource: Resource[IO, Tracer[IO]] = { | ||
import io.opentelemetry.context.{Context => JContext} | ||
import org.typelevel.otel4s.java.trace.TraceScope.Scope | ||
Comment on lines
+69
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neither of these imports are particularly desirable in userspace. |
||
Resource.eval(local(Scope.Root(JContext.root()): Scope)).flatMap { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And this is worse. If we're going to expose Scope, we should have a clean way for users to construct this one. |
||
implicit local: Local[IO, Scope] => | ||
Resource | ||
.eval(IO(GlobalOpenTelemetry.get)) | ||
.evalMap(OtelJava.forSync[IO]) | ||
.evalMap(_.tracerProvider.tracer("Example").get) | ||
} | ||
} | ||
|
||
def run: IO[Unit] = { | ||
tracerResource.use { implicit tracer: Tracer[IO] => | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,18 +16,18 @@ | |
|
||
package org.typelevel.otel4s.java | ||
|
||
import cats.effect.Async | ||
import cats.effect.LiftIO | ||
import cats.effect.Resource | ||
import cats.effect.Sync | ||
import cats.syntax.flatMap._ | ||
import cats.effect.kernel.Async | ||
import cats.effect.kernel.Resource | ||
import cats.effect.kernel.Sync | ||
import cats.mtl.Local | ||
import cats.syntax.functor._ | ||
import io.opentelemetry.api.{OpenTelemetry => JOpenTelemetry} | ||
import io.opentelemetry.sdk.{OpenTelemetrySdk => JOpenTelemetrySdk} | ||
import io.opentelemetry.sdk.common.CompletableResultCode | ||
import org.typelevel.otel4s.Otel4s | ||
import org.typelevel.otel4s.java.Conversions.asyncFromCompletableResultCode | ||
import org.typelevel.otel4s.java.metrics.Metrics | ||
import org.typelevel.otel4s.java.trace.TraceScope | ||
import org.typelevel.otel4s.java.trace.Traces | ||
import org.typelevel.otel4s.metrics.MeterProvider | ||
import org.typelevel.otel4s.trace.TracerProvider | ||
|
@@ -46,10 +46,12 @@ object OtelJava { | |
* @return | ||
* An effect of an [[org.typelevel.otel4s.Otel4s]] resource. | ||
*/ | ||
def forSync[F[_]: LiftIO: Sync](jOtel: JOpenTelemetry): F[Otel4s[F]] = { | ||
def forSync[F[_]: Sync: Local[*[_], TraceScope.Scope]]( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is where Scope starts to leak. I think we can fix this with some Greek letters. [edit] Well, probably not: if |
||
jOtel: JOpenTelemetry | ||
): F[Otel4s[F]] = { | ||
for { | ||
metrics <- Sync[F].pure(Metrics.forSync(jOtel)) | ||
traces <- Traces.ioLocal(jOtel) | ||
traces = Traces.local(jOtel) | ||
} yield new Otel4s[F] { | ||
def meterProvider: MeterProvider[F] = metrics.meterProvider | ||
def tracerProvider: TracerProvider[F] = traces.tracerProvider | ||
|
@@ -64,7 +66,7 @@ object OtelJava { | |
* @return | ||
* An [[org.typelevel.otel4s.Otel4s]] resource. | ||
*/ | ||
def resource[F[_]: LiftIO: Async]( | ||
def resource[F[_]: Async: Local[*[_], TraceScope.Scope]]( | ||
acquire: F[JOpenTelemetrySdk] | ||
): Resource[F, Otel4s[F]] = | ||
Resource | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,11 +16,9 @@ | |
|
||
package org.typelevel.otel4s.java.trace | ||
|
||
import cats.effect.IOLocal | ||
import cats.effect.LiftIO | ||
import cats.effect.Resource | ||
import cats.effect.Sync | ||
import cats.syntax.functor._ | ||
import cats.effect.kernel.Resource | ||
import cats.effect.kernel.Sync | ||
import cats.mtl.Local | ||
import io.opentelemetry.api.trace.{Span => JSpan} | ||
import io.opentelemetry.context.{Context => JContext} | ||
import org.typelevel.otel4s.trace.SpanContext | ||
|
@@ -34,7 +32,7 @@ private[java] trait TraceScope[F[_]] { | |
def noopScope: Resource[F, Unit] | ||
} | ||
|
||
private[java] object TraceScope { | ||
object TraceScope { | ||
|
||
sealed trait Scope | ||
object Scope { | ||
|
@@ -47,67 +45,70 @@ private[java] object TraceScope { | |
case object Noop extends Scope | ||
} | ||
|
||
def fromIOLocal[F[_]: LiftIO: Sync]( | ||
private[java] def fromLocal[F[_]: Sync: Local[*[_], Scope]]( | ||
default: JContext | ||
): F[TraceScope[F]] = { | ||
): TraceScope[F] = { | ||
val scopeRoot = Scope.Root(default) | ||
|
||
IOLocal[Scope](scopeRoot).to[F].map { local => | ||
new TraceScope[F] { | ||
val root: F[Scope.Root] = | ||
Sync[F].pure(scopeRoot) | ||
new TraceScope[F] { | ||
val root: F[Scope.Root] = | ||
Sync[F].pure(scopeRoot) | ||
|
||
def current: F[Scope] = | ||
local.get.to[F] | ||
def current: F[Scope] = | ||
Local[F, Scope].ask[Scope] | ||
|
||
def makeScope(span: JSpan): Resource[F, Unit] = | ||
for { | ||
current <- Resource.eval(current) | ||
_ <- createScope(nextScope(current, span)) | ||
} yield () | ||
def makeScope(span: JSpan): Resource[F, Unit] = | ||
for { | ||
current <- Resource.eval(current) | ||
_ <- createScope(nextScope(current, span)) | ||
} yield () | ||
|
||
def rootScope: Resource[F, Unit] = | ||
Resource.eval(current).flatMap { | ||
case Scope.Root(_) => | ||
createScope(scopeRoot) | ||
def rootScope: Resource[F, Unit] = | ||
Resource.eval(current).flatMap { | ||
case Scope.Root(_) => | ||
createScope(scopeRoot) | ||
|
||
case Scope.Span(_, _, _) => | ||
createScope(scopeRoot) | ||
case Scope.Span(_, _, _) => | ||
createScope(scopeRoot) | ||
|
||
case Scope.Noop => | ||
createScope(Scope.Noop) | ||
} | ||
case Scope.Noop => | ||
createScope(Scope.Noop) | ||
} | ||
|
||
def noopScope: Resource[F, Unit] = | ||
createScope(Scope.Noop) | ||
def noopScope: Resource[F, Unit] = | ||
createScope(Scope.Noop) | ||
|
||
private def createScope(scope: Scope): Resource[F, Unit] = | ||
private def createScope(scope: Scope): Resource[F, Unit] = { | ||
val _ = scope | ||
// There's no getAndSet. This is inherently stateful. | ||
/* | ||
Resource | ||
.make(local.getAndSet(scope).to[F])(p => local.set(p).to[F]) | ||
.void | ||
*/ | ||
Comment on lines
+83
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And this here is where the wheels fall off. Do we want tracing of more than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there's a case for both. If the three phases are part of the same trace, then you'd want them to be siblings, but if they should be part of different traces, then not. Motivation for all three in the same trace: the Motivation for separate traces: a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exposing the
|
||
Resource.unit[F] | ||
} | ||
|
||
private def nextScope(scope: Scope, span: JSpan): Scope = | ||
scope match { | ||
case Scope.Root(ctx) => | ||
Scope.Span( | ||
ctx.`with`(span), | ||
span, | ||
WrappedSpanContext(span.getSpanContext) | ||
) | ||
|
||
case Scope.Span(ctx, _, _) => | ||
Scope.Span( | ||
ctx.`with`(span), | ||
span, | ||
WrappedSpanContext(span.getSpanContext) | ||
) | ||
|
||
case Scope.Noop => | ||
Scope.Noop | ||
} | ||
private def nextScope(scope: Scope, span: JSpan): Scope = | ||
scope match { | ||
case Scope.Root(ctx) => | ||
Scope.Span( | ||
ctx.`with`(span), | ||
span, | ||
WrappedSpanContext(span.getSpanContext) | ||
) | ||
|
||
case Scope.Span(ctx, _, _) => | ||
Scope.Span( | ||
ctx.`with`(span), | ||
span, | ||
WrappedSpanContext(span.getSpanContext) | ||
) | ||
|
||
case Scope.Noop => | ||
Scope.Noop | ||
} | ||
|
||
} | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Negative: another core dependency in a library we hope to integrate with the flagships.