Skip to content

Commit

Permalink
Add open-telemetry sync tracing backend wrapper; rename otel artifact (
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw authored Jan 19, 2025
1 parent e0c116c commit f802b16
Show file tree
Hide file tree
Showing 12 changed files with 389 additions and 115 deletions.
12 changes: 7 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ val http4s_ce3_version = "0.23.30"
val osLibVersion = "0.11.3"
val tethysVersion = "0.29.3"
val openTelemetryVersion = "1.46.0"
val openTelemetrySemconvVersion = "1.26.0-alpha"
val slf4jVersion = "1.7.36"

val compileAndTest = "compile->compile;test->test"
Expand Down Expand Up @@ -228,7 +229,7 @@ lazy val rawAllAggregates =
playJson.projectRefs ++
tethysJson.projectRefs ++
prometheusBackend.projectRefs ++
openTelemetryMetricsBackend.projectRefs ++
openTelemetryBackend.projectRefs ++
openTelemetryTracingZioBackend.projectRefs ++
finagleBackend.projectRefs ++
armeriaBackend.projectRefs ++
Expand Down Expand Up @@ -898,11 +899,12 @@ lazy val prometheusBackend = (projectMatrix in file("observability/prometheus-ba
.jvmPlatform(scalaVersions = scala2And3)
.dependsOn(core)

lazy val openTelemetryMetricsBackend = (projectMatrix in file("observability/opentelemetry-metrics-backend"))
lazy val openTelemetryBackend = (projectMatrix in file("observability/opentelemetry-backend"))
.settings(commonJvmSettings)
.settings(
name := "opentelemetry-metrics-backend",
name := "opentelemetry-backend",
libraryDependencies ++= Seq(
"io.opentelemetry.semconv" % "opentelemetry-semconv" % openTelemetrySemconvVersion,
"io.opentelemetry" % "opentelemetry-api" % openTelemetryVersion,
"io.opentelemetry" % "opentelemetry-sdk-testing" % openTelemetryVersion % Test
),
Expand All @@ -917,7 +919,7 @@ lazy val openTelemetryTracingZioBackend = (projectMatrix in file("observability/
name := "opentelemetry-tracing-zio-backend",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-opentelemetry" % "3.1.1",
"io.opentelemetry.semconv" % "opentelemetry-semconv" % "1.26.0-alpha",
"io.opentelemetry.semconv" % "opentelemetry-semconv" % openTelemetrySemconvVersion,
"io.opentelemetry" % "opentelemetry-api" % openTelemetryVersion,
"io.opentelemetry" % "opentelemetry-sdk-testing" % openTelemetryVersion % Test
)
Expand Down Expand Up @@ -1065,7 +1067,7 @@ lazy val docs: ProjectMatrix = (projectMatrix in file("generated-docs")) // impo
// okhttpMonixBackend,
http4sBackend,
prometheusBackend,
openTelemetryMetricsBackend,
openTelemetryBackend,
openTelemetryTracingZioBackend,
slf4jBackend
)
Expand Down
54 changes: 50 additions & 4 deletions docs/backends/wrappers/opentelemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Currently, the following OpenTelemetry features are supported:

- metrics using `OpenTelemetryMetricsBackend`, wrapping any other backend
- tracing using `OpenTelemetryTracingSyncBackend`, wrapping a synchronous backend
- tracing using `OpenTelemetryTracingZioBackend`, wrapping any ZIO2 backend
- tracing using [trace4cats](https://github.com/trace4cats/trace4cats), wrapping a cats-effect backend

Expand All @@ -12,7 +13,7 @@ The backend depends only on [opentelemetry-api](https://github.com/open-telemetr
following dependency to your project:

```
"com.softwaremill.sttp.client4" %% "opentelemetry-metrics-backend" % "@VERSION@"
"com.softwaremill.sttp.client4" %% "opentelemetry-backend" % "@VERSION@"
```

Then an instance can be obtained as follows:
Expand Down Expand Up @@ -50,6 +51,52 @@ OpenTelemetryMetricsBackend(
)
```

## Tracing (synchronous)

To use, add the following dependency to your project:

```
"com.softwaremill.sttp.client4" %% "opentelemetry-backend" % "@VERSION@"
```

The backend records traces corresponding to HTTP client calls. The default span name is the HTTP method (e.g. `POST`),
but this can be customized to provide more accurate (but still general) span names by providing a custom
span-name-generating method (as [recommended by OpenTelemetry](https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name)).
Alternative span naming strategies might include reading request's attributes (to determine the target URI template),
or parts of the URI.

Other aspects of the backend can be configured as well:

* the `Tracer` instance and context propagators
* how request, response, error attributes are computed

```{note}
The backend relies on context passed using the default mechanism in OpenTelemetry SDK for Java, that is using
`ThreadLocal`s. This means that the backend will **not** work properly when combined with any asynchronous
execution mechanisms, such as `Future`s.
Moreover, for Java 21+, note that `ThreadLocal`s are not inherited when spawning a new virtual thread.
```

Example usage:

```scala mdoc:compile-only
import sttp.client4.*
import sttp.client4.opentelemetry.*
import io.opentelemetry.api.OpenTelemetry

val sttpBackend: SyncBackend = ???
val openTelemetry: OpenTelemetry = ???

OpenTelemetryTracingSyncBackend(
sttpBackend,
OpenTelemetryTracingSyncConfig(
openTelemetry,
spanName = request => request.uri.pathSegments.segments.headOption.map(_.v).getOrElse("root")
)
)
```

## Tracing (ZIO)

To use, add the following dependency to your project:
Expand All @@ -63,7 +110,7 @@ This backend depends on [zio-opentelemetry](https://github.com/zio/zio-telemetry
The OpenTelemetry backend wraps a `Task` based ZIO backend.
In order to do that, you need to provide the wrapper with a `Tracing` from zio-telemetry.

Here's how you construct `ZioTelemetryOpenTelemetryBackend`. I would recommend wrapping this is in `ZLayer`
Here's how you construct `ZioTelemetryOpenTelemetryBackend`:

```scala mdoc:compile-only
import sttp.client4.*
Expand All @@ -77,8 +124,7 @@ val tracing: Tracing = ???
OpenTelemetryTracingZioBackend(zioBackend, tracing)
```

By default, the span is named after the HTTP method (e.g "HTTP POST") as [recommended by OpenTelemetry](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client) for HTTP clients,
and the http method, url and response status codes are set as span attributes.
By default, the span is named after the HTTP method (e.g `POST`) as [recommended by OpenTelemetry](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client) for HTTP clients, and the http method, url and response status codes are set as span attributes.
You can override these defaults by supplying a custom `OpenTelemetryZioTracer`.

## Tracing (cats-effect)
Expand Down
3 changes: 2 additions & 1 deletion docs/migrate_v3_v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ Any `Either`-based response description can be converted to a failing one using
* request attributes replace request tags (same mechanism as in Tapir)
* the parametrization of `ResponseException` is simplified, `DeserializationException` does not have a type parameter, always requiring an `Exception` as the cause instead
* when a `ResponseException` is thrown in the response handling specification, this will be logged as a successful response (as the response was received correctly), and counted as a success in the metrics as well
* `HttpError` is renamed to `UnexpectedStatusCode`, and along with `DeserializationException`, both types are nested within `ResponseException`
* `HttpError` is renamed to `UnexpectedStatusCode`, and along with `DeserializationException`, both types are nested within `ResponseException`
* the `opentelemetry-metrics-backend` module is renamed to `opentelemetry-backend`
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package sttp.client4.opentelemetry

import sttp.client4.GenericRequest
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.semconv.HttpAttributes
import sttp.client4.Response
import io.opentelemetry.semconv.UrlAttributes
import io.opentelemetry.semconv.ErrorAttributes
import io.opentelemetry.semconv.ServerAttributes
import io.opentelemetry.api.common.AttributesBuilder

object OpenTelemetryDefaults {

/** @see https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name */
def spanName(request: GenericRequest[_, _]): String = s"${request.method.method}"

/** @see https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client */
def requestAttributes(request: GenericRequest[_, _]): Attributes = requestAttributesBuilder(request).build()

/** @see
* https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client (full url is required for tracing, but
* not for metrics)
*/
def requestAttributesWithFullUrl(request: GenericRequest[_, _]): Attributes =
requestAttributesBuilder(request).put(UrlAttributes.URL_FULL, request.uri.toString()).build()

private def requestAttributesBuilder(request: GenericRequest[_, _]): AttributesBuilder =
Attributes.builder
.put(HttpAttributes.HTTP_REQUEST_METHOD, request.method.method)
.put(UrlAttributes.URL_FULL, request.uri.toString())
.put(ServerAttributes.SERVER_ADDRESS, request.uri.host.getOrElse("unknown"))
.put(ServerAttributes.SERVER_PORT, request.uri.port.getOrElse(80))

/** @see https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client */
def responseAttributes(request: GenericRequest[_, _], response: Response[_]): Attributes =
Attributes.builder
.put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, response.code.code.toLong: java.lang.Long)
.build()

def errorAttributes(e: Throwable): Attributes = {
val errorType = e match {
case _: java.net.UnknownHostException => "unknown_host"
case _ => e.getClass.getSimpleName
}
Attributes.builder().put(ErrorAttributes.ERROR_TYPE, errorType).build()
}

val instrumentationScopeName = "sttp-client4"

val instrumentationScopeVersion = "1.0.0"
}
Loading

0 comments on commit f802b16

Please sign in to comment.