Hello :wave::skin-tone-4: I was wondering if you ...
# ktor
h
Hello 👋🏽 I was wondering if you could shed some light in terms of the best practices to monitor the performance of a server endpoint in Ktor. I understand that Ktor exposes some metrics such as
ktor.http.server.requests.upper_99
. But I need to add an extra property on top of this metric that says if the request took longer than 1s, add a new property saying that
failed
otherwise
met
. Questions: 1. Is it possible to add new metric on top of this existing metric? Otherwise, would the code below be the most appropriate way to do it? a. I know that after every endpoint execution Ktor logs the result, such as.
200 OK: GET - /healthcheck in 35ms
. Is it possible to extract the value instead of calculating it?
Copy code
get("/healthcheck") {
        val startTime = System.currentTimeMillis()
        println(call.request.uri)
        call.respondText(Constants.HEALTHCHECK_RESPONSE, ContentType.Text.Plain)
        val endTime = System.currentTimeMillis() // Capture the end time
        val duration = endTime - startTime // Calculate the duration
        if (duration < 1000L) {
            //Send Success metric
        }
    }
2. What would be the most appropriate way to get the name of the path.
call.request.call.route.parent
? Is this the most accurate way? Assuming I don't want to get the value of the "queryParameter" and how the endpoint is defined instead. Thanks for your help.
a
1. Do you use the DropwizardMetrics plugin? 2. Do you mean the full original path (including the parent routes) defined in the routing for a specific route?
👀 1
h
Howdy, @Aleksei Tirman [JB] =) I don't use
DropwizardMetrics
Plugin. I'm using
micrometer
. For example:
Copy code
install(MicrometerMetrics) {
        registry = StatsdMeterRegistry.registry
    }
Copy code
object StatsdMeterRegistry {
    val registry = statsdMeterRegistry { configurationParameter ->
        when (configurationParameter) {
            "statsd.host" -> STATSD_HOST
            "statsd.port" -> STATSD_PORT
            else -> null
        }
    }.apply {
        config().commonTags(*MICROMETER_COMMON_TAGS.toTypedArray())
    }

//Here I can create my custom Metrics if I wish.

}


private fun statsdMeterRegistry(setup: (String) -> String?) = StatsdMeterRegistry(StatsdConfig(setup), Clock.SYSTEM)
2. Yes, I would like to be able to capture the fullEndpoint Path, in a similar way that Ktor does. For example. Notice the arrow, that I dont' want to use the queryParameter value, I would like to keep the definition of the route.
A mock of what I was trying to do would look like:
StatsdMeterRegistry
, where I create the
createEndpointLatencyCounter
Copy code
object StatsdMeterRegistry {
    val registry = statsdMeterRegistry { configurationParameter ->
        when (configurationParameter) {
            "statsd.host" -> STATSD_HOST
            "statsd.port" -> STATSD_PORT
            else -> null
        }
    }.apply {
        config().commonTags(*MICROMETER_COMMON_TAGS.toTypedArray())
    }

    fun createEndpointLatencyCounter(endpoint: RoutingNode?, status: String): Counter {
        return Counter.builder("artifactory-permission.endpoint.latency")
            .description("Endpoint latency metric")
            .tag("endpoint", endpoint) //This can be null according to RouteNode.
            .tag("status", status)
            .register(registry)
    }
}

private fun statsdMeterRegistry(setup: (String) -> String?) = StatsdMeterRegistry(StatsdConfig(setup), Clock.SYSTEM)
Here is an example of the
Route
, attempting to send Metric.
Copy code
put("/validate") {
    var result = (mapOf <String, Boolean>())
    val start = System.currentTimeMillis()
    runCatching {
        val requestPayload = call.receive<ArtifactoryPermissionValidatePostRequestModel>()
        call.logger.debug("Received validation requestPayload: $requestPayload", )
        result = ArtifactoryPermissionService.validateUploadPermission(requestPayload)
    }.onSuccess {
        val duration = System.currentTimeMillis() - start
        if (duration < 1000L) {
            StatsdMeterRegistry.createEndpointLatencyCounter(call.request.call.route.parent, "met").increment()
        } else {
            StatsdMeterRegistry.createEndpointLatencyCounter(call.request.call.route.parent, "failed").increment()
        }
        call.respond(HttpStatusCode.OK, result)
    }.onFailure {
        handlePostValidationFailure(it)
    }.getOrThrow()
}
a
1. If it's intended that the counter is created on each call then this approach should work. 2. You can iterate the parent routes, filter them on the selector type and join the corresponding representations with /. Here is an example:
Copy code
fun RoutingNode?.representation(): String {
    val shouldInclude = fun(r: RoutingNode): Boolean {
        return r is RoutingRoot || r.selector is PathSegmentConstantRouteSelector
    }

    if (this == null) return ""

    val routes = mutableListOf<RoutingNode>()

    if (shouldInclude(this)) {
        routes.add(this)
    }

    var parent: RoutingNode? = this.parent
    while (parent != null) {
        if (shouldInclude(parent)) {
            routes.addFirst(parent)
        }

        parent = parent.parent
    }

    return routes.joinToString(separator = "/") { route ->
        if (route is RoutingRoot) {
            ""
        } else {
            when (val selector = route.selector) {
                is PathSegmentConstantRouteSelector -> selector.value
                else -> ""
            }
        }
    }
}
h
Interesting! Thanks so much for that. So, basically, there is no way for us to get access to the "request duration" and the path that's sent to the Logs.
200 OK: GET - /healthcheck in 35ms
. I will see how we can do that then.
a
This line is logged by the CallLogging plugin. You can get access to the duration by calling the
ApplicationCall.processingTimeMillis
method in the
ResponseSent
hook.
h
Oh. I see! I will have a look at it! Thanks so much Aleksei