Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests
Browse files Browse the repository at this point in the history
adamw committed Jan 19, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 43029d4 commit 578c7be
Showing 6 changed files with 103 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ object OpenTelemetryDefaults {
.put(ServerAttributes.SERVER_PORT, request.uri.port.getOrElse(80))

/** @see https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client */
def responseAttributes(response: Response[_]): Attributes =
def responseAttributes(request: GenericRequest[_, _], response: Response[_]): Attributes =
Attributes.builder
.put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, response.code.code.toLong: java.lang.Long)
.build()
Original file line number Diff line number Diff line change
@@ -94,7 +94,7 @@ private class OpenTelemetryMetricsListener(config: OpenTelemetryMetricsConfig)

override def requestSuccessful(request: GenericRequest[_, _], response: Response[_], tag: Option[Long]): Unit = {
val requestAttributes = config.requestAttributes(request)
val responseAttributes = config.responseAttributes(response)
val responseAttributes = config.responseAttributes(request, response)

val combinedAttributes = requestAttributes.toBuilder().putAll(responseAttributes).build()

Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ final case class OpenTelemetryMetricsConfig(
requestToSizeHistogramMapper: GenericRequest[_, _] => Option[HistogramCollectorConfig],
responseToSizeHistogramMapper: (GenericRequest[_, _], Response[_]) => Option[HistogramCollectorConfig],
requestAttributes: GenericRequest[_, _] => Attributes,
responseAttributes: Response[_] => Attributes,
responseAttributes: (GenericRequest[_, _], Response[_]) => Attributes,
errorAttributes: Throwable => Attributes
)

@@ -64,7 +64,8 @@ object OpenTelemetryMetricsConfig {
),
spanName: GenericRequest[_, _] => String = OpenTelemetryDefaults.spanName _,
requestAttributes: GenericRequest[_, _] => Attributes = OpenTelemetryDefaults.requestAttributes _,
responseAttributes: Response[_] => Attributes = OpenTelemetryDefaults.responseAttributes _,
responseAttributes: (GenericRequest[_, _], Response[_]) => Attributes =
OpenTelemetryDefaults.responseAttributes _,
errorAttributes: Throwable => Attributes = OpenTelemetryDefaults.errorAttributes _
): OpenTelemetryMetricsConfig = usingMeter(
openTelemetry
@@ -123,7 +124,8 @@ object OpenTelemetryMetricsConfig {
)
),
requestAttributes: GenericRequest[_, _] => Attributes = OpenTelemetryDefaults.requestAttributes _,
responseAttributes: Response[_] => Attributes = OpenTelemetryDefaults.responseAttributes _,
responseAttributes: (GenericRequest[_, _], Response[_]) => Attributes =
OpenTelemetryDefaults.responseAttributes _,
errorAttributes: Throwable => Attributes = OpenTelemetryDefaults.errorAttributes _
): OpenTelemetryMetricsConfig =
OpenTelemetryMetricsConfig(
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import io.opentelemetry.context.propagation.TextMapSetter
import sttp.capabilities.Effect
import sttp.client4.GenericRequest
import sttp.client4.Response
import sttp.client4.ResponseException
import sttp.client4.SyncBackend
import sttp.client4.wrappers.DelegateBackend
import sttp.client4.wrappers.FollowRedirectsBackend
@@ -14,7 +15,8 @@ import sttp.shared.Identity
import scala.collection.mutable

class OpenTelemetryTracingSyncBackend(delegate: SyncBackend, config: OpenTelemetryTracingSyncConfig)
extends DelegateBackend(delegate) {
extends DelegateBackend(delegate)
with SyncBackend {

private val setter = new TextMapSetter[mutable.Map[String, String]] {
def set(carrier: mutable.Map[String, String], key: String, value: String): Unit = {
@@ -28,18 +30,35 @@ class OpenTelemetryTracingSyncBackend(delegate: SyncBackend, config: OpenTelemet
.setAllAttributes(config.requestAttributes(request))
.startSpan()

val scope = span.makeCurrent()
try {
val carrier = mutable.Map.empty[String, String]
config.propagators.getTextMapPropagator().inject(Context.current(), carrier, setter)
val scope = span.makeCurrent()
try {
val carrier = mutable.Map.empty[String, String]
config.propagators.getTextMapPropagator().inject(Context.current(), carrier, setter)

val requestWithTraceContext = request.headers(carrier.toMap)
val response = delegate.send(requestWithTraceContext)
val requestWithTraceContext = request.headers(carrier.toMap)

span.setAllAttributes(config.responseAttributes(response))
response
try {
val response = delegate.send(requestWithTraceContext)
span.setAllAttributes(config.responseAttributes(request, response))
response
} catch {
case e: Exception =>
ResponseException.find(e) match {
case Some(re) =>
span.setAllAttributes(
config.responseAttributes(request, Response((), re.response.code, request.onlyMetadata))
)
case _ =>
span.setAllAttributes(config.errorAttributes(e))
}
throw e
}
} finally {
scope.close()
}
} finally {
scope.close()
span.end()
}
}
}
@@ -50,6 +69,6 @@ object OpenTelemetryTracingSyncBackend {

def apply(delegate: SyncBackend, config: OpenTelemetryTracingSyncConfig): SyncBackend = {
// redirects should be handled before tracing
FollowRedirectsBackend(OpenTelemetryTracingSyncBackend(delegate, config))
FollowRedirectsBackend(new OpenTelemetryTracingSyncBackend(delegate, config))
}
}
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ case class OpenTelemetryTracingSyncConfig(
clock: Clock,
spanName: GenericRequest[_, _] => String,
requestAttributes: GenericRequest[_, _] => Attributes,
responseAttributes: Response[_] => Attributes,
responseAttributes: (GenericRequest[_, _], Response[_]) => Attributes,
errorAttributes: Throwable => Attributes
)

@@ -24,7 +24,8 @@ object OpenTelemetryTracingSyncConfig {
clock: Clock = Clock.systemUTC(),
spanName: GenericRequest[_, _] => String = OpenTelemetryDefaults.spanName _,
requestAttributes: GenericRequest[_, _] => Attributes = OpenTelemetryDefaults.requestAttributesWithFullUrl _,
responseAttributes: Response[_] => Attributes = OpenTelemetryDefaults.responseAttributes _,
responseAttributes: (GenericRequest[_, _], Response[_]) => Attributes =
OpenTelemetryDefaults.responseAttributes _,
errorAttributes: Throwable => Attributes = OpenTelemetryDefaults.errorAttributes _
): OpenTelemetryTracingSyncConfig = usingTracer(
openTelemetry
@@ -45,7 +46,8 @@ object OpenTelemetryTracingSyncConfig {
clock: Clock = Clock.systemUTC(),
spanName: GenericRequest[_, _] => String = OpenTelemetryDefaults.spanName _,
requestAttributes: GenericRequest[_, _] => Attributes = OpenTelemetryDefaults.requestAttributesWithFullUrl _,
responseAttributes: Response[_] => Attributes = OpenTelemetryDefaults.responseAttributes _,
responseAttributes: (GenericRequest[_, _], Response[_]) => Attributes =
OpenTelemetryDefaults.responseAttributes _,
errorAttributes: Throwable => Attributes = OpenTelemetryDefaults.errorAttributes _
): OpenTelemetryTracingSyncConfig =
OpenTelemetryTracingSyncConfig(
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package sttp.client4.opentelemetry

import org.scalatest.matchers.should.Matchers
import org.scalatest.flatspec.AnyFlatSpec
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.`export`.SimpleSpanProcessor
import sttp.client4.testing.SyncBackendStub
import sttp.client4._
import io.opentelemetry.sdk.OpenTelemetrySdk
import scala.jdk.CollectionConverters._
import io.opentelemetry.semconv.UrlAttributes
import io.opentelemetry.semconv.HttpAttributes
import io.opentelemetry.semconv.ErrorAttributes

class OpenTelemetryTracingSyncBackendTest extends AnyFlatSpec with Matchers {
it should "capture successful spans" in {
// given
val testExporter = InMemorySpanExporter.create()
val tracerProvider = SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(testExporter)).build();
val otel = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build()

val stubBackend = SyncBackendStub.whenAnyRequest.thenRespondOk()
val wrappedBackend = OpenTelemetryTracingSyncBackend(stubBackend, OpenTelemetryTracingSyncConfig(otel))

// when
basicRequest.get(uri"http://test.com/foo").send(wrappedBackend)

// then
val spanItems = testExporter.getFinishedSpanItems().asScala
spanItems should have size 1

val span = spanItems.head
val attributes = span.getAttributes().asMap().asScala
attributes(UrlAttributes.URL_FULL) shouldBe "http://test.com/foo"
attributes(HttpAttributes.HTTP_RESPONSE_STATUS_CODE) shouldBe 200
}

it should "capture spans which end in an exception" in {
// given
val testExporter = InMemorySpanExporter.create()
val tracerProvider = SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(testExporter)).build();
val otel = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build()

val stubBackend = SyncBackendStub.whenAnyRequest.thenRespond(throw new RuntimeException("test"))
val wrappedBackend = OpenTelemetryTracingSyncBackend(stubBackend, OpenTelemetryTracingSyncConfig(otel))

// when
intercept[RuntimeException] {
basicRequest.get(uri"http://test.com/foo").send(wrappedBackend)
}

// then
val spanItems = testExporter.getFinishedSpanItems().asScala
spanItems should have size 1

val span = spanItems.head
val attributes = span.getAttributes().asMap().asScala
attributes(UrlAttributes.URL_FULL) shouldBe "http://test.com/foo"
attributes(ErrorAttributes.ERROR_TYPE) shouldBe "RuntimeException"
}
}

0 comments on commit 578c7be

Please sign in to comment.