Skip to content

Commit

Permalink
Add tests for kotlet.metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
turchenkoalex committed Jul 17, 2024
1 parent 1a6e8bb commit 99db6e1
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@ import jakarta.servlet.AsyncEvent
import jakarta.servlet.AsyncListener
import kotlet.HttpCall
import kotlet.metrics.MetricsCollector
import java.time.Clock

private const val START_TIME_ATTRIBUTE = "kotlet.prometheus.started_at"

class PrometheusMetricsCollector(
registry: PrometheusRegistry
registry: PrometheusRegistry,
private val clock: Clock = Clock.systemUTC()
) : MetricsCollector {
override fun startRequest(call: HttpCall) {
call.rawRequest.setAttribute(START_TIME_ATTRIBUTE, System.nanoTime())
call.rawRequest.setAttribute(START_TIME_ATTRIBUTE, clock.millis())
}

override fun endRequest(call: HttpCall) {
if (call.rawRequest.isAsyncStarted) {
call.rawRequest.asyncContext.addListener(RequestCounterAsyncListener(call, counter, summary))
call.rawRequest.asyncContext.addListener(RequestCounterAsyncListener(call, clock, counter, summary))
} else {
measureRequest(call, counter, summary)
measureRequest(call, clock, counter, summary)
}
}

Expand All @@ -45,31 +47,32 @@ class PrometheusMetricsCollector(

private data class RequestCounterAsyncListener(
private val call: HttpCall,
private val clock: Clock,
private val counter: Counter,
private val summary: Summary,
) : AsyncListener {
override fun onComplete(event: AsyncEvent) {
measureRequest(call, counter, summary)
measureRequest(call, clock, counter, summary)
}

override fun onTimeout(event: AsyncEvent) {
measureRequest(call, counter, summary)
measureRequest(call, clock, counter, summary)
}

override fun onError(event: AsyncEvent) {
measureRequest(call, counter, summary)
measureRequest(call, clock, counter, summary)
}

override fun onStartAsync(event: AsyncEvent) {
}
}

private fun measureRequest(call: HttpCall, counter: Counter, summary: Summary) {
private fun measureRequest(call: HttpCall, clock: Clock, counter: Counter, summary: Summary) {
val statusCode = call.statusCode()
counter.labelValues(call.httpMethod.name, call.routePath, statusCode).inc()
val startTime = call.rawRequest.getAttribute(START_TIME_ATTRIBUTE) as? Long
if (startTime != null) {
val duration = (System.nanoTime() - startTime) / 1_000_000_000.0
val duration = (clock.millis() - startTime) / 1_000.0
summary.labelValues(call.httpMethod.name, call.routePath, statusCode).observe(duration)
call.rawRequest.removeAttribute(START_TIME_ATTRIBUTE)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ import io.mockk.verify
import io.prometheus.metrics.core.metrics.Metric
import io.prometheus.metrics.model.registry.Collector
import io.prometheus.metrics.model.registry.PrometheusRegistry
import io.prometheus.metrics.model.snapshots.CounterSnapshot.CounterDataPointSnapshot
import io.prometheus.metrics.model.snapshots.Quantile
import io.prometheus.metrics.model.snapshots.SummarySnapshot.SummaryDataPointSnapshot
import kotlet.HttpMethod
import kotlet.Kotlet
import kotlet.metrics.installMetrics
import kotlet.mocks.Mocks
import java.time.Clock
import kotlin.test.Test
import kotlin.test.assertEquals

class PrometheusMetricsCollectorUnitTest {
@Test
Expand All @@ -36,6 +43,65 @@ class PrometheusMetricsCollectorUnitTest {
}
}

@Test
fun `test kotlet_http_requests metric values`() {
val registry = PrometheusRegistry()
val prometheusMetricsCollector = PrometheusMetricsCollector(registry)

val call = Mocks.httpCall(
method = HttpMethod.GET
)

prometheusMetricsCollector.startRequest(call)
prometheusMetricsCollector.endRequest(call)

val httpCallCounter = registry.scrape { name -> name == "kotlet_http_requests" }.single()
val point = httpCallCounter.dataPoints.single() as CounterDataPointSnapshot

assertEquals(1.0, point.value)
assertEquals(3, point.labels.size())
assertEquals("GET", point.labels.get("method"))
assertEquals("/", point.labels.get("path"))
assertEquals("200", point.labels.get("status"))
}

@Test
fun `test kotlet_http_requests_duration_seconds metric values`() {
val registry = PrometheusRegistry()
val clock = mockk<Clock>()
val prometheusMetricsCollector = PrometheusMetricsCollector(registry, clock)

val call = Mocks.httpCall(
method = HttpMethod.POST,
routePath = "/test"
)

val now = System.currentTimeMillis()
every { clock.millis() } returns now andThen (now + 5000L)

prometheusMetricsCollector.startRequest(call)
call.status = 400
prometheusMetricsCollector.endRequest(call)

val httpCallCounter = registry.scrape { name -> name == "kotlet_http_requests_duration_seconds" }.single()
val point = httpCallCounter.dataPoints.single() as SummaryDataPointSnapshot

assertEquals(1, point.count)
assertEquals(3, point.labels.size())
assertEquals(5.0, point.sum)
assertEquals(0.5, point.quantiles.get(0).quantile)
assertEquals(5.0, point.quantiles.get(0).value)
assertEquals(0.9, point.quantiles.get(1).quantile)
assertEquals(5.0, point.quantiles.get(1).value)
assertEquals(0.95, point.quantiles.get(2).quantile)
assertEquals(5.0, point.quantiles.get(2).value)
assertEquals(0.99, point.quantiles.get(3).quantile)
assertEquals(5.0, point.quantiles.get(3).value)
assertEquals("POST", point.labels.get("method"))
assertEquals("/test", point.labels.get("path"))
assertEquals("400", point.labels.get("status"))
}

data class MetricMatch(val name: String) : Matcher<Metric> {
override fun match(arg: Metric?): Boolean {
if (arg == null) {
Expand Down
8 changes: 7 additions & 1 deletion mocks/src/main/kotlin/kotlet/mocks/Mocks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ import kotlet.mocks.http.MockHttpCall
object Mocks {
fun httpCall(
method: HttpMethod,
routePath: String = "/",
headers: Map<String, String> = emptyMap(),
data: ByteArray = ByteArray(0)
): MockHttpCall {
return MockHttpCall(httpMethod = method, headers = headers, requestData = data)
return MockHttpCall(
httpMethod = method,
routePath = routePath,
headers = headers,
requestData = data
)
}

val okHandler: Handler = { call ->
Expand Down
2 changes: 1 addition & 1 deletion mocks/src/main/kotlin/kotlet/mocks/http/MockHttpCall.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import java.io.ByteArrayOutputStream
*/
class MockHttpCall(
override val httpMethod: HttpMethod,
override val routePath: String,
headers: Map<String, String>,
requestData: ByteArray,
) : HttpCall {
private var contentTypeField: String = ""
private var statusField: Int = 200
private val responseStream = ByteArrayOutputStream()

override val routePath = "/"
override val parameters: Map<String, String> = emptyMap()
override val rawRequest: HttpServletRequest
override val rawResponse: HttpServletResponse
Expand Down

0 comments on commit 99db6e1

Please sign in to comment.