-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
7 changed files
with
368 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
src/main/scala/com/evolutiongaming/prometheus/ClockPlatform.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 9 additions & 2 deletions
11
src/main/scala/com/evolutiongaming/prometheus/HasObserve.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,16 @@ | ||
package com.evolutiongaming.prometheus | ||
|
||
/** A type-class abstracting over prometheus client histogram-like classes providing "observe a Double value" method. | ||
* | ||
* Check [[PrometheusHelper]] for available implicit instances. | ||
*/ | ||
trait HasObserve[F] { | ||
def observe(observer: F, duration: Double): Unit | ||
|
||
/** Observe a new sample value on a histogram-like metric type | ||
*/ | ||
def observe(observer: F, value: Double): Unit | ||
} | ||
|
||
object HasObserve { | ||
def apply[F](implicit hasObserve: HasObserve[F]) = hasObserve | ||
def apply[F](implicit hasObserve: HasObserve[F]): HasObserve[F] = hasObserve | ||
} |
201 changes: 160 additions & 41 deletions
201
src/main/scala/com/evolutiongaming/prometheus/ObserveDuration.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,184 @@ | ||
package com.evolutiongaming.prometheus | ||
|
||
import io.prometheus.client.Collector | ||
import io.prometheus.client.{Collector, SimpleTimer} | ||
|
||
import scala.annotation.{nowarn, unused} | ||
import scala.concurrent.{ExecutionContext, Future} | ||
|
||
trait ObserveDuration[F] { | ||
|
||
import scala.math.Numeric.Implicits.* | ||
import scala.util.Try | ||
|
||
/** Time duration measurement syntax for [[HasObserve]]-kind metrics from the prometheus client, i.e., Summary, Histogram. | ||
* | ||
* Here time is always reported in seconds, which means your prometheus metric name should end in `_seconds`. | ||
* | ||
* The class is not supposed to be used directly but as an implicit syntax provided by [[PrometheusHelper]]. | ||
*/ | ||
sealed trait ObserveDuration[F] { | ||
|
||
/** Measures evaluation time of a block in seconds with nano-time precision | ||
*/ | ||
def timeFunc[T](f: => T): T | ||
def timeFuncNanos[T](f: => T): T | ||
|
||
/** Measures evaluation time of a block in seconds with nano-time precision | ||
* | ||
* @deprecated | ||
* since 1.1.0 timeFunc has been changed to use nano-time precision, this method is obsolete and will be removed | ||
*/ | ||
@deprecated( | ||
message = "use timeFunc instead - it has nano-time precision now", | ||
since = "1.1.0" | ||
) | ||
def timeFuncNanos[T](f: => T): T = timeFunc(f) | ||
|
||
/** Measures evaluation time of an asynchronous block in seconds with nano-time precision | ||
*/ | ||
def timeFuture[T](f: => Future[T]): Future[T] | ||
def timeFutureNanos[T](f: => Future[T]): Future[T] | ||
|
||
/** Measures evaluation time of an asynchronous block in seconds with nano-time precision | ||
* | ||
* @deprecated | ||
* since 1.1.0 timeFuture has been changed to use nano-time precision, this method is obsolete and will be removed | ||
*/ | ||
@deprecated( | ||
message = "use timeFuture instead - it has nano-time precision now", | ||
since = "1.1.0" | ||
) | ||
def timeFutureNanos[T](f: => Future[T]): Future[T] = timeFuture(f) | ||
|
||
/** Measures in seconds the time spent since the provided start time obtained using [[ClockPlatform.nowMillis]] | ||
* | ||
* @param start | ||
* start time from a millisecond-precision clock | ||
* @deprecated | ||
* since 1.1.0, use timeTillNowMillis(: Long) with a primitive arg type and explicit precision name suffix | ||
*/ | ||
@deprecated( | ||
message = "use timeTillNowMillis(: Long) with a primitive arg type and explicit precision name suffix", | ||
since = "1.1.0" | ||
) | ||
def timeTillNow[T](start: T)(implicit numeric: Numeric[T]): Unit | ||
|
||
/** Measures in seconds the time spent since the provided start time obtained using [[ClockPlatform.nowNano]] | ||
* | ||
* @param start | ||
* start time from a nanosecond-precision clock | ||
* @deprecated | ||
* since 1.1.0, use timeTillNowNanos(: Long) with a primitive arg type | ||
*/ | ||
@deprecated( | ||
message = "use timeTillNowNanos(: Long) with a primitive arg type", | ||
since = "1.1.0" | ||
) | ||
def timeTillNowNanos[T](start: T)(implicit numeric: Numeric[T]): Unit | ||
} | ||
|
||
object ObserveDuration { | ||
def apply[F](implicit observeDuration: ObserveDuration[F]): ObserveDuration[F] = observeDuration | ||
/** Measures in seconds the time spent since the provided start time obtained using [[ClockPlatform.nowMillis]] | ||
*/ | ||
def timeTillNowMillis(startMs: Long): Unit = { | ||
// default impl for a new method of a trait - added for keeping binary compatibility | ||
// TODO: bincompat leftover, remove in 2.x | ||
|
||
def fromHasObserver[F](observer: F)(implicit hasObserve: HasObserve[F], clock: ClockPlatform, ec: ExecutionContext): ObserveDuration[F] = | ||
new ObserveDuration[F] { | ||
timeTillNow[Long](startMs): @nowarn("cat=deprecation") | ||
} | ||
|
||
override def timeFunc[T](f: => T): T = | ||
measureFunction(f, clock.nowMillis, timeTillNow[Long]) | ||
/** Measures in seconds the time spent since the provided start time obtained using [[ClockPlatform.nowNano]] | ||
*/ | ||
def timeTillNowNanos(startNs: Long): Unit = { | ||
// default impl for a new method of a trait - added for keeping binary compatibility | ||
// TODO: bincompat leftover, remove in 2.x | ||
|
||
override def timeFuncNanos[T](f: => T): T = | ||
measureFunction(f, clock.nowNano, timeTillNowNanos[Long]) | ||
timeTillNowNanos[Long](startNs): @nowarn("cat=deprecation") | ||
} | ||
} | ||
|
||
private def measureFunction[A](f: => A, start: Long, measurer: Long => Unit): A = | ||
try f | ||
finally measurer(start) | ||
object ObserveDuration { | ||
def apply[F](implicit observeDuration: ObserveDuration[F]): ObserveDuration[F] = observeDuration | ||
|
||
override def timeFuture[T](f: => Future[T]): Future[T] = | ||
measureFuture(f, clock.nowMillis, timeTillNow[Long]) | ||
// TODO: bincompat leftover, remove in 2.x | ||
@deprecated( | ||
message = "use create", | ||
since = "1.1.0" | ||
) | ||
def fromHasObserver[F]( | ||
observer: F | ||
)(implicit | ||
hasObserve: HasObserve[F], | ||
clock: ClockPlatform, | ||
@unused ec: ExecutionContext | ||
): ObserveDuration[F] = new ObserveDurationImpl[F](observer) | ||
|
||
/** Creates [[ObserveDuration]] implementation instance | ||
* | ||
* @param observer | ||
* metric instance which has [[HasObserve]] | ||
* @param hasObserve | ||
* [[HasObserve]] instance for the metric | ||
* @param clock | ||
* [[ClockPlatform]] to use for time measurement | ||
* @tparam F | ||
* metric type | ||
*/ | ||
def create[F]( | ||
observer: F | ||
)(implicit | ||
hasObserve: HasObserve[F], | ||
clock: ClockPlatform | ||
): ObserveDuration[F] = new ObserveDurationImpl[F](observer) | ||
|
||
private final class ObserveDurationImpl[F]( | ||
observer: F | ||
)(implicit | ||
hasObserve: HasObserve[F], | ||
clock: ClockPlatform | ||
) extends ObserveDuration[F] { | ||
|
||
override def timeFunc[T](f: => T): T = { | ||
val startNs = clock.nowNano | ||
try { | ||
f | ||
} finally { | ||
timeTillNowNanos(startNs) | ||
} | ||
} | ||
|
||
override def timeFutureNanos[T](f: => Future[T]): Future[T] = | ||
measureFuture(f, clock.nowNano, timeTillNowNanos[Long]) | ||
override def timeFuture[T](f: => Future[T]): Future[T] = { | ||
val startNs = clock.nowNano | ||
Future | ||
.fromTry(Try { | ||
f | ||
}) | ||
.flatten | ||
.andThen { case _ => | ||
timeTillNowNanos(startNs) | ||
}(ExecutionContext.parasitic) | ||
} | ||
|
||
private def measureFuture[A]( | ||
f: => Future[A], | ||
start: Long, | ||
measurer: Long => Unit | ||
)(implicit ec: ExecutionContext): Future[A] = f andThen { case _ => measurer(start) } | ||
override def timeTillNow[T]( | ||
start: T | ||
)(implicit numeric: Numeric[T]): Unit = { | ||
val value = duration(start, clock.nowMillis) / Collector.MILLISECONDS_PER_SECOND | ||
hasObserve.observe(observer, value) | ||
} | ||
|
||
override def timeTillNow[T]( | ||
start: T | ||
)(implicit numeric: Numeric[T]): Unit = { | ||
val value = duration(start, clock.nowMillis) / Collector.MILLISECONDS_PER_SECOND | ||
hasObserve.observe(observer, value) | ||
} | ||
override def timeTillNowNanos[T](start: T)(implicit | ||
numeric: Numeric[T] | ||
): Unit = { | ||
val value = duration(start, clock.nowNano) / Collector.NANOSECONDS_PER_SECOND | ||
hasObserve.observe(observer, value) | ||
} | ||
|
||
override def timeTillNowNanos[T](start: T)(implicit | ||
numeric: Numeric[T] | ||
): Unit = { | ||
val value = duration(start, clock.nowNano) / Collector.NANOSECONDS_PER_SECOND | ||
hasObserve.observe(observer, value) | ||
} | ||
override def timeTillNowMillis(startMs: Long): Unit = { | ||
val endMs = clock.nowMillis | ||
val elapsedSeconds = (endMs - startMs).toDouble / Collector.MILLISECONDS_PER_SECOND | ||
hasObserve.observe(observer, elapsedSeconds) | ||
} | ||
|
||
private def duration[A](start: A, now: Long)(implicit a: Numeric[A]): Double = { | ||
now.toDouble - a.toDouble(start) | ||
override def timeTillNowNanos(startNs: Long): Unit = { | ||
val endNs = clock.nowNano | ||
hasObserve.observe(observer, SimpleTimer.elapsedSecondsFromNanos(startNs, endNs)) | ||
} | ||
} | ||
|
||
private def duration[A: Numeric](start: A, now: Long): Double = { | ||
now.toDouble - start.toDouble | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.