Skip to content

Commit

Permalink
site: add Tracing context propagation page
Browse files Browse the repository at this point in the history
  • Loading branch information
iRevive committed Oct 27, 2023
1 parent 31ec53a commit 83febbc
Showing 1 changed file with 132 additions and 0 deletions.
132 changes: 132 additions & 0 deletions docs/tracing-context-propagation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Tracing context propagation

The tracing context propagation logic revolves around [cats.mtl.Local][mtl-local] semantics:
```scala
trait Local[F[_], E] {
def ask: F[E]
def local(fa: F[A])(f: E => E): F[A]
}
```

It allows us to express and manage local modifications of the tracing context within effectful computations.

`Local` works out of the box with the [cats.data.Kleisli][kleisli].
It also works with [cats.effect.IOLocal][io-local] with a little help.

Here is an example of how the span propagation works:
```mermaid
graph LR
A["Fiber A (no span)"] -->|fork| B_1["Fiber B (no span)"] -->|start span 'B' | B_2["Fiber B (span 'B')"]
A --> A_2["Fiber A (no span)"]
A --> |fork| C_1["Fiber C (no span)"] -->|start span 'C' | C_2["Fiber C (span 'C')"]
```

## How to choose the context carrier

In the vast majority of cases, `IOLocal` is the preferred and efficient way to propagate the context.
You can find both examples below and choose which one suits your requirements.

### 1. IOLocal

```scala mdoc:silent:reset
import cats.effect._
import cats.mtl.Local
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 program[F[_] : Async](otel4s: OtelJava[F]): F[Unit] =
Async[F].unit

val run: IO[Unit] =
IOLocal(Context.root).flatMap { implicit ioLocal: IOLocal[Context] =>
createOtel4s[IO].use(otel4s => program(otel4s))
}
```

If you don't need direct access to the `IOLocal` instance, there is also a shortcut `OtelJava.forAsync`:

```scala mdoc:silent:reset
import cats.effect._
import org.typelevel.otel4s.java.OtelJava
import io.opentelemetry.api.GlobalOpenTelemetry

def createOtel4s[F[_]: Async: LiftIO]: Resource[F, OtelJava[F]] =
Resource
.eval(Async[F].delay(GlobalOpenTelemetry.get))
.evalMap(OtelJava.forAsync[F])

def program[F[_]: Async](otel4s: OtelJava[F]): F[Unit] =
Async[F].unit

val run: IO[Unit] =
createOtel4s[IO].use(otel4s => program(otel4s))
```

### 2. Kleisli

```scala mdoc:silent:reset
import cats.effect._
import cats.data.Kleisli
import cats.mtl.Local
import org.typelevel.otel4s.java.context.Context
import org.typelevel.otel4s.java.OtelJava
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 program[F[_]: Async](otel4s: OtelJava[F]): F[Unit] =
Async[F].unit

val kleisli: Kleisli[IO, Context, Unit] =
createOtel4s[Kleisli[IO, Context, *]].use(otel4s => program(otel4s))

val run: IO[Unit] = kleisli.run(Context.root)
```

## Limitations

The current encoding of [cats.effect.Resource][resource] is incompatible with `Local` semantics.
For example, it's impossible to trace different stages of the resource (e.g. acquire, use, release) **and**
stay within the resource. A similar situation is with `fs2.Stream`.

For example, you cannot get the following structure of the traces without allocation of the resource:
```
> resource
> acquire
> use
> inner spans
> release
```

To make it partially work, you need to trace each stage explicitly:
```scala
def acquire: F[Connection[F]] = ???
def release(c: Connection[F]): F[Unit] = ???

val resource: Resource[F, Connection[F]] =
Resource.make(Tracer[F].span("acquire-connection").surround(acquire)) { connection =>
Tracer[F].span("release").surround(release(connection))
}

val io: F[Unit] = Tracer[F].span("resource").surround(
resource.use(connection => Tracer[F].span("use").surround(fa(connection)))
)
```



[mtl-local]: https://typelevel.org/cats-mtl/mtl-classes/local.html
[io-local]: https://typelevel.org/cats-effect/docs/core/io-local
[kleisli]: https://typelevel.org/cats/datatypes/kleisli.html
[resource]: https://typelevel.org/cats-effect/docs/std/resource

0 comments on commit 83febbc

Please sign in to comment.