Kotlin Monitoring
Introduction
Monitoring is a critical aspect of maintaining a healthy and reliable backend application. For Kotlin backends, proper monitoring allows you to track performance metrics, identify bottlenecks, detect errors, and ensure your application meets service level objectives (SLOs). In this guide, we'll explore how to implement monitoring in Kotlin backend applications, from basic logging to advanced observability solutions.
Why Monitoring Matters
Before diving into the technical details, let's understand why monitoring is essential:
- Early Problem Detection: Identify issues before they affect your users
- Performance Optimization: Discover and eliminate bottlenecks
- Resource Planning: Make informed decisions about scaling
- Business Intelligence: Gain insights into user behavior and system usage
- Security: Detect unusual patterns that might indicate security breaches
Basic Monitoring with Logging
Setting Up a Logger
The simplest form of monitoring begins with proper logging. Kotlin applications typically use SLF4J with an implementation like Logback or Log4j2.
First, add the required dependencies to your build.gradle.kts
:
dependencies {
implementation("org.slf4j:slf4j-api:1.7.36")
implementation("ch.qos.logback:logback-classic:1.2.11")
}
Create a basic Logback configuration file (src/main/resources/logback.xml
):
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
Now, you can use the logger in your Kotlin code:
import org.slf4j.LoggerFactory
class UserService {
private val logger = LoggerFactory.getLogger(UserService::class.java)
fun createUser(username: String): User {
logger.info("Creating new user: $username")
try {
// User creation logic
val user = User(id = generateId(), username = username)
logger.info("Successfully created user: ${user.id}")
return user
} catch (e: Exception) {
logger.error("Failed to create user: $username", e)
throw e
}
}
}
Structured Logging
For more advanced analysis, structured logging in JSON format can be useful:
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
<root level="INFO">
<appender-ref ref="JSON" />
</root>
</configuration>
Remember to add the Logstash encoder dependency:
implementation("net.logstash.logback:logstash-logback-encoder:7.2")
Metrics Collection with Micrometer
Micrometer provides a simple facade for the most popular monitoring systems, allowing you to instrument your code with dimensional metrics.
Add Micrometer to your project:
dependencies {
implementation("io.micrometer:micrometer-registry-prometheus:1.9.2")
}
Here's how to use Micrometer to track application metrics:
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer
import org.springframework.stereotype.Service
@Service
class OrderService(private val registry: MeterRegistry) {
private val orderProcessingTimer = registry.timer("order.processing")
fun processOrder(order: Order) {
// Record how long order processing takes
Timer.Sample.start(registry).use { sample ->
try {
// Process the order
// ...
// Increment a counter for successful orders
registry.counter("orders.success").increment()
sample.stop(orderProcessingTimer)
} catch (e: Exception) {
// Increment a counter for failed orders
registry.counter("orders.failure").increment()
throw e
}
}
}
fun trackInventoryLevel(productId: String, quantity: Int) {
// Report current inventory level as a gauge
registry.gauge("inventory.quantity", listOf(Tag.of("product", productId)), quantity)
}
}
Integrating with Prometheus and Grafana
Prometheus is a popular open-source monitoring system that works well with Kotlin applications. Grafana provides visualization for Prometheus metrics.
Setting up Prometheus with Spring Boot
If you're using Spring Boot, add the Actuator and Prometheus dependencies:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
}
Configure your application.properties
or application.yml
file:
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
This exposes a /actuator/prometheus
endpoint that Prometheus can scrape.
Basic Prometheus Configuration
Create a prometheus.yml
file:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'kotlin-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
Visualizing with Grafana
Once Prometheus is collecting your metrics, you can set up Grafana dashboards to visualize them. Here's a sample JSON for a basic dashboard:
{
"annotations": {
"list": []
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 1,
"links": [],
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"hiddenSeries": false,
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "order_processing_seconds_count",
"legendFormat": "Order Processing Count",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Order Processing",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"schemaVersion": 22,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Kotlin App Dashboard",
"uid": "kotlin-app",
"version": 1
}
Health Checks with Spring Boot Actuator
For basic application health monitoring, Spring Boot Actuator provides a /actuator/health
endpoint:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
}
You can customize health checks by implementing your own health indicators:
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.stereotype.Component
@Component
class DatabaseHealthIndicator(private val dataSource: DataSource) : HealthIndicator {
override fun health(): Health {
return try {
val connection = dataSource.connection
connection.use {
if (it.isValid(1000)) {
Health.up()
.withDetail("database", "PostgreSQL")
.withDetail("status", "Connected")
.build()
} else {
Health.down()
.withDetail("error", "Failed to validate database connection")
.build()
}
}
} catch (e: Exception) {
Health.down()
.withDetail("error", e.message)
.build()
}
}
}
Distributed Tracing with OpenTelemetry
For microservices architectures, distributed tracing helps you understand the flow of requests across multiple services.
Add OpenTelemetry dependencies:
dependencies {
implementation("io.opentelemetry:opentelemetry-api:1.15.0")
implementation("io.opentelemetry:opentelemetry-sdk:1.15.0")
implementation("io.opentelemetry:opentelemetry-exporter-jaeger:1.15.0")
}
Configure OpenTelemetry in your application:
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor
fun initTracing(): Tracer {
val jaegerExporter = JaegerGrpcSpanExporter.builder()
.setEndpoint("http://localhost:14250")
.build()
val tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(SimpleSpanProcessor.create(jaegerExporter))
.build()
val openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.build()
GlobalOpenTelemetry.set(openTelemetry)
return openTelemetry.getTracer("my-kotlin-app")
}
Use the tracer in your code:
class OrderProcessingService(private val tracer: Tracer) {
fun processOrder(order: Order) {
tracer.spanBuilder("processOrder").startSpan().use { span ->
// Add contextual information to the span
span.setAttribute("order.id", order.id)
span.setAttribute("customer.id", order.customerId)
try {
// Process the order
validateOrder(order)
checkInventory(order)
processPayment(order)
span.setAttribute("status", "success")
} catch (e: Exception) {
span.recordException(e)
span.setAttribute("status", "failed")
throw e
}
}
}
private fun validateOrder(order: Order) {
tracer.spanBuilder("validateOrder").startSpan().use { span ->
// Validation logic
}
}
private fun checkInventory(order: Order) {
tracer.spanBuilder("checkInventory").startSpan().use { span ->
// Inventory check logic
}
}
private fun processPayment(order: Order) {
tracer.spanBuilder("processPayment").startSpan().use { span ->
// Payment processing logic
}
}
}
Real-World Example: Monitoring a Kotlin Web Service
Let's put everything together in a comprehensive example of monitoring a Kotlin web service built with Spring Boot:
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.*
import java.util.concurrent.atomic.AtomicInteger
@SpringBootApplication
class MonitoredAppApplication
fun main(args: Array<String>) {
runApplication<MonitoredAppApplication>(*args)
}
data class Product(val id: String, val name: String, val price: Double)
@RestController
@RequestMapping("/api/products")
class ProductController(private val productService: ProductService) {
private val logger = LoggerFactory.getLogger(ProductController::class.java)
@GetMapping
fun getAllProducts(): List<Product> {
logger.info("Fetching all products")
return productService.getAllProducts()
}
@GetMapping("/{id}")
fun getProduct(@PathVariable id: String): Product {
logger.info("Fetching product with ID: $id")
return productService.getProduct(id)
}
@PostMapping
fun createProduct(@RequestBody product: Product): Product {
logger.info("Creating new product: ${product.name}")
return productService.createProduct(product)
}
}
@Service
class ProductService(private val meterRegistry: MeterRegistry) {
private val logger = LoggerFactory.getLogger(ProductService::class.java)
private val products = mutableMapOf<String, Product>()
private val productCount = AtomicInteger(0)
// Register gauges
init {
meterRegistry.gauge("products.count", productCount)
}
fun getAllProducts(): List<Product> {
val timer = meterRegistry.timer("product.service.get.all")
return timer.record<List<Product>> {
logger.debug("Returning ${products.size} products")
products.values.toList()
}
}
fun getProduct(id: String): Product {
val timer = meterRegistry.timer("product.service.get.one", "productId", id)
return timer.record<Product> {
logger.debug("Looking up product with ID: $id")
products[id] ?: throw ProductNotFoundException(id)
}
}
fun createProduct(product: Product): Product {
val timer = meterRegistry.timer("product.service.create")
return timer.record<Product> {
logger.info("Creating product: ${product.name}")
products[product.id] = product
productCount.incrementAndGet()
meterRegistry.counter("product.created").increment()
product
}
}
}
class ProductNotFoundException(id: String) : RuntimeException("Product not found: $id")
@RestControllerAdvice
class GlobalExceptionHandler(private val meterRegistry: MeterRegistry) {
private val logger = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)
@ExceptionHandler(ProductNotFoundException::class)
@ResponseStatus(HttpStatus.NOT_FOUND)
fun handleProductNotFound(ex: ProductNotFoundException): Map<String, String> {
logger.warn("Product not found: ${ex.message}")
meterRegistry.counter("errors", "type", "product_not_found").increment()
return mapOf("error" to ex.message!!)
}
@ExceptionHandler(Exception::class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun handleGenericError(ex: Exception): Map<String, String> {
logger.error("Unexpected error", ex)
meterRegistry.counter("errors", "type", "server_error").increment()
return mapOf("error" to "An unexpected error occurred")
}
}
Alerts and Notifications
Once you have monitoring in place, you'll want to set up alerts to notify you when things go wrong. Most monitoring systems like Prometheus support alert rules.
Here's a sample alert rule for Prometheus that will trigger if request latency gets too high:
groups:
- name: example
rules:
- alert: HighRequestLatency
expr: http_request_duration_seconds{path="/api/products"} > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "High request latency on {{ $labels.instance }}"
description: "Request latency is above 500ms (current value: {{ $value }}s)"
Summary
Effective monitoring is crucial for maintaining reliable Kotlin backend services. In this guide, we covered:
- Basic Logging: Using SLF4J and Logback for application logs
- Metrics Collection: Implementing Micrometer for gathering application metrics
- Prometheus Integration: Exposing metrics for Prometheus to collect
- Grafana Visualization: Creating dashboards to visualize application metrics
- Health Checks: Implementing custom health indicators with Spring Boot Actuator
- Distributed Tracing: Using OpenTelemetry to trace requests across services
- Alerting: Setting up notifications for when things go wrong
By implementing these monitoring strategies, you'll gain better visibility into your Kotlin backend applications and be able to respond quickly to issues.
Additional Resources
- Spring Boot Actuator Documentation
- Micrometer Documentation
- Prometheus Getting Started Guide
- Grafana Documentation
- OpenTelemetry for Java/Kotlin
Exercises
- Set up basic logging in a simple Kotlin application and experiment with different log levels.
- Implement Micrometer metrics in a Spring Boot application to track API request counts.
- Configure Prometheus to scrape metrics from your application and create a simple Grafana dashboard.
- Write a custom health indicator that checks the connectivity to an external service.
- Implement distributed tracing in a multi-service application using OpenTelemetry.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)