Monitoring Kotlin Applications: Advanced Metrics and Alerts
In modern software development, monitoring is no longer an option — it is a mandatory element of production infrastructure. For Kotlin applications running under high load, simple logging is not enough. Full observability is required: collecting metrics, setting up alerts, and visualizing data in real time. In this article, we will explore advanced techniques for monitoring Kotlin applications using the Prometheus, Grafana, and Micrometer stack.
1. Tooling: Micrometer and Prometheus
The best way to implement monitoring in a Kotlin application is to use the Micrometer library. It provides a facade for various monitoring systems, including Prometheus. Micrometer supports both JVM metrics and custom business indicators.
Add the dependencies in build.gradle.kts:
dependencies { implementation("io.micrometer:micrometer-core:1.12.0") implementation("io.micrometer:micrometer-registry-prometheus:1.12.0") implementation("io.ktor:ktor-server-metrics-micrometer:2.3.7")}For a Ktor application, the setup looks like this:
import io.ktor.server.engine.*import io.ktor.server.netty.*import io.ktor.server.application.*import io.ktor.server.metrics.micrometer.*import io.micrometer.prometheus.PrometheusMeterRegistryimport io.micrometer.prometheus.PrometheusConfig
fun main() { val meterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
embeddedServer(Netty, port = 8080) { install(MicrometerMetrics) { registry = meterRegistry // Add standard JVM metrics meterRegistry.config().commonTags("application", "my-kotlin-app") } routing { get("/metrics") { call.respond(meterRegistry.scrape()) } } }.start(wait = true)}This code automatically collects metrics for HTTP requests, thread pool, and JVM. The /metrics endpoint outputs data in Prometheus format.
2. Custom Metrics: Business Indicators
Standard metrics are often not enough. For advanced monitoring, you need to collect business indicators: number of orders, payment processing time, validation error rate.
Example of creating a custom counter and timer:
import io.micrometer.core.instrument.Counterimport io.micrometer.core.instrument.MeterRegistryimport io.micrometer.core.instrument.Timerimport java.util.concurrent.TimeUnit
class OrderMetrics(private val registry: MeterRegistry) { private val orderCounter: Counter = Counter.builder("orders.created") .description("Total number of created orders") .register(registry)
private val processTimer: Timer = Timer.builder("orders.processing.time") .description("Time to process an order") .publishPercentiles(0.5, 0.95, 0.99) .register(registry)
fun recordOrderCreation() { orderCounter.increment() }
fun measureProcessingTime(block: () -> Unit) { processTimer.record(block) }}Usage in a service:
class OrderService(private val metrics: OrderMetrics) { fun createOrder(order: Order): Order { metrics.recordOrderCreation() return metrics.measureProcessingTime { // business logic Thread.sleep(100) // simulation order } }}Note the percentiles: they allow tracking not only the average time but also the "tails" (p99), which is critical for SLAs.
3. Setting Up Alerts in Prometheus
Collecting metrics is useless without an alerting system. Alerts are configured in the Prometheus configuration file (alert.rules.yml).
Example rules for Kotl