Skip to content

Commit 4bc14e8

Browse files
committed
Migrate to nanoTime for time* methods, improve API DX
timeFunc and timeFuture used millisecond precision, which lead to issues like Summary quantiles being skewed towards 0 if measured latencies are close to 1 ms. To fix that: - time* methods use System.nanoTime() now - deprecate specific time*Nano methods, millisecond precision shouldn't be a choice to avoid mistakes - improve method naming, document all time-related public functionality - deprecate unused TemporalOps implicit conversion - overall developer experience improvements This change breaks source compatibility for some usages, but binary compatibility is preserved.
1 parent b86b650 commit 4bc14e8

File tree

7 files changed

+344
-76
lines changed

7 files changed

+344
-76
lines changed

build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Compile / doc / scalacOptions ++= Seq("-groups", "-implicits", "-no-link-warning
2828
publishTo := Some(Resolver.evolutionReleases)
2929

3030
libraryDependencies ++= Seq(
31+
// executor-tools dependency is not used anymore, left as is so MiMa bincompat report doesn't complain
32+
// TODO: remove in 2.x
3133
"com.evolutiongaming" %% "executor-tools" % "1.0.4",
3234
"io.prometheus" % "simpleclient_common" % "0.8.1",
3335
"org.scalameta" %% "munit" % "1.0.0" % Test

src/main/scala/com/evolutiongaming/prometheus/ClockPlatform.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
package com.evolutiongaming.prometheus
22

3+
/** Exposes platform time measuring capabilities needed for metrics
4+
*/
35
trait ClockPlatform {
6+
7+
/** @see
8+
* [[System#currentTimeMillis()]]
9+
*/
410
def nowMillis: Long
11+
12+
/** @see
13+
* [[System#nanoTime()]]
14+
*/
515
def nowNano: Long
616
}
717

818
object ClockPlatform {
919

20+
/** Global singleton instance for default [[ClockPlatform]] for JVM
21+
*/
1022
val default: ClockPlatform = new Default
1123

24+
/** Default [[ClockPlatform]] impl for JVM - use [[ClockPlatform.default]] global singleton instance!
25+
*/
1226
class Default extends ClockPlatform {
1327
override def nowMillis: Long = System.currentTimeMillis()
1428
override def nowNano: Long = System.nanoTime()
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package com.evolutiongaming.prometheus
22

3+
/** A type-class abstracting over prometheus client histogram-like classes providing "observe a Double value" method.
4+
*
5+
* Check [[PrometheusHelper]] for available implicit instances.
6+
*/
37
trait HasObserve[F] {
4-
def observe(observer: F, duration: Double): Unit
8+
9+
/** Observe a new sample value on a histogram-like metric type
10+
*/
11+
def observe(observer: F, value: Double): Unit
512
}
613

714
object HasObserve {
8-
def apply[F](implicit hasObserve: HasObserve[F]) = hasObserve
15+
def apply[F](implicit hasObserve: HasObserve[F]): HasObserve[F] = hasObserve
916
}
Lines changed: 136 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,160 @@
11
package com.evolutiongaming.prometheus
22

3-
import io.prometheus.client.Collector
3+
import io.prometheus.client.{Collector, SimpleTimer}
44

5+
import scala.annotation.{nowarn, unused}
56
import scala.concurrent.{ExecutionContext, Future}
6-
7-
trait ObserveDuration[F] {
8-
7+
import scala.math.Numeric.Implicits.*
8+
import scala.util.Try
9+
10+
/** Time duration measurement syntax for [[HasObserve]]-kind metrics from the prometheus client, i.e., Summary, Histogram.
11+
*
12+
* Here time is always reported in seconds, which means your prometheus metric name should end in `_seconds`.
13+
*
14+
* The class is not supposed to be used directly but as an implicit syntax provided by [[PrometheusHelper]].
15+
*/
16+
sealed trait ObserveDuration[F] {
17+
18+
/** Measures evaluation time of a block in seconds with nano-time precision
19+
*/
920
def timeFunc[T](f: => T): T
10-
def timeFuncNanos[T](f: => T): T
1121

22+
@deprecated(
23+
message = "use timeFunc instead - it has nano-time precision now",
24+
since = "1.1.0"
25+
)
26+
def timeFuncNanos[T](f: => T): T = timeFunc(f)
27+
28+
/** Measures evaluation time of an asynchronous block in seconds with nano-time precision
29+
*/
1230
def timeFuture[T](f: => Future[T]): Future[T]
13-
def timeFutureNanos[T](f: => Future[T]): Future[T]
1431

32+
@deprecated(
33+
message = "use timeFuture instead - it has nano-time precision now",
34+
since = "1.1.0"
35+
)
36+
def timeFutureNanos[T](f: => Future[T]): Future[T] = timeFuture(f)
37+
38+
@deprecated(
39+
message = "use timeTillNowMillis(: Long) with a primitive arg type and explicit precision name suffix",
40+
since = "1.1.0"
41+
)
1542
def timeTillNow[T](start: T)(implicit numeric: Numeric[T]): Unit
43+
44+
@deprecated(
45+
message = "use timeTillNowNanos(: Long) with a primitive arg type",
46+
since = "1.1.0"
47+
)
1648
def timeTillNowNanos[T](start: T)(implicit numeric: Numeric[T]): Unit
17-
}
1849

19-
object ObserveDuration {
20-
def apply[F](implicit observeDuration: ObserveDuration[F]): ObserveDuration[F] = observeDuration
50+
/** Measures in seconds the time spent since the provided start time obtained using [[ClockPlatform.nowMillis]]
51+
*/
52+
def timeTillNowMillis(@unused startMs: Long): Unit = {
53+
// default impl for a new method of a trait - added for keeping binary compatibility
54+
// TODO: bincompat leftover, remove in 2.x
2155

22-
def fromHasObserver[F](observer: F)(implicit hasObserve: HasObserve[F], clock: ClockPlatform, ec: ExecutionContext): ObserveDuration[F] =
23-
new ObserveDuration[F] {
56+
timeTillNow[Long](startMs): @nowarn
57+
}
2458

25-
override def timeFunc[T](f: => T): T =
26-
measureFunction(f, clock.nowMillis, timeTillNow[Long])
59+
/** Measures in seconds the time spent since the provided start time obtained using [[ClockPlatform.nowNano]]
60+
*/
61+
def timeTillNowNanos(@unused startNs: Long): Unit = {
62+
// default impl for a new method of a trait - added for keeping binary compatibility
63+
// TODO: bincompat leftover, remove in 2.x
2764

28-
override def timeFuncNanos[T](f: => T): T =
29-
measureFunction(f, clock.nowNano, timeTillNowNanos[Long])
65+
timeTillNowNanos[Long](startNs): @nowarn
66+
}
67+
}
3068

31-
private def measureFunction[A](f: => A, start: Long, measurer: Long => Unit): A =
32-
try f
33-
finally measurer(start)
69+
object ObserveDuration {
70+
def apply[F](implicit observeDuration: ObserveDuration[F]): ObserveDuration[F] = observeDuration
3471

35-
override def timeFuture[T](f: => Future[T]): Future[T] =
36-
measureFuture(f, clock.nowMillis, timeTillNow[Long])
72+
// TODO: bincompat leftover, remove in 2.x
73+
@deprecated(
74+
message = "use create",
75+
since = "1.1.0"
76+
)
77+
def fromHasObserver[F](
78+
observer: F
79+
)(implicit
80+
hasObserve: HasObserve[F],
81+
clock: ClockPlatform,
82+
@unused ec: ExecutionContext
83+
): ObserveDuration[F] = new ObserveDurationImpl[F](observer)
84+
85+
/** Creates [[ObserveDuration]] implementation instance
86+
*
87+
* @param observer
88+
* metric instance which has [[HasObserve]]
89+
* @param hasObserve
90+
* [[HasObserve]] instance for the metric
91+
* @param clock
92+
* [[ClockPlatform]] to use for time measurement
93+
* @tparam F
94+
* metric type
95+
*/
96+
def create[F](
97+
observer: F
98+
)(implicit
99+
hasObserve: HasObserve[F],
100+
clock: ClockPlatform
101+
): ObserveDuration[F] = new ObserveDurationImpl[F](observer)
102+
103+
private final class ObserveDurationImpl[F](
104+
observer: F
105+
)(implicit
106+
hasObserve: HasObserve[F],
107+
clock: ClockPlatform
108+
) extends ObserveDuration[F] {
109+
110+
override def timeFunc[T](f: => T): T = {
111+
val startNs = clock.nowNano
112+
try {
113+
f
114+
} finally {
115+
timeTillNowNanos(startNs)
116+
}
117+
}
37118

38-
override def timeFutureNanos[T](f: => Future[T]): Future[T] =
39-
measureFuture(f, clock.nowNano, timeTillNowNanos[Long])
119+
override def timeFuture[T](f: => Future[T]): Future[T] = {
120+
val startNs = clock.nowNano
121+
Future
122+
.fromTry(Try {
123+
f
124+
})
125+
.flatten
126+
.andThen { case _ =>
127+
timeTillNowNanos(startNs)
128+
}(ExecutionContext.parasitic)
129+
}
40130

41-
private def measureFuture[A](
42-
f: => Future[A],
43-
start: Long,
44-
measurer: Long => Unit
45-
)(implicit ec: ExecutionContext): Future[A] = f andThen { case _ => measurer(start) }
131+
override def timeTillNow[T](
132+
start: T
133+
)(implicit numeric: Numeric[T]): Unit = {
134+
val value = duration(start, clock.nowMillis) / Collector.MILLISECONDS_PER_SECOND
135+
hasObserve.observe(observer, value)
136+
}
46137

47-
override def timeTillNow[T](
48-
start: T
49-
)(implicit numeric: Numeric[T]): Unit = {
50-
val value = duration(start, clock.nowMillis) / Collector.MILLISECONDS_PER_SECOND
51-
hasObserve.observe(observer, value)
52-
}
138+
override def timeTillNowNanos[T](start: T)(implicit
139+
numeric: Numeric[T]
140+
): Unit = {
141+
val value = duration(start, clock.nowNano) / Collector.NANOSECONDS_PER_SECOND
142+
hasObserve.observe(observer, value)
143+
}
53144

54-
override def timeTillNowNanos[T](start: T)(implicit
55-
numeric: Numeric[T]
56-
): Unit = {
57-
val value = duration(start, clock.nowNano) / Collector.NANOSECONDS_PER_SECOND
58-
hasObserve.observe(observer, value)
59-
}
145+
override def timeTillNowMillis(startMs: Long): Unit = {
146+
val endMs = clock.nowMillis
147+
val elapsedSeconds = (endMs - startMs).toDouble / Collector.MILLISECONDS_PER_SECOND
148+
hasObserve.observe(observer, elapsedSeconds)
60149
}
61150

62-
private def duration[A](start: A, now: Long)(implicit a: Numeric[A]): Double = {
63-
now.toDouble - a.toDouble(start)
151+
override def timeTillNowNanos(startNs: Long): Unit = {
152+
val endNs = clock.nowNano
153+
hasObserve.observe(observer, SimpleTimer.elapsedSecondsFromNanos(startNs, endNs))
154+
}
155+
}
156+
157+
private def duration[A: Numeric](start: A, now: Long): Double = {
158+
now.toDouble - start.toDouble
64159
}
65160
}

src/main/scala/com/evolutiongaming/prometheus/PrometheusHelper.scala

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
package com.evolutiongaming.prometheus
22

3-
import com.evolutiongaming.concurrent.CurrentThreadExecutionContext
43
import io.prometheus.client.{Gauge, Histogram, SimpleCollector, Summary}
54

6-
import scala.concurrent.{ExecutionContext, Future}
7-
5+
import scala.concurrent.Future
6+
import scala.math.Numeric.Implicits.*
7+
8+
/** Main entry point for prometheus-tools goodies, mainly extension method for prometheus client classes.
9+
*
10+
* Usage:
11+
* {{{
12+
* import com.evolutiongaming.prometheus.PrometheusHelper.*
13+
* }}}
14+
*
15+
* @see
16+
* [[ObserveDuration]]
17+
*/
818
object PrometheusHelper {
9-
private implicit val ec: ExecutionContext = CurrentThreadExecutionContext
1019
private implicit val clock: ClockPlatform = ClockPlatform.default
1120

1221
implicit val histogramObs: HasObserve[Histogram] = (histogram: Histogram, duration: Double) => histogram.observe(duration)
@@ -19,15 +28,42 @@ object PrometheusHelper {
1928

2029
implicit class GaugeOps(val gauge: Gauge) extends AnyVal {
2130

22-
def collect[T](f: => T)(implicit numeric: Numeric[T]): Gauge = {
31+
def collect[T: Numeric](f: => T): Gauge = {
2332
val child = new Gauge.Child() {
24-
override def get() = numeric.toDouble(f)
33+
override def get(): Double = f.toDouble
2534
}
2635
gauge.setChild(child)
2736
}
2837
}
2938

30-
implicit class TemporalOps[A: ObserveDuration](val a: A) {
39+
/*
40+
TODO: bincompat leftover, remove in 2.x
41+
42+
Without this magic method MiMa complained:
43+
static method TemporalOps(java.lang.Object,com.evolutiongaming.prometheus.ObserveDuration)com.evolutiongaming.prometheus.PrometheusHelper#TemporalOps
44+
in class com.evolutiongaming.prometheus.PrometheusHelper does not have a correspondent in current version
45+
46+
The visibility also has to be public, otherwise scalac does not reliably generate static method for both 2.13 and 3
47+
*/
48+
@deprecated(
49+
message = "referencing TemporalOps directly is deprecated, use implicit syntax",
50+
since = "1.1.0"
51+
)
52+
def TemporalOps[A](
53+
v1: A,
54+
v2: com.evolutiongaming.prometheus.ObserveDuration[A]
55+
): com.evolutiongaming.prometheus.PrometheusHelper.TemporalOps[A] = new TemporalOps[A](v1)(v2)
56+
57+
/*
58+
TODO: bincompat leftover, remove in 2.x
59+
60+
TemporalOps wasn't needed to provide ObserveDuration syntax, should be removed - see PrometheusHelperSpec
61+
*/
62+
@deprecated(
63+
message = "referencing TemporalOps directly is deprecated, use implicit syntax",
64+
since = "1.1.0"
65+
)
66+
private[prometheus] class TemporalOps[A: ObserveDuration](val a: A) {
3167

3268
def timeFunc[T](f: => T): T = ObserveDuration[A].timeFunc(f)
3369

@@ -46,7 +82,7 @@ object PrometheusHelper {
4682
ObserveDuration[A].timeTillNowNanos(start)
4783
}
4884

49-
implicit class BuilderOps[C <: SimpleCollector[_], B <: SimpleCollector.Builder[
85+
implicit class BuilderOps[C <: SimpleCollector[?], B <: SimpleCollector.Builder[
5086
B,
5187
C
5288
]](val self: B)
@@ -60,7 +96,8 @@ object PrometheusHelper {
6096
}
6197
}
6298

63-
implicit def observeDuration[F](observer: F)(implicit hasObserve: HasObserve[F]): ObserveDuration[F] = ObserveDuration.fromHasObserver(observer)
99+
implicit def observeDuration[F](observer: F)(implicit hasObserve: HasObserve[F]): ObserveDuration[F] =
100+
ObserveDuration.create(observer)
64101

65102
implicit class RichSummaryBuilder(val summaryBuilder: Summary.Builder) extends AnyVal {
66103

0 commit comments

Comments
 (0)