Skip to content

Commit

Permalink
Merge pull request #1320 from softwaremill/zio2-opentelemetry
Browse files Browse the repository at this point in the history
Add separate modules for zio-opentelemetry 2, keeping zio1 compatibility in zio1 modules
  • Loading branch information
adamw authored Feb 14, 2022
2 parents 5c93f0d + 58c3cc3 commit 459d9ec
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 33 deletions.
43 changes: 36 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ lazy val allAggregates = projectsWithOptionalNative ++
playJson.projectRefs ++
openTracingBackend.projectRefs ++
prometheusBackend.projectRefs ++
zio1TelemetryOpenTelemetryBackend.projectRefs ++
zio1TelemetryOpenTracingBackend.projectRefs ++
zioTelemetryOpenTelemetryBackend.projectRefs ++
zioTelemetryOpenTracingBackend.projectRefs ++
httpClientBackend.projectRefs ++
Expand Down Expand Up @@ -956,10 +958,10 @@ lazy val prometheusBackend = (projectMatrix in file("metrics/prometheus-backend"
.jvmPlatform(scalaVersions = scala2 ++ scala3)
.dependsOn(core)

lazy val zioTelemetryOpenTelemetryBackend = (projectMatrix in file("metrics/zio-telemetry-open-telemetry-backend"))
lazy val zio1TelemetryOpenTelemetryBackend = (projectMatrix in file("metrics/zio1-telemetry-open-telemetry-backend"))
.settings(commonJvmSettings)
.settings(
name := "zio-telemetry-opentelemetry-backend",
name := "zio1-telemetry-opentelemetry-backend",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-opentelemetry" % "1.0.0",
"org.scala-lang.modules" %% "scala-collection-compat" % "2.6.0",
Expand All @@ -971,10 +973,10 @@ lazy val zioTelemetryOpenTelemetryBackend = (projectMatrix in file("metrics/zio-
.dependsOn(zio1 % compileAndTest)
.dependsOn(core)

lazy val zioTelemetryOpenTracingBackend = (projectMatrix in file("metrics/zio-telemetry-open-tracing-backend"))
lazy val zio1TelemetryOpenTracingBackend = (projectMatrix in file("metrics/zio1-telemetry-open-tracing-backend"))
.settings(commonJvmSettings)
.settings(
name := "zio-telemetry-opentracing-backend",
name := "zio1-telemetry-opentracing-backend",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-opentracing" % "1.0.0",
"org.scala-lang.modules" %% "scala-collection-compat" % "2.6.0"
Expand All @@ -984,6 +986,33 @@ lazy val zioTelemetryOpenTracingBackend = (projectMatrix in file("metrics/zio-te
.dependsOn(zio1 % compileAndTest)
.dependsOn(core)

lazy val zioTelemetryOpenTelemetryBackend = (projectMatrix in file("metrics/zio-telemetry-open-telemetry-backend"))
.settings(commonJvmSettings)
.settings(
name := "zio-telemetry-opentelemetry-backend",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-opentelemetry" % "2.0.0-RC1",
"io.opentelemetry" % "opentelemetry-sdk-testing" % "1.11.0" % Test
),
scalaTest
)
.jvmPlatform(scalaVersions = List(scala2_12, scala2_13) ++ scala3)
.dependsOn(zio % compileAndTest)
.dependsOn(core)

lazy val zioTelemetryOpenTracingBackend = (projectMatrix in file("metrics/zio-telemetry-open-tracing-backend"))
.settings(commonJvmSettings)
.settings(
name := "zio-telemetry-opentracing-backend",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-opentracing" % "2.0.0-RC1",
"org.scala-lang.modules" %% "scala-collection-compat" % "2.6.0"
)
)
.jvmPlatform(scalaVersions = List(scala2_12, scala2_13) ++ scala3)
.dependsOn(zio % compileAndTest)
.dependsOn(core)

lazy val scribeBackend = (projectMatrix in file("logging/scribe"))
.settings(commonJvmSettings)
.settings(
Expand Down Expand Up @@ -1114,8 +1143,8 @@ lazy val docs: ProjectMatrix = (projectMatrix in file("generated-docs")) // impo
httpClientZioBackend,
openTracingBackend,
prometheusBackend,
slf4jBackend
// zioTelemetryOpenTelemetryBackend, // TODO: re-enable once these projects are migrated to zio2
// zioTelemetryOpenTracingBackend
slf4jBackend,
zioTelemetryOpenTelemetryBackend,
zioTelemetryOpenTracingBackend
)
.jvmPlatform(scalaVersions = List(scala2_13))
11 changes: 6 additions & 5 deletions docs/backends/wrappers/zio-opentelemetry.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# zio-telemetry opentelemetry backend

To use, add the following dependency to your project:
To use, add the following dependency to your project (the `zio-*` modules depend on ZIO 2.x; for ZIO 1.x support, use `zio1-*`):

```
"com.softwaremill.sttp.client3" %% "zio-telemetry-opentelemetry-backend" % "@VERSION@"
"com.softwaremill.sttp.client3" %% "zio-telemetry-opentelemetry-backend" % "@VERSION@" // for ZIO 2.x
"com.softwaremill.sttp.client3" %% "zio1-telemetry-opentelemetry-backend" % "@VERSION@" // for ZIO 1.x
```

This backend depends on [zio-opentelemetry](https://github.com/zio/zio-telemetry).
Expand All @@ -23,7 +24,7 @@ ZioTelemetryOpenTelemetryBackend(
By default, the span is named after the HTTP method (e.g "HTTP POST") as [recommended by OpenTelemetry](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#name) for HTTP clients.
You can override the default span name or add additional tags per request by supplying a `ZioTelemetryOpenTelemetryTracer`.

```scala
```scala mdoc:compile-only
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import sttp.client3._
import zio._
Expand All @@ -36,12 +37,12 @@ val tracing: Tracing.Service = ???
def sttpTracer: ZioTelemetryOpenTelemetryTracer = new ZioTelemetryOpenTelemetryTracer {
override def spanName[T](request: Request[T, Nothing]): String = ???

def before[T](request: Request[T, Nothing]): RIO[Tracing, Unit] =
def before[T](request: Request[T, Nothing]): RIO[Tracing.Service, Unit] =
Tracing.setAttribute(SemanticAttributes.HTTP_METHOD.getKey, request.method.method) *>
Tracing.setAttribute(SemanticAttributes.HTTP_URL.getKey, request.uri.toString()) *>
ZIO.unit

def after[T](response: Response[T]): RIO[Tracing, Unit] =
def after[T](response: Response[T]): RIO[Tracing.Service, Unit] =
Tracing.setAttribute(SemanticAttributes.HTTP_STATUS_CODE.getKey, response.code.code) *>
ZIO.unit
}
Expand Down
11 changes: 6 additions & 5 deletions docs/backends/wrappers/zio-opentracing.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# zio-telemetry opentracing backend

To use, add the following dependency to your project:
To use, add the following dependency to your project (the `zio-*` modules depend on ZIO 2.x; for ZIO 1.x support, use `zio1-*`):

```
"com.softwaremill.sttp.client3" %% "zio-telemetry-opentracing-backend" % "@VERSION@"
"com.softwaremill.sttp.client3" %% "zio-telemetry-opentracing-backend" % "@VERSION@" // for ZIO 2.x
"com.softwaremill.sttp.client3" %% "zio1-telemetry-opentracing-backend" % "@VERSION@" // for ZIO 1.x
```

This backend depends on [zio-opentracing](https://github.com/zio/zio-telemetry).
Expand All @@ -18,7 +19,7 @@ new ZioTelemetryOpenTracingBackend(zioBackend)

Additionally you can add tags per request by supplying a `ZioTelemetryOpenTracingTracer`

```scala
```scala mdoc:compile-only
import sttp.client3._
import zio._
import zio.telemetry.opentracing._
Expand All @@ -27,14 +28,14 @@ import sttp.client3.ziotelemetry.opentracing._
implicit val zioBackend: SttpBackend[Task, Any] = ???

def sttpTracer: ZioTelemetryOpenTracingTracer = new ZioTelemetryOpenTracingTracer {
def before[T](request: Request[T, Nothing]): RIO[OpenTracing, Unit] =
def before[T](request: Request[T, Nothing]): RIO[OpenTracing.Service, Unit] =
OpenTracing.tag("span.kind", "client") *>
OpenTracing.tag("http.method", request.method.method) *>
OpenTracing.tag("http.url", request.uri.toString()) *>
OpenTracing.tag("type", "ext") *>
OpenTracing.tag("subtype", "http")

def after[T](response: Response[T]): RIO[OpenTracing, Unit] =
def after[T](response: Response[T]): RIO[OpenTracing.Service, Unit] =
OpenTracing.tag("http.status_code", response.code.code)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import scala.collection.mutable
private class ZioTelemetryOpenTelemetryBackend[+P](
delegate: SttpBackend[Task, P],
tracer: ZioTelemetryOpenTelemetryTracer,
tracing: Tracing
tracing: Tracing.Service
) extends DelegateSttpBackend[Task, P](delegate) {
def send[T, R >: P with Effect[Task]](request: Request[T, R]): Task[Response[T]] = {
val carrier: mutable.Map[String, String] = mutable.Map().empty
Expand All @@ -28,7 +28,7 @@ private class ZioTelemetryOpenTelemetryBackend[+P](
_ <- tracer.after(resp)
} yield resp)
.span(tracer.spanName(request), SpanKind.CLIENT, { case _ => StatusCode.ERROR })
.provide(tracing)
.provideService(tracing)
}
}

Expand All @@ -38,19 +38,19 @@ object ZioTelemetryOpenTelemetryBackend {
tracing: Tracing.Service,
tracer: ZioTelemetryOpenTelemetryTracer = ZioTelemetryOpenTelemetryTracer.empty
): SttpBackend[Task, P] =
new ZioTelemetryOpenTelemetryBackend[P](other, tracer, Has(tracing))
new ZioTelemetryOpenTelemetryBackend[P](other, tracer, tracing)

}

trait ZioTelemetryOpenTelemetryTracer {
def spanName[T](request: Request[T, Nothing]): String = s"HTTP ${request.method.method}"
def before[T](request: Request[T, Nothing]): RIO[Tracing, Unit]
def after[T](response: Response[T]): RIO[Tracing, Unit]
def before[T](request: Request[T, Nothing]): RIO[Tracing.Service, Unit]
def after[T](response: Response[T]): RIO[Tracing.Service, Unit]
}

object ZioTelemetryOpenTelemetryTracer {
val empty: ZioTelemetryOpenTelemetryTracer = new ZioTelemetryOpenTelemetryTracer {
def before[T](request: Request[T, Nothing]): RIO[Tracing, Unit] = ZIO.unit
def after[T](response: Response[T]): RIO[Tracing, Unit] = ZIO.unit
def before[T](request: Request[T, Nothing]): RIO[Tracing.Service, Unit] = ZIO.unit
def after[T](response: Response[T]): RIO[Tracing.Service, Unit] = ZIO.unit
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import zio.telemetry.opentracing._
import scala.jdk.CollectionConverters._

class ZioTelemetryOpenTracingBackend[+P] private (
delegate: SttpBackend[RIO[OpenTracing, *], P],
delegate: SttpBackend[RIO[OpenTracing.Service, *], P],
tracer: ZioTelemetryOpenTracingTracer
) extends DelegateSttpBackend[RIO[OpenTracing, *], P](delegate) {
def send[T, R >: P with Effect[RIO[OpenTracing, *]]](request: Request[T, R]): RIO[OpenTracing, Response[T]] = {
) extends DelegateSttpBackend[RIO[OpenTracing.Service, *], P](delegate) {
def send[T, R >: P with Effect[RIO[OpenTracing.Service, *]]](
request: Request[T, R]
): RIO[OpenTracing.Service, Response[T]] = {
val headers = scala.collection.mutable.Map.empty[String, String]
val buffer = new TextMapAdapter(headers.asJava)
OpenTracing.inject(Format.Builtin.HTTP_HEADERS, buffer).flatMap { _ =>
Expand All @@ -30,19 +32,19 @@ object ZioTelemetryOpenTracingBackend {
def apply[P](
other: SttpBackend[Task, P],
tracer: ZioTelemetryOpenTracingTracer = ZioTelemetryOpenTracingTracer.empty
): SttpBackend[RIO[OpenTracing, *], P] = {
new ZioTelemetryOpenTracingBackend[P](other.extendEnv[OpenTracing], tracer)
): SttpBackend[RIO[OpenTracing.Service, *], P] = {
new ZioTelemetryOpenTracingBackend[P](other.extendEnv[OpenTracing.Service], tracer)
}
}

trait ZioTelemetryOpenTracingTracer {
def before[T](request: Request[T, Nothing]): RIO[OpenTracing, Unit]
def after[T](response: Response[T]): RIO[OpenTracing, Unit]
def before[T](request: Request[T, Nothing]): RIO[OpenTracing.Service, Unit]
def after[T](response: Response[T]): RIO[OpenTracing.Service, Unit]
}

object ZioTelemetryOpenTracingTracer {
val empty: ZioTelemetryOpenTracingTracer = new ZioTelemetryOpenTracingTracer {
def before[T](request: Request[T, Nothing]): RIO[OpenTracing, Unit] = ZIO.unit
def after[T](response: Response[T]): RIO[OpenTracing, Unit] = ZIO.unit
def before[T](request: Request[T, Nothing]): RIO[OpenTracing.Service, Unit] = ZIO.unit
def after[T](response: Response[T]): RIO[OpenTracing.Service, Unit] = ZIO.unit
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package sttp.client3.ziotelemetry.opentelemetry

import io.opentelemetry.api.trace.{SpanKind, StatusCode}
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator
import io.opentelemetry.context.propagation.{TextMapPropagator, TextMapSetter}
import sttp.capabilities.Effect
import sttp.client3._
import zio._
import zio.telemetry.opentelemetry.TracingSyntax.OpenTelemetryZioOps
import zio.telemetry.opentelemetry._

import scala.collection.mutable

private class ZioTelemetryOpenTelemetryBackend[+P](
delegate: SttpBackend[Task, P],
tracer: ZioTelemetryOpenTelemetryTracer,
tracing: Tracing
) extends DelegateSttpBackend[Task, P](delegate) {
def send[T, R >: P with Effect[Task]](request: Request[T, R]): Task[Response[T]] = {
val carrier: mutable.Map[String, String] = mutable.Map().empty
val propagator: TextMapPropagator = W3CTraceContextPropagator.getInstance()
val setter: TextMapSetter[mutable.Map[String, String]] = (carrier, key, value) => carrier.update(key, value)

(for {
_ <- Tracing.inject(propagator, carrier, setter)
_ <- tracer.before(request)
resp <- delegate.send(request.headers(carrier.toMap))
_ <- tracer.after(resp)
} yield resp)
.span(tracer.spanName(request), SpanKind.CLIENT, { case _ => StatusCode.ERROR })
.provide(tracing)
}
}

object ZioTelemetryOpenTelemetryBackend {
def apply[P](
other: SttpBackend[Task, P],
tracing: Tracing.Service,
tracer: ZioTelemetryOpenTelemetryTracer = ZioTelemetryOpenTelemetryTracer.empty
): SttpBackend[Task, P] =
new ZioTelemetryOpenTelemetryBackend[P](other, tracer, Has(tracing))

}

trait ZioTelemetryOpenTelemetryTracer {
def spanName[T](request: Request[T, Nothing]): String = s"HTTP ${request.method.method}"
def before[T](request: Request[T, Nothing]): RIO[Tracing, Unit]
def after[T](response: Response[T]): RIO[Tracing, Unit]
}

object ZioTelemetryOpenTelemetryTracer {
val empty: ZioTelemetryOpenTelemetryTracer = new ZioTelemetryOpenTelemetryTracer {
def before[T](request: Request[T, Nothing]): RIO[Tracing, Unit] = ZIO.unit
def after[T](response: Response[T]): RIO[Tracing, Unit] = ZIO.unit
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package sttp.client3.ziotelemetry.opentelemetry

import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.`export`.SimpleSpanProcessor
import org.scalatest.BeforeAndAfter
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import sttp.client3.impl.zio.{RIOMonadAsyncError, ZioTestBase}
import sttp.client3.testing.SttpBackendStub
import sttp.client3.{Request, Response, SttpBackend, UriContext, basicRequest}
import sttp.model.StatusCode
import zio.Task
import zio.telemetry.opentelemetry.Tracing
import scala.collection.JavaConverters._

import scala.collection.mutable

class ZioTelemetryOpenTelemetryBackendTest extends AnyFlatSpec with Matchers with BeforeAndAfter with ZioTestBase {

private val recordedRequests = mutable.ListBuffer[Request[_, _]]()

private val spanExporter = InMemorySpanExporter.create()

private val mockTracer =
SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)).build().get(getClass.getName)
private val mockTracing = runtime.unsafeRun(Tracing.managed(mockTracer).useNow)

private val backend: SttpBackend[Task, Any] =
ZioTelemetryOpenTelemetryBackend(
SttpBackendStub(new RIOMonadAsyncError[Any]).whenRequestMatchesPartial {
case r if r.uri.toString.contains("echo") =>
recordedRequests += r
Response.ok("")
case r if r.uri.toString.contains("error") =>
throw new RuntimeException("something went wrong")
},
mockTracing
)

before {
recordedRequests.clear()
spanExporter.reset()
}

"ZioTelemetryOpenTelemetryBackend" should "record spans for requests" in {
val response = runtime.unsafeRun(basicRequest.post(uri"http://stub/echo").send(backend))
response.code shouldBe StatusCode.Ok

val spans = spanExporter.getFinishedSpanItems.asScala
spans should have size 1
spans.head.getName shouldBe "HTTP POST"
}

it should "propagate span" in {
val response = runtime.unsafeRun(basicRequest.post(uri"http://stub/echo").send(backend))
response.code shouldBe StatusCode.Ok

val spans = spanExporter.getFinishedSpanItems.asScala
spans should have size 1

val spanId = spans.head.getSpanId
val traceId = spans.head.getTraceId
recordedRequests(0).header("traceparent") shouldBe Some(s"00-${traceId}-${spanId}-01")
}

it should "set span status in case of error" in {
runtime.unsafeRunSync(basicRequest.post(uri"http://stub/error").send(backend))

val spans = spanExporter.getFinishedSpanItems.asScala
spans should have size 1

spans.head.getStatus.getStatusCode shouldBe io.opentelemetry.api.trace.StatusCode.ERROR
}

}
Loading

0 comments on commit 459d9ec

Please sign in to comment.