Why Observability is a Must-Have for Kotlin Applications
\n\nImagine: your Kotlin application is running in production, and suddenly users complain that requests are taking 10 seconds. You check the logs — silence. Metrics show 100% CPU, but don't tell you which specific endpoint is slowing down. Sound familiar?
\n\nWelcome to the world where observability is not an option, but a necessity. Unlike traditional monitoring, which simply answers "yes/no" to the question "is the system alive," observability allows you to ask questions you didn't even know you had. And Kotlin, with its conciseness and powerful ecosystem, is ideally suited for implementing this culture.
\n\nIn this article, we will break down the three pillars of observability — tracing, metrics, and logs — and show how to integrate them with Kotlin. No fluff, only real code examples.
\n\nTracing: A Request's Journey Through Microservices
\n\nDistributed tracing is like GPS for your request. You can see which services the request passed through, how long each step took, and where the delay occurred. For Kotlin, the best friend for this is OpenTelemetry (OTel).
\n\nSetting up OpenTelemetry in Kotlin
\n\nAdd dependencies to build.gradle.kts:
dependencies {\n implementation("io.opentelemetry:opentelemetry-api:1.36.0")\n implementation("io.opentelemetry:opentelemetry-sdk:1.36.0")\n implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.36.0")\n implementation("io.opentelemetry:opentelemetry-extension-kotlin:1.36.0")\n}\n\nNow let's create a simple tracer:
\n\nimport io.opentelemetry.api.GlobalOpenTelemetry\nimport io.opentelemetry.api.trace.Span\nimport io.opentelemetry.api.trace.Tracer\nimport io.opentelemetry.context.Scope\n\nclass OrderService(private val tracer: Tracer = GlobalOpenTelemetry.getTracer("order-service")) {\n\n fun processOrder(orderId: String) {\n val span: Span = tracer.spanBuilder("processOrder")\n .setAttribute("order.id", orderId)\n .startSpan()\n\n span.use { // use() will automatically close the span\n try {\n // Simulate work\n Thread.sleep(100)\n validateOrder(orderId)\n chargePayment(orderId)\n } catch (e: Exception) {\n span.recordException(e)\n span.setStatus(StatusCode.ERROR, "Order processing failed")\n throw e\n }\n }\n }\n\n private fun validateOrder(orderId: String) {\n val span: Span = tracer.spanBuilder("validateOrder")\n .setAttribute("order.id", orderId)\n .startSpan()\n\n span.use {\n Thread.sleep(50) // Simulate validation\n }\n }\n\n private fun chargePayment(orderId: String) {\n val span: Span = tracer.spanBuilder("chargePayment")\n .setAttribute("order.id", orderId)\n .startSpan()\n\n span.use {\n Thread.sleep(150) // Simulate payment\n }\n }\n}\n\nNotice span.use — this is an extension function from opentelemetry-extension-kotlin that automatically closes the span after the block executes. The beauty of Kotlin in action!
Context Propagation via Coroutines
\n\nIn Kotlin, we actively use coroutines. OpenTelemetry integrates seamlessly with them through the Context mechanism:
\n\nimport kotlinx.coroutines.withContext\nimport io.opentelemetry.api.trace.Span\nimport io.opentelemetry.context.Context\n\nsuspend fun processOrderAsync(orderId: String) = withContext(Context.current()) {\n val span = tracer.spanBuilder("processOrderAsync").startSpan()\n span.use {\n // Your asynchronous logic\n }\n}\n\nTracing reveals the full picture: you can see that 80% of the time is spent on paymen