Skip to content

Commit 5214224

Browse files
author
Remi Momprive
committed
Add url.template attribute to otel4s-metrics-backend
1 parent ed0c271 commit 5214224

File tree

3 files changed

+35
-3
lines changed

3 files changed

+35
-3
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,7 @@ lazy val otel4sMetricsBackend = (projectMatrix in file("observability/otel4s-met
967967
libraryDependencies ++= Seq(
968968
"org.typelevel" %%% "otel4s-core-metrics" % otel4s,
969969
"org.typelevel" %%% "otel4s-semconv" % otel4s,
970+
"org.typelevel" %%% "otel4s-semconv-experimental" % otel4s,
970971
"org.typelevel" %%% "otel4s-semconv-metrics-experimental" % otel4s % Test,
971972
"org.typelevel" %%% "otel4s-sdk-metrics-testkit" % otel4sSdk % Test
972973
)

observability/otel4s-metrics-backend/src/main/scala/sttp/client4/opentelemetry/otel4s/Otel4sMetricsBackend.scala

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,18 @@ import org.typelevel.otel4s.semconv.attributes.{
1818
ServerAttributes,
1919
UrlAttributes
2020
}
21+
import org.typelevel.otel4s.semconv.experimental.attributes.UrlExperimentalAttributes
2122
import sttp.client4.listener.{ListenerBackend, RequestListener}
2223
import sttp.client4._
23-
import sttp.model.{HttpVersion, ResponseMetadata, StatusCode}
24+
import sttp.model.{HttpVersion, ResponseMetadata, StatusCode, Uri}
2425
import sttp.client4.wrappers.FollowRedirectsBackend
2526

2627
import scala.concurrent.duration.FiniteDuration
2728
import scala.util.chaining._
2829

2930
object Otel4sMetricsBackend {
31+
private val IdPlaceholder = "{id}"
32+
private val IdRegex = """[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|\d+""".r
3033

3134
def apply[F[_]: Async: MeterProvider](
3235
delegate: Backend[F],
@@ -173,6 +176,7 @@ object Otel4sMetricsBackend {
173176
b ++= ServerAttributes.ServerAddress.maybe(request.uri.host)
174177
b ++= ServerAttributes.ServerPort.maybe(request.uri.port.map(_.toLong))
175178
b ++= UrlAttributes.UrlScheme.maybe(request.uri.scheme)
179+
b ++= UrlExperimentalAttributes.UrlTemplate.maybe(urlTemplate(request))
176180

177181
b.result()
178182
}
@@ -196,6 +200,7 @@ object Otel4sMetricsBackend {
196200
b ++= ServerAttributes.ServerPort.maybe(request.uri.port.map(_.toLong))
197201
b ++= NetworkAttributes.NetworkProtocolVersion.maybe(request.httpVersion.map(networkProtocol))
198202
b ++= UrlAttributes.UrlScheme.maybe(request.uri.scheme)
203+
b ++= UrlExperimentalAttributes.UrlTemplate.maybe(urlTemplate(request))
199204

200205
// response
201206
b ++= HttpAttributes.HttpResponseStatusCode.maybe(responseStatusCode.map(_.code.toLong))
@@ -211,6 +216,33 @@ object Otel4sMetricsBackend {
211216
case HttpVersion.HTTP_2 => "2"
212217
case HttpVersion.HTTP_3 => "3"
213218
}
219+
220+
private def urlTemplate(request: GenericRequest[_, _]): Option[String] = {
221+
val rawSegments = request.uri.pathSegments.segments
222+
val templatedSegments = rawSegments.map(s => if (IdRegex.matches(s.v)) IdPlaceholder else s.v)
223+
val pathChanged = rawSegments.zip(templatedSegments).exists { case (s, t) => s.v != t }
224+
225+
val rawQueryParts = request.uri.querySegments.map {
226+
case Uri.QuerySegment.KeyValue(k, v, _, _) => s"$k=$v"
227+
case Uri.QuerySegment.Value(v, _) => v
228+
case Uri.QuerySegment.Plain(v, _) => v
229+
}
230+
val templatedQueryParts = request.uri.querySegments.map {
231+
case Uri.QuerySegment.KeyValue(k, v, _, _) =>
232+
s"$k=${if (IdRegex.matches(v)) IdPlaceholder else v}"
233+
case Uri.QuerySegment.Value(v, _) =>
234+
if (IdRegex.matches(v)) IdPlaceholder else v
235+
case Uri.QuerySegment.Plain(v, _) =>
236+
IdRegex.replaceAllIn(v, IdPlaceholder)
237+
}
238+
val queryChanged = rawQueryParts != templatedQueryParts
239+
240+
if (pathChanged || queryChanged) {
241+
val pathPart = "/" + templatedSegments.mkString("/")
242+
val queryPart = if (templatedQueryParts.isEmpty) "" else "?" + templatedQueryParts.mkString("&")
243+
Some(pathPart + queryPart)
244+
} else None
245+
}
214246
}
215247

216248
}

observability/otel4s-metrics-backend/src/test/scala/sttp/client4/opentelemetry/otel4s/Otel4sMetricsBackendTest.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import org.typelevel.otel4s.metrics.MeterProvider
1010
import org.typelevel.otel4s.sdk.metrics.data.MetricData
1111
import org.typelevel.otel4s.sdk.testkit.metrics.MetricsTestkit
1212
import org.typelevel.otel4s.semconv.experimental.metrics.HttpExperimentalMetrics
13-
import org.typelevel.otel4s.semconv.metrics.HttpMetrics
1413
import org.typelevel.otel4s.semconv.{MetricSpec, Requirement}
1514
import sttp.model.{Header, StatusCode}
1615
import sttp.client4._
@@ -27,7 +26,7 @@ class Otel4sMetricsBackendTest extends AsyncFreeSpec with Matchers {
2726
"Otel4sMetricsBackend" - {
2827
"should pass the client semantic test" in {
2928
val specs = List(
30-
HttpMetrics.ClientRequestDuration,
29+
HttpExperimentalMetrics.ClientRequestDuration,
3130
HttpExperimentalMetrics.ClientRequestBodySize,
3231
HttpExperimentalMetrics.ClientResponseBodySize,
3332
HttpExperimentalMetrics.ClientActiveRequests

0 commit comments

Comments
 (0)