Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions modules/core/shared/src/main/scala/Span.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ trait Span[F[_]] {
*/
def traceUri: F[Option[URI]]

/** Run impure code with the span of underlying tracing backend activated on the current thread
* for the duration of the call.
*
* This is useful when calling into Java or other non-cats-effect code that is instrumented by the tracing backend.
*
* This should always be wrapped in `Sync.suspend` or equivalent.
*/
def unsafeRunWithActivatedSpan[T](run: => T): T = run
Copy link
Member

@bpholt bpholt Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a surprising API, in my opinion. I think it's very likely to be a footgun for inexperienced developers, despite the note about it needing to be wrapped in Sync.suspend. Is there a way we can do more to enforce that requirement? Why not accept and return an F[T]?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bpholt there's no way to take an F[T] and ensure that the span is activated every time it runs compute tasks (which is a prerequisite to having the desired semantics). You can't even force it to run on one thread with evalOn because it might contain blocking inside.

I have dealt with this exact problem before, and had a custom implementation that made it work... but the only good thing we can do here is return F[T], imo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't call it from inside suspend if I take or return F[T]. This is meant to be used entirely on impure side hence unsafe in the name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coming back to this, I think it might be possible to take an F. With caveats:

  • if your F contains async boundaries, auto-cedes, etc. you'd be calling that unsafeRunWithActivatedSpan every time you go back to "compute" tasks.
  • if there are any evalOn/blockings inside the effect, they wouldn't have the span activated.

Something like this:

def runWithActivatedSpan[T](ft: F[T]): F[T] = {
  val ec = new ExecutionContext {
    def execute(r: Runnable): Unit = unsafeRunWithActivatedSpan(r.run())
    def reportFailure(e: Throwable): Unit = throw e
  }
  ft.evalOn(ec)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is your use case? The libraries that require such integration do not use cats (or scala).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usecase is when you already have an effect that wraps Java code, and you want to give it the scope. This might happen if you're using a library that wraps Java (so you can't modify it to use the underlying unsafe function) or you want to centralize that kind of context handling in a middleware of sorts

Copy link
Contributor Author

@mwisnicki mwisnicki Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An effect wrapping Java code would have to wrap it in blocking (pretty much any code using tracing is blocking) and you just said this code does not work with blocking inside effect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol fair enough, I take that back then 😆

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though not all Java code is blocking... still I don't think it's very useful without support for blocking blocks


/** Converts this `Span[F]` to a `Span[G]` using a `F ~> G`. */
def mapK[G[_]](f: F ~> G)(implicit
F: MonadCancel[F, _],
Expand Down
6 changes: 6 additions & 0 deletions modules/datadog/src/main/scala/DDSpan.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,10 @@ final case class DDSpan[F[_]: Sync](
) ++ fields.toList.nested.map(_.value).value.toMap).asJava
)
}.void

override def unsafeRunWithActivatedSpan[T](run: => T): T = {
val scope = tracer.activateSpan(span)
try run
finally scope.close()
}
}
6 changes: 6 additions & 0 deletions modules/jaeger/src/main/scala/JaegerSpan.scala
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ private[jaeger] final case class JaegerSpan[F[_]: Sync](
(Nested(prefix.pure[F]), Nested(traceId)).mapN { (uri, id) =>
uri.resolve(s"/trace/$id")
}.value

override def unsafeRunWithActivatedSpan[T](run: => T): T = {
val scope = tracer.activateSpan(span)
try run
finally scope.close()
}
}

private[jaeger] object JaegerSpan {
Expand Down
6 changes: 6 additions & 0 deletions modules/lightstep/src/main/scala/LightstepSpan.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,10 @@ private[lightstep] final case class LightstepSpan[F[_]: Sync](

// TODO
def traceUri: F[Option[URI]] = none.pure[F]

override def unsafeRunWithActivatedSpan[T](run: => T): T = {
val scope = tracer.activateSpan(span)
try run
finally scope.close()
}
}
6 changes: 6 additions & 0 deletions modules/opencensus/src/main/scala/OpenCensusSpan.scala
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ private[opencensus] final case class OpenCensusSpan[F[_]: Sync](
("error.class" -> TraceValue.StringValue(err.getClass.getSimpleName)) ::
fields.toList: _*
)

override def unsafeRunWithActivatedSpan[T](run: => T): T = {
val scope = tracer.withSpan(span)
try run
finally scope.close()
}
}

private[opencensus] object OpenCensusSpan {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ private[opentelemetry] final case class OpenTelemetrySpan[F[_]: Sync](
(Nested(prefix.pure[F]), Nested(traceId)).mapN { (uri, id) =>
uri.resolve(s"/trace/$id")
}.value

override def unsafeRunWithActivatedSpan[T](run: => T): T = {
val scope = span.makeCurrent()
try run
finally scope.close()
}
}

private[opentelemetry] object OpenTelemetrySpan {
Expand Down