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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ And this one if you would like to have Json support:
libraryDependencies += "dev.profunktor" %% "fs2-rabbit-circe" % Version
```

OpenTelemetry tracing with otel4s is available as a separate module:

```scala
libraryDependencies += "dev.profunktor" %% "fs2-rabbit-otel4s" % Version
```

## Usage Guide

Check the [official guide](https://fs2-rabbit.profunktor.dev/guide.html) for updated compiling examples.
Expand Down
27 changes: 25 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Dependencies.*
import microsites.ExtraMdFileConfig
import sbtbuildinfo.BuildInfoPlugin
import sbtbuildinfo.BuildInfoPlugin.autoImport.*
import scala.collection.immutable

ThisBuild / name := "fs2-rabbit"
Expand Down Expand Up @@ -84,6 +86,14 @@ def ExamplesDependencies(scalaVersionStr: String): List[ModuleID] =

def TestKitDependencies(scalaVersionStr: String): List[ModuleID] = List(Libraries.scalaCheck)

def Otel4sDependencies(scalaVersionStr: String): List[ModuleID] =
List(
Libraries.otel4sCoreTrace,
Libraries.otel4sSemconv,
Libraries.otel4sSemconvExp % Test,
Libraries.otel4sTestkit % Test
)

def TestsDependencies(scalaVersionStr: String): List[ModuleID] =
List(
Libraries.disciplineScalaCheck % Test,
Expand All @@ -102,7 +112,7 @@ lazy val noPublish = List(
lazy val `fs2-rabbit-root`: Project = project
.in(file("."))
.disablePlugins(MimaPlugin)
.aggregate(`fs2-rabbit`, `fs2-rabbit-circe`, tests, examples, microsite, `fs2-rabbit-testkit`)
.aggregate(`fs2-rabbit`, `fs2-rabbit-circe`, `fs2-rabbit-otel4s`, tests, examples, microsite, `fs2-rabbit-testkit`)
.settings(noPublish)

lazy val `fs2-rabbit`: Project = project
Expand All @@ -120,6 +130,19 @@ lazy val `fs2-rabbit-circe`: Project = project
.enablePlugins(AutomateHeaderPlugin)
.dependsOn(`fs2-rabbit`)

lazy val `fs2-rabbit-otel4s`: Project = project
.in(file("otel4s"))
.settings(commonSettings: _*)
.settings(libraryDependencies ++= Otel4sDependencies(scalaVersion.value))
.settings(
buildInfoPackage := "dev.profunktor.fs2rabbit.otel4s",
buildInfoOptions += BuildInfoOption.PackagePrivate,
buildInfoKeys := Seq[BuildInfoKey]("version" -> version.value),
Test / parallelExecution := false
)
.enablePlugins(AutomateHeaderPlugin, BuildInfoPlugin)
.dependsOn(`fs2-rabbit`)

lazy val tests: Project = project
.in(file("tests"))
.settings(commonSettings: _*)
Expand Down Expand Up @@ -185,7 +208,7 @@ lazy val microsite: Project = project
"-Xlint:-missing-interpolator,_"
)
)
.dependsOn(`fs2-rabbit`, `fs2-rabbit-circe`, `examples`)
.dependsOn(`fs2-rabbit`, `fs2-rabbit-circe`, `fs2-rabbit-otel4s`, `examples`)

// CI build
addCommandAlias("buildFs2Rabbit", ";clean;+test;mdoc")
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2017-2026 ProfunKtor
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.profunktor.fs2rabbit.otel4s

import dev.profunktor.fs2rabbit.model.{DeliveryTag, ExchangeName, QueueName, RoutingKey}

final case class ProcessSpanContext(
queueName: QueueName,
exchangeName: ExchangeName,
routingKey: RoutingKey,
destinationName: String,
deliveryTag: DeliveryTag,
messageId: Option[String],
conversationId: Option[String],
redelivered: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2017-2026 ProfunKtor
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.profunktor.fs2rabbit.otel4s

import dev.profunktor.fs2rabbit.model.{ExchangeName, RoutingKey}

final case class PublishSpanContext(
exchangeName: ExchangeName,
routingKey: RoutingKey,
destinationName: String,
messageId: Option[String],
conversationId: Option[String]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright 2017-2026 ProfunKtor
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.profunktor.fs2rabbit.otel4s

import cats.effect.{Concurrent, Resource}
import cats.syntax.functor.*
import cats.syntax.semigroup.*
import dev.profunktor.fs2rabbit.interpreter.RabbitClient
import org.typelevel.otel4s.semconv.attributes.{ErrorAttributes, ServerAttributes}
import org.typelevel.otel4s.trace.{SpanFinalizer, StatusCode, Tracer, TracerProvider}
import org.typelevel.otel4s.{Attribute, Attributes}

trait RabbitTracer[F[_]] {
def client(client: RabbitClient[F]): TracedRabbitClient[F]
}

object RabbitTracer {

sealed trait Config {
private[otel4s] def tracerName: String
private[otel4s] def constAttributes: Attributes
private[otel4s] def clientId: Option[String]
private[otel4s] def publishSpanSetup: PublishSpanContext => Config.SpanSetup
private[otel4s] def processSpanSetup: ProcessSpanContext => Config.SpanSetup

def withConstAttributes(attributes: Attributes): Config
def addConstAttributes(head: Attribute[?], tail: Attribute[?]*): Config
def withClientId(clientId: String): Config
def withPublishSpanSetup(f: PublishSpanContext => Config.SpanSetup): Config
def withProcessSpanSetup(f: ProcessSpanContext => Config.SpanSetup): Config
def withServerAddress(serverAddress: String, serverPort: Option[Int]): Config
}

object Config {

object Defaults {
val tracerName: String = "fs2.rabbit"

val publishSpanSetup: PublishSpanContext => SpanSetup =
context => SpanSetup(s"publish ${context.destinationName}")

val processSpanSetup: ProcessSpanContext => SpanSetup =
context => SpanSetup(s"process ${context.destinationName}")

val spanFinalizationStrategy: SpanFinalizer.Strategy = {
case Resource.ExitCase.Errored(error) =>
val errorType = Option(error.getClass.getCanonicalName).getOrElse(error.getClass.getName)
val setStatus = Option(error.getMessage)
.map(message => SpanFinalizer.setStatus(StatusCode.Error, message))
.getOrElse(SpanFinalizer.setStatus(StatusCode.Error))

SpanFinalizer.recordException(error) |+|
SpanFinalizer.addAttribute(ErrorAttributes.ErrorType(errorType)) |+|
setStatus

case Resource.ExitCase.Canceled =>
SpanFinalizer.addAttribute(ErrorAttributes.ErrorType("canceled")) |+|
SpanFinalizer.setStatus(StatusCode.Error, "canceled")
}
}

sealed trait SpanSetup {
def spanName: String
def attributes: Attributes
def finalizationStrategy: SpanFinalizer.Strategy
}

object SpanSetup {
def apply(
spanName: String,
attributes: Attributes,
finalizationStrategy: SpanFinalizer.Strategy
): SpanSetup =
SpanSetupImpl(spanName, attributes, finalizationStrategy)

private[RabbitTracer] def apply(spanName: String): SpanSetup =
SpanSetup(spanName, Attributes.empty, Defaults.spanFinalizationStrategy)

final private case class SpanSetupImpl(
spanName: String,
attributes: Attributes,
finalizationStrategy: SpanFinalizer.Strategy
) extends SpanSetup
}

val default: Config =
ConfigImpl(
tracerName = Defaults.tracerName,
constAttributes = Attributes.empty,
clientId = None,
publishSpanSetup = Defaults.publishSpanSetup,
processSpanSetup = Defaults.processSpanSetup
)

final private case class ConfigImpl(
tracerName: String,
constAttributes: Attributes,
clientId: Option[String],
publishSpanSetup: PublishSpanContext => SpanSetup,
processSpanSetup: ProcessSpanContext => SpanSetup
) extends Config {
override def withConstAttributes(attributes: Attributes): Config = copy(constAttributes = attributes)

override def addConstAttributes(head: Attribute[?], tail: Attribute[?]*): Config =
copy(constAttributes = constAttributes + head ++ tail)

override def withClientId(clientId: String): Config = copy(clientId = Some(clientId))

override def withPublishSpanSetup(f: PublishSpanContext => SpanSetup): Config = copy(publishSpanSetup = f)

override def withProcessSpanSetup(f: ProcessSpanContext => SpanSetup): Config = copy(processSpanSetup = f)

override def withServerAddress(serverAddress: String, serverPort: Option[Int]): Config =
copy(
constAttributes = constAttributes +
ServerAttributes.ServerAddress(serverAddress) ++
ServerAttributes.ServerPort.maybe(serverPort.map(_.toLong))
)
}
}

def apply[F[_]](implicit rabbitTracer: RabbitTracer[F]): RabbitTracer[F] = rabbitTracer

def noop[F[_]: Concurrent]: RabbitTracer[F] = new Noop[F]

def create[F[_]: Concurrent: TracerProvider](config: Config): F[RabbitTracer[F]] =
TracerProvider[F]
.tracer(config.tracerName)
.withVersion(BuildInfo.version)
.get
.map(implicit tracer => new Impl[F](config))

def resource[F[_]: Concurrent: TracerProvider](config: Config): Resource[F, RabbitTracer[F]] =
Resource.eval(create(config))

final private class Impl[F[_]: Concurrent: Tracer](config: Config) extends RabbitTracer[F] {
override def client(client: RabbitClient[F]): TracedRabbitClient[F] =
new TracedRabbitClient.Impl[F](client, config)
}

final private class Noop[F[_]: Concurrent] extends RabbitTracer[F] {
override def client(client: RabbitClient[F]): TracedRabbitClient[F] =
TracedRabbitClient.noop(client)
}
}
Loading