Skip to content

Commit

Permalink
Use {Meter,Tracer}Provider, not {Meter,Tracer}
Browse files Browse the repository at this point in the history
Use `MeterProvider` for `MetricsOps` rather than `Meter` and use
`TracerProvider for middlewares rather than `Tracer`.
  • Loading branch information
NthPortal committed Dec 12, 2024
1 parent e1aa749 commit 3b029ea
Show file tree
Hide file tree
Showing 9 changed files with 413 additions and 361 deletions.
4 changes: 4 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ lazy val `http4s-otel4s-middleware` = tlCrossRootProject
lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("core"))
.enablePlugins(BuildInfoPlugin)
.settings(sharedSettings)
.settings(
name := s"$baseName-core",
Expand All @@ -57,6 +58,9 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
"org.typelevel" %%% "otel4s-core-common" % otel4sV,
"org.typelevel" %%% "otel4s-semconv" % otel4sV,
),
buildInfoKeys := Seq(version),
buildInfoPackage := "org.http4s.otel4s.middleware",
buildInfoOptions += BuildInfoOption.PackagePrivate,
)

lazy val metrics = crossProject(JVMPlatform, JSPlatform, NativePlatform)
Expand Down
26 changes: 14 additions & 12 deletions examples/src/main/scala/example/Http4sExample.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ import org.http4s.otel4s.middleware.trace.server.ServerMiddlewareBuilder
import org.http4s.server.Server
import org.http4s.server.middleware.Metrics
import org.typelevel.otel4s.Otel4s
import org.typelevel.otel4s.metrics.Meter
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.trace.Tracer
import org.typelevel.otel4s.trace.TracerProvider

/** Start up Jaeger thus:
*
Expand Down Expand Up @@ -73,20 +74,21 @@ object Http4sExample extends IOApp with Common {
def tracer[F[_]](otel: Otel4s[F]): F[Tracer[F]] =
otel.tracerProvider.tracer("Http4sExample").get

def meter[F[_]](otel: Otel4s[F]): F[Meter[F]] =
otel.meterProvider.meter("Http4sExample").get

// Our main app resource
def server[F[_]: Async: Network: Tracer: Meter]: Resource[F, Server] =
def server[F[_]: Async: Network: TracerProvider: Tracer: MeterProvider]: Resource[F, Server] =
for {
clientMiddleware <- ClientMiddlewareBuilder.default(redactor).build.toResource
client <- EmberClientBuilder
.default[F]
.build
.map(ClientMiddlewareBuilder.default(redactor).build)
.map(clientMiddleware)
metricsOps <- OtelMetrics.serverMetricsOps[F]().toResource
app = ServerMiddlewareBuilder.default[F](redactor).buildHttpApp {
Metrics(metricsOps)(routes(client)).orNotFound
}
app <- ServerMiddlewareBuilder
.default[F](redactor)
.buildHttpApp {
Metrics(metricsOps)(routes(client)).orNotFound
}
.toResource
sv <- EmberServerBuilder.default[F].withPort(port"8080").withHttpApp(app).build
} yield sv

Expand All @@ -95,10 +97,10 @@ object Http4sExample extends IOApp with Common {
OtelJava
.autoConfigured[IO]()
.flatMap { otel4s =>
implicit val TP: TracerProvider[IO] = otel4s.tracerProvider
implicit val MP: MeterProvider[IO] = otel4s.meterProvider
Resource.eval(tracer(otel4s)).flatMap { implicit T: Tracer[IO] =>
Resource.eval(meter(otel4s)).flatMap { implicit M: Meter[IO] =>
server[IO]
}
server[IO]
}
}
.use(_ => IO.never)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ object OtelMetrics {
* @param attributes additional [[org.typelevel.otel4s.Attributes]] that are added to all metrics
* @param responseDurationSecondsHistogramBuckets histogram buckets for the response duration metrics
*/
def clientMetricsOps[F[_]: Monad: Meter](
def clientMetricsOps[F[_]: Monad: MeterProvider](
attributes: Attributes = Attributes.empty,
responseDurationSecondsHistogramBuckets: BucketBoundaries = DefaultHistogramBuckets,
): F[MetricsOps[F]] =
Expand All @@ -72,7 +72,7 @@ object OtelMetrics {
* @param attributes additional [[org.typelevel.otel4s.Attributes]] that are added to all metrics
* @param responseDurationSecondsHistogramBuckets histogram buckets for the response duration metrics
*/
def serverMetricsOps[F[_]: Monad: Meter](
def serverMetricsOps[F[_]: Monad: MeterProvider](
attributes: Attributes = Attributes.empty,
responseDurationSecondsHistogramBuckets: BucketBoundaries = DefaultHistogramBuckets,
): F[MetricsOps[F]] =
Expand All @@ -82,16 +82,23 @@ object OtelMetrics {
responseDurationSecondsHistogramBuckets,
)

private def metricsOps[F[_]: Monad: Meter](
private def metricsOps[F[_]: Monad: MeterProvider](
kind: String,
attributes: Attributes,
responseDurationSecondsHistogramBuckets: BucketBoundaries,
): F[MetricsOps[F]] =
for {
metrics <- createMetricsCollection(
kind,
responseDurationSecondsHistogramBuckets,
)
meter <- MeterProvider[F]
.meter(s"org.http4s.otel4s.middleware.$kind")
.withVersion(org.http4s.otel4s.middleware.BuildInfo.version)
.get
metrics <- {
implicit val M: Meter[F] = meter
createMetricsCollection(
kind,
responseDurationSecondsHistogramBuckets,
)
}
} yield createMetricsOps(
metrics,
attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import cats.effect.IO
import munit.CatsEffectSuite
import org.http4s.server.middleware.Metrics
import org.typelevel.otel4s.Attributes
import org.typelevel.otel4s.metrics.Meter
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.sdk.metrics.data.MetricPoints
import org.typelevel.otel4s.sdk.metrics.data.PointData
import org.typelevel.otel4s.sdk.testkit.metrics.MetricsTestkit
Expand All @@ -33,9 +33,8 @@ class OtelMetricsTests extends CatsEffectSuite {
.inMemory[IO]()
.use { testkit =>
for {
meterIO <- testkit.meterProvider.get("meter")
metricsOps <- {
implicit val meter: Meter[IO] = meterIO
implicit val MP: MeterProvider[IO] = testkit.meterProvider
OtelMetrics.serverMetricsOps[IO]()
}
_ <- {
Expand Down
1 change: 1 addition & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.17.5")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@ import cats.effect.Outcome
import cats.effect.Resource
import cats.syntax.applicative._
import cats.syntax.flatMap._
import cats.syntax.functor._
import fs2.Stream
import org.http4s.client.Client
import org.typelevel.otel4s.trace.SpanKind
import org.typelevel.otel4s.trace.StatusCode
import org.typelevel.otel4s.trace.Tracer
import org.typelevel.otel4s.trace.TracerProvider

/** Middleware builder for wrapping an http4s `Client` to add tracing.
*
* @see [[https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client]]
*/
class ClientMiddlewareBuilder[F[_]: Tracer: Concurrent] private (
class ClientMiddlewareBuilder[F[_]: TracerProvider: Concurrent] private (
urlRedactor: UriRedactor,
spanDataProvider: SpanDataProvider,
urlTemplateClassifier: UriTemplateClassifier,
Expand Down Expand Up @@ -81,79 +82,87 @@ class ClientMiddlewareBuilder[F[_]: Tracer: Concurrent] private (
copy(shouldTrace = shouldTrace)

/** @return the configured middleware */
def build: Client[F] => Client[F] = (client: Client[F]) =>
Client[F] { (req: Request[F]) => // Resource[F, Response[F]]
if (
!shouldTrace(req.requestPrelude).shouldTrace ||
!Tracer[F].meta.isEnabled
) {
client.run(req)
} else {
val reqNoBody = req.withBodyStream(Stream.empty)
val shared =
spanDataProvider.processSharedData(
reqNoBody,
urlTemplateClassifier,
urlRedactor,
)
val spanName =
spanDataProvider.spanName(
reqNoBody,
urlTemplateClassifier,
urlRedactor,
shared,
)
val reqAttributes =
spanDataProvider.requestAttributes(
reqNoBody,
urlTemplateClassifier,
urlRedactor,
shared,
headersAllowedAsAttributes.request,
)
def build: F[Client[F] => Client[F]] =
for {
tracer <- TracerProvider[F]
.tracer("org.http4s.otel4s.middleware.client")
.withVersion(org.http4s.otel4s.middleware.BuildInfo.version)
.get
} yield (client: Client[F]) =>
Client[F] { (req: Request[F]) => // Resource[F, Response[F]]
if (
!shouldTrace(req.requestPrelude).shouldTrace ||
!tracer.meta.isEnabled
) {
client.run(req)
} else {
val reqNoBody = req.withBodyStream(Stream.empty)
val shared =
spanDataProvider.processSharedData(
reqNoBody,
urlTemplateClassifier,
urlRedactor,
)
val spanName =
spanDataProvider.spanName(
reqNoBody,
urlTemplateClassifier,
urlRedactor,
shared,
)
val reqAttributes =
spanDataProvider.requestAttributes(
reqNoBody,
urlTemplateClassifier,
urlRedactor,
shared,
headersAllowedAsAttributes.request,
)

MonadCancelThrow[Resource[F, *]].uncancelable { poll =>
for {
res <- Tracer[F]
.spanBuilder(spanName)
.withSpanKind(SpanKind.Client)
.addAttributes(reqAttributes)
.build
.resource
span = res.span
trace = res.trace
traceHeaders <- Resource.eval(Tracer[F].propagate(Headers.empty)).mapK(trace)
newReq = req.withHeaders(traceHeaders ++ req.headers)
MonadCancelThrow[Resource[F, *]].uncancelable { poll =>
for {
res <- tracer
.spanBuilder(spanName)
.withSpanKind(SpanKind.Client)
.addAttributes(reqAttributes)
.build
.resource
span = res.span
trace = res.trace
traceHeaders <- Resource.eval(tracer.propagate(Headers.empty)).mapK(trace)
newReq = req.withHeaders(traceHeaders ++ req.headers)

resp <- poll(client.run(newReq).mapK(trace)).guaranteeCase {
case Outcome.Succeeded(fa) =>
fa.evalMap { resp =>
val respAttributes =
spanDataProvider.responseAttributes(
resp.withBodyStream(Stream.empty),
headersAllowedAsAttributes.response,
)
span.addAttributes(respAttributes) >> span
.setStatus(StatusCode.Error)
.unlessA(resp.status.isSuccess)
}
resp <- poll(client.run(newReq).mapK(trace)).guaranteeCase {
case Outcome.Succeeded(fa) =>
fa.evalMap { resp =>
val respAttributes =
spanDataProvider.responseAttributes(
resp.withBodyStream(Stream.empty),
headersAllowedAsAttributes.response,
)
span.addAttributes(respAttributes) >> span
.setStatus(StatusCode.Error)
.unlessA(resp.status.isSuccess)
}

case Outcome.Errored(e) =>
Resource.eval(span.addAttributes(TypedAttributes.errorType(e)))
case Outcome.Errored(e) =>
Resource.eval(span.addAttributes(TypedAttributes.errorType(e)))

case Outcome.Canceled() =>
Resource.unit
}
} yield resp
case Outcome.Canceled() =>
Resource.unit
}
} yield resp
}
}
}
}
}

object ClientMiddlewareBuilder {

/** @return a client middleware builder with default configuration */
def default[F[_]: Tracer: Concurrent](urlRedactor: UriRedactor): ClientMiddlewareBuilder[F] =
def default[F[_]: TracerProvider: Concurrent](
urlRedactor: UriRedactor
): ClientMiddlewareBuilder[F] =
new ClientMiddlewareBuilder[F](
urlRedactor,
Defaults.spanDataProvider,
Expand Down
Loading

0 comments on commit 3b029ea

Please sign in to comment.