Skip to content

Commit 7dd29e2

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 7dd29e2

File tree

7 files changed

+368
-76
lines changed

7 files changed

+368
-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: 160 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,184 @@
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+
/** Measures evaluation time of a block in seconds with nano-time precision
23+
*
24+
* @deprecated
25+
* since 1.1.0 timeFunc has been changed to use nano-time precision, this method is obsolete and will be removed
26+
*/
27+
@deprecated(
28+
message = "use timeFunc instead - it has nano-time precision now",
29+
since = "1.1.0"
30+
)
31+
def timeFuncNanos[T](f: => T): T = timeFunc(f)
32+
33+
/** Measures evaluation time of an asynchronous block in seconds with nano-time precision
34+
*/
1235
def timeFuture[T](f: => Future[T]): Future[T]
13-
def timeFutureNanos[T](f: => Future[T]): Future[T]
1436

37+
/** Measures evaluation time of an asynchronous block in seconds with nano-time precision
38+
*
39+
* @deprecated
40+
* since 1.1.0 timeFuture has been changed to use nano-time precision, this method is obsolete and will be removed
41+
*/
42+
@deprecated(
43+
message = "use timeFuture instead - it has nano-time precision now",
44+
since = "1.1.0"
45+
)
46+
def timeFutureNanos[T](f: => Future[T]): Future[T] = timeFuture(f)
47+
48+
/** Measures in seconds the time spent since the provided start time obtained using [[ClockPlatform.nowMillis]]
49+
*
50+
* @param start
51+
* start time from a millisecond-precision clock
52+
* @deprecated
53+
* since 1.1.0, use timeTillNowMillis(: Long) with a primitive arg type and explicit precision name suffix
54+
*/
55+
@deprecated(
56+
message = "use timeTillNowMillis(: Long) with a primitive arg type and explicit precision name suffix",
57+
since = "1.1.0"
58+
)
1559
def timeTillNow[T](start: T)(implicit numeric: Numeric[T]): Unit
60+
61+
/** Measures in seconds the time spent since the provided start time obtained using [[ClockPlatform.nowNano]]
62+
*
63+
* @param start
64+
* start time from a nanosecond-precision clock
65+
* @deprecated
66+
* since 1.1.0, use timeTillNowNanos(: Long) with a primitive arg type
67+
*/
68+
@deprecated(
69+
message = "use timeTillNowNanos(: Long) with a primitive arg type",
70+
since = "1.1.0"
71+
)
1672
def timeTillNowNanos[T](start: T)(implicit numeric: Numeric[T]): Unit
17-
}
1873

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

22-
def fromHasObserver[F](observer: F)(implicit hasObserve: HasObserve[F], clock: ClockPlatform, ec: ExecutionContext): ObserveDuration[F] =
23-
new ObserveDuration[F] {
80+
timeTillNow[Long](startMs): @nowarn("cat=deprecation")
81+
}
2482

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

28-
override def timeFuncNanos[T](f: => T): T =
29-
measureFunction(f, clock.nowNano, timeTillNowNanos[Long])
89+
timeTillNowNanos[Long](startNs): @nowarn("cat=deprecation")
90+
}
91+
}
3092

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

35-
override def timeFuture[T](f: => Future[T]): Future[T] =
36-
measureFuture(f, clock.nowMillis, timeTillNow[Long])
96+
// TODO: bincompat leftover, remove in 2.x
97+
@deprecated(
98+
message = "use create",
99+
since = "1.1.0"
100+
)
101+
def fromHasObserver[F](
102+
observer: F
103+
)(implicit
104+
hasObserve: HasObserve[F],
105+
clock: ClockPlatform,
106+
@unused ec: ExecutionContext
107+
): ObserveDuration[F] = new ObserveDurationImpl[F](observer)
108+
109+
/** Creates [[ObserveDuration]] implementation instance
110+
*
111+
* @param observer
112+
* metric instance which has [[HasObserve]]
113+
* @param hasObserve
114+
* [[HasObserve]] instance for the metric
115+
* @param clock
116+
* [[ClockPlatform]] to use for time measurement
117+
* @tparam F
118+
* metric type
119+
*/
120+
def create[F](
121+
observer: F
122+
)(implicit
123+
hasObserve: HasObserve[F],
124+
clock: ClockPlatform
125+
): ObserveDuration[F] = new ObserveDurationImpl[F](observer)
126+
127+
private final class ObserveDurationImpl[F](
128+
observer: F
129+
)(implicit
130+
hasObserve: HasObserve[F],
131+
clock: ClockPlatform
132+
) extends ObserveDuration[F] {
133+
134+
override def timeFunc[T](f: => T): T = {
135+
val startNs = clock.nowNano
136+
try {
137+
f
138+
} finally {
139+
timeTillNowNanos(startNs)
140+
}
141+
}
37142

38-
override def timeFutureNanos[T](f: => Future[T]): Future[T] =
39-
measureFuture(f, clock.nowNano, timeTillNowNanos[Long])
143+
override def timeFuture[T](f: => Future[T]): Future[T] = {
144+
val startNs = clock.nowNano
145+
Future
146+
.fromTry(Try {
147+
f
148+
})
149+
.flatten
150+
.andThen { case _ =>
151+
timeTillNowNanos(startNs)
152+
}(ExecutionContext.parasitic)
153+
}
40154

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) }
155+
override def timeTillNow[T](
156+
start: T
157+
)(implicit numeric: Numeric[T]): Unit = {
158+
val value = duration(start, clock.nowMillis) / Collector.MILLISECONDS_PER_SECOND
159+
hasObserve.observe(observer, value)
160+
}
46161

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-
}
162+
override def timeTillNowNanos[T](start: T)(implicit
163+
numeric: Numeric[T]
164+
): Unit = {
165+
val value = duration(start, clock.nowNano) / Collector.NANOSECONDS_PER_SECOND
166+
hasObserve.observe(observer, value)
167+
}
53168

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-
}
169+
override def timeTillNowMillis(startMs: Long): Unit = {
170+
val endMs = clock.nowMillis
171+
val elapsedSeconds = (endMs - startMs).toDouble / Collector.MILLISECONDS_PER_SECOND
172+
hasObserve.observe(observer, elapsedSeconds)
60173
}
61174

62-
private def duration[A](start: A, now: Long)(implicit a: Numeric[A]): Double = {
63-
now.toDouble - a.toDouble(start)
175+
override def timeTillNowNanos(startNs: Long): Unit = {
176+
val endNs = clock.nowNano
177+
hasObserve.observe(observer, SimpleTimer.elapsedSecondsFromNanos(startNs, endNs))
178+
}
179+
}
180+
181+
private def duration[A: Numeric](start: A, now: Long): Double = {
182+
now.toDouble - start.toDouble
64183
}
65184
}

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)