Skip to content

Commit

Permalink
Add MetricsBehavior flag and allow setting no totalime (#30)
Browse files Browse the repository at this point in the history
Currently the only way to make it so that no `totaltime` timeseries data for a metric is emitted is to set it at the `MetricsFactory` level with `TotaltimeType.NONE`.

However, many observability services (e.g. Lightstep, Datadog, Cloudwatch) charge per dimension and timeseries, so there may be cases where you want to more easily specify no `totaltime` for some metrics but not for others in order to reduce usage.

This adds a new `MetricsBehavior` enum that can be specified per metric.  For now, the only options for this are `DEFAULT` and `NO_TOTALTIME` (which, as the name suggests, means it won't include `totaltime`). In the future we may want to add an option e.g. to not include "prescient"/"environment" dimensions, among other possibilities.
  • Loading branch information
rlinehan authored Oct 27, 2022
1 parent 9b04009 commit 48b268f
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 3 deletions.
6 changes: 6 additions & 0 deletions kotlin/goodmetrics/src/main/kotlin/goodmetrics/Metrics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ package goodmetrics

import goodmetrics.pipeline.bucketBase2

enum class MetricsBehavior {
DEFAULT,
NO_TOTALTIME // don't include a `totaltime` timeseries for the metric
}

/**
* Not thread safe.
*/
data class Metrics internal constructor(
internal val name: String,
internal var timestampNanos: Long,
internal val startNanoTime: Long,
internal val metricsBehavior: MetricsBehavior = MetricsBehavior.DEFAULT,
) {
sealed interface Dimension {
val name: kotlin.String
Expand Down
24 changes: 21 additions & 3 deletions kotlin/goodmetrics/src/main/kotlin/goodmetrics/MetricsFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,22 @@ class MetricsFactory(
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val metrics = internals.getMetrics(name, stampAt)
return recordWithBehavior(name, stampAt, MetricsBehavior.DEFAULT, block)
}

/**
* Passes a Metrics into your scope. Record your unit of work; when the scope exits
* the Metrics will be stamped with `totaltime` and emitted through the pipeline.
*
* Allows for setting specific MetricsBehaviors for the metric.
*
* If you don't want `totaltime` timeseries data, then specify `metricBehavior: MetricBehavior.NO_TOTALTIME`.
*/
inline fun <T> recordWithBehavior(name: String, stampAt: TimestampAt = TimestampAt.Start, metricsBehavior: MetricsBehavior, block: (Metrics) -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val metrics = internals.getMetrics(name, stampAt, metricsBehavior)
try {
return block(metrics)
} finally {
Expand All @@ -80,12 +95,12 @@ class MetricsFactory(
/**
* For every getMetrics(), you need to also emit() that Metrics object via this same MetricsFactory.
*/
fun getMetrics(name: String, stampAt: TimestampAt): Metrics {
fun getMetrics(name: String, stampAt: TimestampAt, metricsBehavior: MetricsBehavior = MetricsBehavior.DEFAULT): Metrics {
val timestamp = when (stampAt) {
TimestampAt.Start -> self.timeSource.epochNanos()
TimestampAt.End -> -1
}
return Metrics(name, timestamp, System.nanoTime())
return Metrics(name, timestamp, System.nanoTime(), metricsBehavior)
}

/**
Expand All @@ -102,6 +117,9 @@ class MetricsFactory(
if (metrics.timestampNanos < 1) {
metrics.timestampNanos = timeSource.epochNanos()
}
if (metrics.metricsBehavior == MetricsBehavior.NO_TOTALTIME) {
return
}
val duration = System.nanoTime() - metrics.startNanoTime
when (totaltimeType) {
TotaltimeType.DistributionMicroseconds -> metrics.distribution("totaltime", duration / 1000)
Expand Down
133 changes: 133 additions & 0 deletions kotlin/goodmetrics/src/test/kotlin/goodmetrics/MetricsFactoryTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package goodmetrics

import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals

internal class MetricsFactoryTest {
private val emittedMetrics: MutableList<Metrics> = mutableListOf()
private var nowNanos = 0L

@BeforeTest
fun before() {
nowNanos = 0
emittedMetrics.clear()
}

@Test
fun testDistributionTotaltimeType() {
val metricsFactory = MetricsFactory(
sink = emittedMetrics::add,
timeSource = { nowNanos },
totaltimeType = MetricsFactory.TotaltimeType.DistributionMicroseconds
)

metricsFactory.record("test") { metrics ->
metrics.dimension("a_dimension", "a")
metrics.measure("a_measurement", 0)
metrics.distribution("a_distribution", 1)
}
assertEquals(1, emittedMetrics.size)
val metric = emittedMetrics[0]
metric.assertPresence(
dimensions = setOf("a_dimension"),
measurements = setOf("a_measurement"),
distributions = setOf("totaltime", "a_distribution")
)
}

@Test
fun testDistributionTotaltimeTypeNoTotaltimeBehavior() {
val metricsFactory = MetricsFactory(
sink = emittedMetrics::add,
timeSource = { nowNanos },
totaltimeType = MetricsFactory.TotaltimeType.DistributionMicroseconds
)

metricsFactory.recordWithBehavior("test", metricsBehavior = MetricsBehavior.NO_TOTALTIME) { metrics ->
metrics.dimension("a_dimension", "a")
metrics.measure("a_measurement", 0)
metrics.distribution("a_distribution", 1)
}
assertEquals(1, emittedMetrics.size)
val metric = emittedMetrics[0]
metric.assertPresence(
dimensions = setOf("a_dimension"),
measurements = setOf("a_measurement"),
distributions = setOf("a_distribution")
)
}

@Test
fun testMeasurementTotaltimeType() {
val metricsFactory = MetricsFactory(
sink = emittedMetrics::add,
timeSource = { nowNanos },
totaltimeType = MetricsFactory.TotaltimeType.MeasurementMicroseconds
)

metricsFactory.record("test") { metrics ->
metrics.dimension("a_dimension", "a")
metrics.measure("a_measurement", 0)
metrics.distribution("a_distribution", 1)
}
assertEquals(1, emittedMetrics.size)
val metric = emittedMetrics[0]
metric.assertPresence(
dimensions = setOf("a_dimension"),
measurements = setOf("totaltime", "a_measurement"),
distributions = setOf("a_distribution")
)
}

@Test
fun testMeasurementTotaltimeTypeNoTotaltimeBehavior() {
val metricsFactory = MetricsFactory(
sink = emittedMetrics::add,
timeSource = { nowNanos },
totaltimeType = MetricsFactory.TotaltimeType.MeasurementMicroseconds
)

metricsFactory.recordWithBehavior("test", metricsBehavior = MetricsBehavior.NO_TOTALTIME) { metrics ->
metrics.dimension("a_dimension", "a")
metrics.measure("a_measurement", 0)
metrics.distribution("a_distribution", 1)
}
assertEquals(1, emittedMetrics.size)
val metric = emittedMetrics[0]
metric.assertPresence(
dimensions = setOf("a_dimension"),
measurements = setOf("a_measurement"),
distributions = setOf("a_distribution")
)
}

@Test
fun testNoTotaltimeType() {
val metricsFactory = MetricsFactory(
sink = emittedMetrics::add,
timeSource = { nowNanos },
totaltimeType = MetricsFactory.TotaltimeType.None
)

metricsFactory.record("test") { metrics ->
metrics.dimension("a_dimension", "a")
metrics.measure("a_measurement", 0)
metrics.distribution("a_distribution", 1)
}
assertEquals(1, emittedMetrics.size)
val metric = emittedMetrics[0]
metric.assertPresence(
dimensions = setOf("a_dimension"),
measurements = setOf("a_measurement"),
distributions = setOf("a_distribution")
)
}
}

fun Metrics.assertPresence(dimensions: Set<String>, measurements: Set<String>, distributions: Set<String>) {
val view = getView()
assertEquals(dimensions, view.dimensions.keys)
assertEquals(measurements, view.measurements.keys)
assertEquals(distributions, view.distributions.keys)
}

0 comments on commit 48b268f

Please sign in to comment.