Skip to content
Merged
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
22 changes: 9 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,6 @@ val playJsonVersion = "3.0.6"
val catsEffect_3_version = "3.7.0"
val fs2_3_version = "3.13.0"

// This version provides Scala Native 0.5.x support. Drop this when 3.7.0 is released.
val catsEffect_3_RC_version = "3.7.0-RC1"

val catsEffect_2_version = "2.5.5"

val fs2_2_version = "2.5.13"
Expand Down Expand Up @@ -170,8 +167,8 @@ val osLibVersion = "0.11.4"
val tethysVersion = "0.29.8"
val openTelemetryVersion = "1.59.0"
val openTelemetrySemconvVersion = "1.40.0"
val otel4s = "0.15.2"
val otel4sSdk = "0.17.0"
val otel4s = "0.16.0"
val otel4sSdk = "0.18.0"
val slf4jVersion = "1.7.36"

val compileAndTest = "compile->compile;test->test"
Expand Down Expand Up @@ -397,13 +394,10 @@ lazy val catsCe2 = (projectMatrix in file("effects/cats-ce2"))
)

lazy val catsEffect = Def.setting {
val ceVersion =
if (virtualAxes.value.contains(VirtualAxis.native)) catsEffect_3_RC_version
else catsEffect_3_version
Seq(
"org.typelevel" %%% "cats-effect-kernel" % ceVersion,
"org.typelevel" %%% "cats-effect-std" % ceVersion,
"org.typelevel" %%% "cats-effect" % ceVersion % Test
"org.typelevel" %%% "cats-effect-kernel" % catsEffect_3_version,
"org.typelevel" %%% "cats-effect-std" % catsEffect_3_version,
"org.typelevel" %%% "cats-effect" % catsEffect_3_version % Test
)
}

Expand All @@ -424,7 +418,7 @@ lazy val cats = (projectMatrix in file("effects/cats"))
settings = commonJsSettings ++ commonJsBackendSettings ++ browserChromeTestSettings ++ testServerSettings
)
.nativePlatform(
scalaVersions = List(scala3),
scalaVersions = scala2And3,
settings = commonNativeSettings ++ testServerSettings
)

Expand Down Expand Up @@ -975,13 +969,14 @@ lazy val otel4sMetricsBackend = (projectMatrix in file("observability/otel4s-met
libraryDependencies ++= Seq(
"org.typelevel" %%% "otel4s-core-metrics" % otel4s,
"org.typelevel" %%% "otel4s-semconv" % otel4s,
"org.typelevel" %%% "otel4s-semconv-experimental" % otel4s,
"org.typelevel" %%% "otel4s-semconv-experimental" % otel4s % Test,
"org.typelevel" %%% "otel4s-semconv-metrics-experimental" % otel4s % Test,
"org.typelevel" %%% "otel4s-sdk-metrics-testkit" % otel4sSdk % Test
)
)
.jvmPlatform(scalaVersions = scala2_13And3, settings = commonJvmSettings)
.jsPlatform(scalaVersions = scala2_13And3, settings = commonJsSettings)
.nativePlatform(scalaVersions = scala2_13And3, settings = commonNativeSettings)
.dependsOn(cats % Test)
.dependsOn(core % compileAndTest)

Expand All @@ -997,6 +992,7 @@ lazy val otel4sTracingBackend = (projectMatrix in file("observability/otel4s-tra
)
.jvmPlatform(scalaVersions = scala2_13And3, settings = commonJvmSettings)
.jsPlatform(scalaVersions = scala2_13And3, settings = commonJsSettings)
.nativePlatform(scalaVersions = scala2_13And3, settings = commonNativeSettings)
.dependsOn(cats % Test)
.dependsOn(core % compileAndTest)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package sttp.client4.curl.cats

import cats.effect.kernel.Sync
import sttp.capabilities.Effect
import sttp.client4.curl.AbstractSyncCurlBackend
import sttp.client4.impl.cats.CatsMonadError
import sttp.client4.wrappers.FollowRedirectsBackend
import sttp.client4.Backend

class CurlCatsBackend[F[_]: Sync] private (verbose: Boolean)
extends AbstractSyncCurlBackend[F](new CatsMonadError[F], verbose)
with Backend[F] {
override type R = Effect[F]
}
with Backend[F]

object CurlCatsBackend {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import org.typelevel.otel4s.semconv.attributes.{
ServerAttributes,
UrlAttributes
}
import org.typelevel.otel4s.semconv.experimental.attributes.UrlExperimentalAttributes
import sttp.client4.listener.{ListenerBackend, RequestListener}
import sttp.client4._
import sttp.model.{HttpVersion, ResponseMetadata, StatusCode}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package sttp.client4.opentelemetry.otel4s

import org.typelevel.otel4s.AttributeKey

private object UrlExperimentalAttributes {
// url.template is still experimental, so we need to wait for it to be stable before pulling it from the otel4s semconv
val UrlTemplate: AttributeKey[String] =
AttributeKey("url.template")
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
package sttp.client4.opentelemetry.otel4s

import cats.effect.IO
import cats.effect.unsafe.IORuntime
import cats.effect.unsafe.implicits.global
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.freespec.AsyncFreeSpec
import org.scalatest.matchers.should.Matchers
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.sdk.metrics.data.MetricData
import org.typelevel.otel4s.sdk.testkit.metrics.{MetricExpectation, MetricExpectations}
import org.typelevel.otel4s.sdk.testkit.metrics.MetricsTestkit
import org.typelevel.otel4s.semconv.metrics.HttpMetrics
import org.typelevel.otel4s.semconv.experimental.metrics.HttpExperimentalMetrics
import org.typelevel.otel4s.semconv.{MetricSpec, Requirement}
import sttp.model.{Header, StatusCode}
import sttp.client4._
import sttp.client4.impl.cats.CatsMonadAsyncError
import sttp.client4.testing.{BackendStub, ResponseStub, StubBody}

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

class Otel4sMetricsBackendTest extends AsyncFreeSpec with Matchers {

override def executionContext: ExecutionContext = ExecutionContext.global
class Otel4sMetricsBackendTest extends AsyncFreeSpec with Matchers with AsyncExecutionContext {

"Otel4sMetricsBackend" - {
"should pass the client semantic test" in {
val specs = List(
HttpExperimentalMetrics.ClientRequestDuration,
HttpMetrics.ClientRequestDuration,
HttpExperimentalMetrics.ClientRequestBodySize,
HttpExperimentalMetrics.ClientResponseBodySize,
HttpExperimentalMetrics.ClientActiveRequests
Expand All @@ -50,41 +47,41 @@ class Otel4sMetricsBackendTest extends AsyncFreeSpec with Matchers {

makeBackend.use { backend =>
for {
r <- backend.send(basicRequest.post(uri"http://localhost:8080/success").body("payload"))
_ <- backend.send(basicRequest.post(uri"http://localhost:8080/success").body("payload"))
// we use `.unsafeRunAndForget()` in the backend and JS could be slow
_ <- IO.sleep(1.second)
metrics <- testkit.collectMetrics
_ = specs.foreach(spec => specTest(metrics, spec))
} yield succeed
} yield assertMetricsMatch(metrics, specs.map(metricExpectation))
}
}
.unsafeToFuture()
}
}

private def specTest(metrics: List[MetricData], spec: MetricSpec): Unit = {
val metric = metrics.find(_.name == spec.name)
assert(
metric.isDefined,
s"${spec.name} metric is missing. Available [${metrics.map(_.name).mkString(", ")}]"
)

metric.foreach { md =>
md.name shouldBe spec.name
md.description shouldBe Some(spec.description)
md.unit shouldBe Some(spec.unit)

val required = spec.attributeSpecs
.filter(_.requirement.level == Requirement.Level.Required)
.map(_.key)
.toSet
private def metricExpectation(spec: MetricSpec): MetricExpectation = {
val required = spec.attributeSpecs
.filter(_.requirement.level == Requirement.Level.Required)
.map(_.key)
.toSet

val current = md.data.points.toVector
.flatMap(_.attributes.map(_.key))
.filter(key => required.contains(key))
.toSet
MetricExpectation
.name(spec.name)
.description(spec.description)
.unit(spec.unit)
.clue(spec.name)
.where("required semantic-convention attributes are present") { metric =>
metric.data.points.iterator
.flatMap(_.attributes.iterator.map(_.key))
.filter(required.contains)
.toSet == required
}
}

current shouldBe required
private def assertMetricsMatch(metrics: List[MetricData], expectations: List[MetricExpectation]) =
MetricExpectations.checkAllDistinct(metrics, expectations) match {
case Right(_) =>
succeed
case Left(mismatches) =>
fail(MetricExpectations.format(mismatches))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package sttp.client4.opentelemetry.otel4s

import org.scalatest.AsyncTestSuite

import scala.concurrent.ExecutionContext

trait AsyncExecutionContext { self: AsyncTestSuite =>
override def executionContext: ExecutionContext = ExecutionContext.global
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package sttp.client4.opentelemetry.otel4s

trait AsyncExecutionContext {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package sttp.client4.opentelemetry.otel4s

trait AsyncExecutionContext {}
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,20 @@ import cats.effect.unsafe.implicits.global
import org.scalatest.freespec.AsyncFreeSpec
import org.scalatest.matchers.should.Matchers
import org.typelevel.otel4s.sdk.data.LimitedData
import org.typelevel.otel4s.sdk.testkit.trace.TracesTestkit
import org.typelevel.otel4s.sdk.testkit.trace._
import org.typelevel.otel4s.sdk.trace.context.propagation.W3CTraceContextPropagator
import org.typelevel.otel4s.sdk.trace.data.{EventData, StatusData}
import org.typelevel.otel4s.trace.{StatusCode, TracerProvider}
import org.typelevel.otel4s.sdk.trace.data.{EventData, SpanData}
import org.typelevel.otel4s.trace.TracerProvider
import org.typelevel.otel4s.{Attribute, Attributes}
import sttp.client4._
import sttp.client4.impl.cats.CatsMonadAsyncError
import sttp.client4.testing.{BackendStub, ResponseStub, StubBody}
import sttp.model.{StatusCode => HttpStatusCode}

import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import scala.util.control.NoStackTrace

class Otel4sTracingBackendTest extends AsyncFreeSpec with Matchers {

override def executionContext: ExecutionContext = ExecutionContext.global
class Otel4sTracingBackendTest extends AsyncFreeSpec with Matchers with AsyncExecutionContext {

"Otel4sTracingBackend" - {
"should add tracing headers to the request" in {
Expand Down Expand Up @@ -86,8 +83,6 @@ class Otel4sTracingBackendTest extends AsyncFreeSpec with Matchers {
response <- backend.send(basicRequest.get(uri"http://user:pwd@localhost:8080/success?q=v"))
spans <- testkit.finishedSpans
} yield {
val status = StatusData(StatusCode.Unset)

val attributes = Attributes(
Attribute("http.request.method", "GET"),
Attribute("http.response.status_code", 200L),
Expand All @@ -99,11 +94,19 @@ class Otel4sTracingBackendTest extends AsyncFreeSpec with Matchers {

response.code shouldBe HttpStatusCode.Ok

spans.map(_.attributes.elements) shouldBe List(attributes)
spans.map(_.events.elements) shouldBe List(Vector.empty)
spans.map(_.status) shouldBe List(status)

succeed
assertSpansMatch(
spans,
TraceForestExpectation.ordered(
TraceExpectation.leaf(
SpanExpectation
.client("GET")
.noParentSpanContext
.attributesExact(attributes)
.status(StatusExpectation.unset)
.eventCount(0)
)
)
)
}
}
.unsafeToFuture()
Expand All @@ -130,8 +133,6 @@ class Otel4sTracingBackendTest extends AsyncFreeSpec with Matchers {
response <- backend.send(basicRequest.get(uri"http://user@localhost:8080/bad-request?q=v"))
spans <- testkit.finishedSpans
} yield {
val status = StatusData(StatusCode.Error)

val attributes = Attributes(
Attribute("http.request.method", "GET"),
Attribute("http.response.status_code", 400L),
Expand All @@ -144,11 +145,19 @@ class Otel4sTracingBackendTest extends AsyncFreeSpec with Matchers {

response.code shouldBe HttpStatusCode.BadRequest

spans.map(_.attributes.elements) shouldBe List(attributes)
spans.map(_.events.elements) shouldBe List(Vector.empty)
spans.map(_.status) shouldBe List(status)

succeed
assertSpansMatch(
spans,
TraceForestExpectation.ordered(
TraceExpectation.leaf(
SpanExpectation
.client("GET")
.noParentSpanContext
.attributesExact(attributes)
.status(StatusExpectation.error)
.eventCount(0)
)
)
)
}
}
.unsafeToFuture()
Expand Down Expand Up @@ -179,8 +188,6 @@ class Otel4sTracingBackendTest extends AsyncFreeSpec with Matchers {
response <- backend.send(basicRequest.get(uri"http://localhost/error")).attempt
spans <- testkit.finishedSpans
} yield {
val status = StatusData(StatusCode.Error)

val attributes = Attributes(
Attribute("http.request.method", "GET"),
Attribute("server.address", "localhost"),
Expand All @@ -197,16 +204,34 @@ class Otel4sTracingBackendTest extends AsyncFreeSpec with Matchers {

response shouldBe Left(Err)

spans.map(_.attributes.elements) shouldBe List(attributes)
spans.map(_.events.elements) shouldBe List(Vector(event))
spans.map(_.status) shouldBe List(status)

succeed
assertSpansMatch(
spans,
TraceForestExpectation.ordered(
TraceExpectation.leaf(
SpanExpectation
.client("GET")
.noParentSpanContext
.attributesExact(attributes)
.status(StatusExpectation.error)
.exactlyEvents(
EventExpectation.any.where("matches the recorded exception event")(_ == event)
)
)
)
)
}
}
}
.unsafeToFuture()
}
}

private def assertSpansMatch(spans: List[SpanData], expectation: TraceForestExpectation) =
TraceExpectations.check(spans, expectation) match {
case Right(_) =>
succeed
case Left(mismatches) =>
fail(TraceExpectations.format(mismatches))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package sttp.client4.opentelemetry.otel4s

import org.scalatest.AsyncTestSuite

import scala.concurrent.ExecutionContext

trait AsyncExecutionContext { self: AsyncTestSuite =>
override def executionContext: ExecutionContext = ExecutionContext.global
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package sttp.client4.opentelemetry.otel4s

trait AsyncExecutionContext {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package sttp.client4.opentelemetry.otel4s

trait AsyncExecutionContext {}
Loading