Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Jan 19, 2025
1 parent 43029d4 commit 578c7be
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand All @@ -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()
}
}
}
Expand All @@ -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
Expand Up @@ -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
)

Expand All @@ -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
Expand All @@ -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(
Expand Down
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.