Skip to content

Commit 8443468

Browse files
authored
fix: Wrong use of Micrometer metrics (#244)
* fix: Wrong use of Micrometer metrics feat: New sst-cats-effect subproject with F.time method * refactor: Put FOps into TimeSyntax and subpackage syntax * refactor: Remove locally as it is not needed * fix: Suppress Scalafix warning * refactor: Provide the time function separate to the syntax
1 parent bfa8819 commit 8443468

File tree

12 files changed

+165
-113
lines changed

12 files changed

+165
-113
lines changed

build.sbt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ lazy val root = project
55
bundleZioHttp4sBlaze,
66
cassandraDatastaxDriver,
77
cassandraDatastaxDriverPureConfig,
8+
catsEffect,
89
doobieHikari,
910
doobieHikariPureConfig,
1011
example,
@@ -98,6 +99,11 @@ lazy val cassandraDatastaxDriverPureConfig = project
9899
libraryDependencies += Dependencies.pureConfig
99100
)
100101

102+
lazy val catsEffect = project
103+
.in(file("cats-effect"))
104+
.settings(BuildSettings.common)
105+
.settings(name := "sst-cats-effect")
106+
101107
lazy val doobieHikari = project
102108
.in(file("doobie-hikari"))
103109
.settings(BuildSettings.common)
@@ -256,7 +262,10 @@ lazy val http4sServerMicrometer = project
256262
.settings(BuildSettings.common)
257263
.settings(
258264
name := "sst-http4s-server-micrometer",
259-
libraryDependencies += Dependencies.micrometerCore
265+
libraryDependencies ++= Seq(
266+
Dependencies.micrometerCore,
267+
Dependencies.jsr305 // required because of Scala compiler
268+
)
260269
)
261270

262271
lazy val jvm = project
@@ -273,7 +282,10 @@ lazy val jvmMicrometer = project
273282
.settings(BuildSettings.common)
274283
.settings(
275284
name := "sst-jvm-micrometer",
276-
libraryDependencies += Dependencies.micrometerCore
285+
libraryDependencies ++= Seq(
286+
Dependencies.micrometerCore,
287+
Dependencies.jsr305 // required because of Scala compiler
288+
)
277289
)
278290

279291
lazy val jvmPureConfig = project
@@ -438,6 +450,6 @@ lazy val sslConfig = project
438450

439451
addCommandAlias(
440452
"check",
441-
"; scalafmtSbtCheck; scalafmtCheckAll; compile:scalafix --check; test:scalafix --check; +test"
453+
"; scalafmtSbtCheck; scalafmtCheckAll; +test"
442454
)
443455
addCommandAlias("fix", "; compile:scalafix; test:scalafix; scalafmtSbt; scalafmtAll")
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.avast.sst.catseffect
2+
3+
import java.util.concurrent.TimeUnit
4+
5+
import cats.effect.syntax.bracket._
6+
import cats.effect.{Bracket, Clock}
7+
import cats.syntax.flatMap._
8+
import cats.syntax.functor._
9+
10+
import scala.concurrent.duration.Duration
11+
12+
object TimeUtils {
13+
14+
/** Measures the time it takes the effect to finish and records it using the provided function. */
15+
def time[F[_], A](f: F[A])(record: Duration => F[Unit])(implicit F: Bracket[F, Throwable], C: Clock[F]): F[A] = {
16+
val unit = TimeUnit.NANOSECONDS
17+
for {
18+
start <- C.monotonic(unit)
19+
result <- f.guarantee {
20+
C.monotonic(unit).map(computeTime(start)).flatMap(record)
21+
}
22+
} yield result
23+
}
24+
25+
private def computeTime(start: Long)(end: Long) = Duration.fromNanos(end - start)
26+
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.avast.sst.catseffect.syntax
2+
3+
import cats.effect.{Bracket, Clock}
4+
import com.avast.sst.catseffect.TimeUtils
5+
import com.avast.sst.catseffect.syntax.TimeSyntax.FOps
6+
7+
import scala.concurrent.duration.Duration
8+
9+
trait TimeSyntax {
10+
11+
@SuppressWarnings(Array("scalafix:DisableSyntax.implicitConversion"))
12+
implicit def sstFOps[F[_], A](f: F[A]): FOps[F, A] = new FOps(f)
13+
14+
}
15+
16+
object TimeSyntax {
17+
18+
final class FOps[F[_], A](private val f: F[A]) extends AnyVal {
19+
20+
/** Measures the time it takes the effect to finish and records it using the provided function. */
21+
def time(record: Duration => F[Unit])(implicit F: Bracket[F, Throwable], C: Clock[F]): F[A] = TimeUtils.time(f)(record)
22+
23+
}
24+
25+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.avast.sst.catseffect
2+
3+
package object syntax {
4+
5+
object time extends TimeSyntax
6+
7+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.avast.sst.catseffect.syntax
2+
3+
import cats.effect.concurrent.Ref
4+
import cats.effect.{Clock, IO, Timer}
5+
import com.avast.sst.catseffect.syntax.time._
6+
import org.scalatest.funsuite.AsyncFunSuite
7+
8+
import scala.concurrent.ExecutionContext
9+
import scala.concurrent.duration.{Duration, TimeUnit}
10+
11+
class FOpsTest extends AsyncFunSuite {
12+
13+
implicit private val timer: Timer[IO] = IO.timer(ExecutionContext.global)
14+
15+
test("time") {
16+
val sleepTime = Duration.fromNanos(500000000)
17+
implicit val mockClock: Clock[IO] = new Clock[IO] {
18+
var values = List(0L, sleepTime.toNanos)
19+
override def monotonic(unit: TimeUnit): IO[Long] = {
20+
val time = values.head
21+
values = values.tail
22+
IO.pure(time)
23+
}
24+
override def realTime(unit: TimeUnit): IO[Long] = ???
25+
}
26+
val io = for {
27+
ref <- Ref.of[IO, Option[Duration]](None)
28+
_ <- IO.sleep(sleepTime).time(d => ref.set(Some(d)))
29+
result <- ref.get
30+
} yield assert(result.isDefined && result.get.toMillis === sleepTime.toMillis)
31+
32+
io.unsafeToFuture()
33+
}
34+
35+
}

http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/HttpStatusMetrics.scala

Lines changed: 0 additions & 25 deletions
This file was deleted.

http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModule.scala

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package com.avast.sst.http4s.server.micrometer
22

33
import java.util.concurrent.TimeUnit
44

5-
import cats.effect.Sync
6-
import cats.syntax.flatMap._
5+
import cats.effect.Effect
6+
import cats.effect.concurrent.Ref
77
import cats.syntax.functor._
88
import io.micrometer.core.instrument.MeterRegistry
99
import org.http4s.metrics.{MetricsOps, TerminationType}
@@ -12,38 +12,42 @@ import org.http4s.{Method, Status}
1212
object MicrometerHttp4sMetricsOpsModule {
1313

1414
/** Makes [[org.http4s.metrics.MetricsOps]] to record the usual HTTP server metrics. */
15-
def make[F[_]: Sync](meterRegistry: MeterRegistry): F[MetricsOps[F]] = {
16-
val F = Sync[F]
15+
def make[F[_]: Effect](meterRegistry: MeterRegistry): F[MetricsOps[F]] = {
16+
val F = Effect[F]
1717

18-
F.delay {
19-
new MetricsOps[F] {
20-
private val prefix = "http.global"
21-
private val activeRequests = meterRegistry.counter(s"$prefix.active-requests")
22-
private val headersTime = meterRegistry.timer(s"$prefix.headers-time")
23-
private val totalTime = meterRegistry.timer(s"$prefix.total-time")
24-
private val failureTime = meterRegistry.timer(s"$prefix.failure-time")
25-
private val httpStatusCodes = new HttpStatusMetrics(prefix, meterRegistry)
18+
for {
19+
activeRequests <- Ref.of[F, Long](0L)
20+
} yield new MetricsOps[F] {
2621

27-
override def increaseActiveRequests(classifier: Option[String]): F[Unit] = F.delay(activeRequests.increment())
22+
private val prefix = "http.global"
23+
private val failureTime = meterRegistry.timer(s"$prefix.failure-time")
2824

29-
override def decreaseActiveRequests(classifier: Option[String]): F[Unit] = F.delay(activeRequests.increment(-1))
25+
meterRegistry.gauge(
26+
s"$prefix.active-requests",
27+
activeRequests,
28+
(_: Ref[F, Long]) => Effect[F].toIO(activeRequests.get).unsafeRunSync().toDouble
29+
)
3030

31-
override def recordHeadersTime(method: Method, elapsed: Long, classifier: Option[String]): F[Unit] = {
32-
F.delay(headersTime.record(elapsed, TimeUnit.NANOSECONDS))
33-
}
31+
override def increaseActiveRequests(classifier: Option[String]): F[Unit] = activeRequests.update(_ + 1)
3432

35-
override def recordTotalTime(method: Method, status: Status, elapsed: Long, classifier: Option[String]): F[Unit] = {
36-
for {
37-
_ <- F.delay(totalTime.record(elapsed, TimeUnit.NANOSECONDS))
38-
_ <- F.delay(httpStatusCodes.recordHttpStatus(status))
39-
} yield ()
40-
}
33+
override def decreaseActiveRequests(classifier: Option[String]): F[Unit] = activeRequests.update(_ - 1)
4134

42-
override def recordAbnormalTermination(elapsed: Long, terminationType: TerminationType, classifier: Option[String]): F[Unit] = {
43-
F.delay(failureTime.record(elapsed, TimeUnit.NANOSECONDS))
44-
}
35+
override def recordHeadersTime(method: Method, elapsed: Long, classifier: Option[String]): F[Unit] = {
36+
F.delay(meterRegistry.timer(s"$prefix.headers-time", "method", method.name).record(elapsed, TimeUnit.NANOSECONDS))
37+
}
38+
39+
override def recordTotalTime(method: Method, status: Status, elapsed: Long, classifier: Option[String]): F[Unit] = {
40+
F.delay(
41+
meterRegistry
42+
.timer(s"$prefix.total-time", "status", s"${status.code}", "status-class", s"${status.code / 100}xx")
43+
.record(elapsed, TimeUnit.NANOSECONDS)
44+
)
45+
}
4546

47+
override def recordAbnormalTermination(elapsed: Long, terminationType: TerminationType, classifier: Option[String]): F[Unit] = {
48+
F.delay(failureTime.record(elapsed, TimeUnit.NANOSECONDS))
4649
}
50+
4751
}
4852
}
4953

http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sServerMetricsModule.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ object MicrometerHttp4sServerMetricsModule {
1818

1919
for {
2020
metricsOps <- MicrometerHttp4sMetricsOpsModule.make[F](meterRegistry)
21-
routeMetrics <- Sync[F].delay(new RouteMetrics[F](meterRegistry, clock))
21+
routeMetrics <- Sync[F].delay(new RouteMetrics[F](meterRegistry))
2222
} yield new MicrometerHttp4sServerMetricsModule[F](Metrics(metricsOps), routeMetrics)
2323
}
2424

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,34 @@
11
package com.avast.sst.http4s.server.micrometer
22

3-
import java.util.concurrent.TimeUnit
4-
3+
import cats.effect.Sync
54
import cats.effect.syntax.bracket._
6-
import cats.effect.{Clock, Sync}
75
import cats.syntax.flatMap._
86
import cats.syntax.functor._
9-
import io.micrometer.core.instrument.MeterRegistry
7+
import io.micrometer.core.instrument.{MeterRegistry, Timer}
108
import org.http4s.Response
119

12-
/** Provides the usual metrics for single HTTP route. */
13-
class RouteMetrics[F[_]: Sync](meterRegistry: MeterRegistry, clock: Clock[F]) {
10+
/** Provides the usual metrics for a single HTTP route. */
11+
class RouteMetrics[F[_]: Sync](meterRegistry: MeterRegistry) {
1412

1513
private val F = Sync[F]
1614

17-
/** Wraps a single route with the usual metrics (requests in-flight, timer, HTTP status counts).
15+
/** Wraps a single route with the usual metrics (count, times, HTTP status codes).
1816
*
1917
* @param name will be used in metric name
2018
*/
2119
def wrap(name: String)(route: => F[Response[F]]): F[Response[F]] = {
22-
val prefix = s"http.$name"
23-
val activeRequests = meterRegistry.counter(s"$prefix.active-requests")
24-
val timer = meterRegistry.timer(s"$prefix.total-time")
25-
val httpStatusCodes = new HttpStatusMetrics(prefix, meterRegistry)
2620
for {
27-
start <- clock.monotonic(TimeUnit.NANOSECONDS)
28-
response <-
29-
F.delay(activeRequests.increment())
30-
.bracket { _ => route.flatTap(response => F.delay(httpStatusCodes.recordHttpStatus(response.status))) } { _ =>
31-
for {
32-
time <- computeTime(start)
33-
_ <- F.delay(activeRequests.increment(-1))
34-
_ <- F.delay(timer.record(time, TimeUnit.NANOSECONDS))
35-
} yield ()
36-
}
21+
start <- F.delay(Timer.start(meterRegistry))
22+
response <- route.bracket(F.pure) { response =>
23+
F.delay(
24+
start.stop(
25+
meterRegistry
26+
.timer(s"http.$name", "status", s"${response.status.code}", "status-class", s"${response.status.code / 100}xx")
27+
)
28+
)
29+
.as(())
30+
}
3731
} yield response
3832
}
3933

40-
private def computeTime(start: Long): F[Long] = clock.monotonic(TimeUnit.NANOSECONDS).map(_ - start)
41-
4234
}

http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/HttpStatusMetricsTest.scala

Lines changed: 0 additions & 25 deletions
This file was deleted.

http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModuleTest.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.avast.sst.http4s.server.micrometer
22

33
import java.util.concurrent.TimeUnit
44

5-
import cats.effect.SyncIO
5+
import cats.effect.IO
66
import io.micrometer.core.instrument.simple.SimpleMeterRegistry
77
import org.http4s.{Method, Status}
88
import org.scalatest.funsuite.AnyFunSuite
@@ -11,15 +11,16 @@ class MicrometerHttp4sMetricsOpsModuleTest extends AnyFunSuite {
1111

1212
test("http4s MetricsOps for Micrometer") {
1313
val registry = new SimpleMeterRegistry()
14-
val metricsOps = MicrometerHttp4sMetricsOpsModule.make[SyncIO](registry).unsafeRunSync()
14+
val metricsOps = MicrometerHttp4sMetricsOpsModule.make[IO](registry).unsafeRunSync()
1515

1616
metricsOps.increaseActiveRequests(None).unsafeRunSync()
1717
metricsOps.recordTotalTime(Method.GET, Status.Ok, 2500, None).unsafeRunSync()
1818

19-
assert(registry.get("http.global.active-requests").counter().count() === 1)
19+
assert(registry.get("http.global.active-requests").gauge().value() === 1)
2020
assert(registry.get("http.global.total-time").timer().count() === 1)
2121
assert(registry.get("http.global.total-time").timer().totalTime(TimeUnit.NANOSECONDS) > 2499)
22-
assert(registry.get("http.global.status.200").counter().count() === 1)
22+
assert(registry.get("http.global.total-time").tags("status", "200").timer().count() === 1)
23+
assert(registry.get("http.global.total-time").tags("status-class", "2xx").timer().count() === 1)
2324
}
2425

2526
}

0 commit comments

Comments
 (0)