
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Debugging latency issues in distributed Kotlin applications requires more than log aggregation; you need correlated telemetry data to pinpoint bottlenecks across service boundaries. Implementing observability for Kotlin with OpenTelemetry provides the standardized signals necessary to understand system behavior without vendor lock-in. This guide covers the practical configuration of the Java Agent, manual instrumentation patterns specific to Kotlin coroutines, and exporting data via OTLP to backends like Jaeger or Grafana Tempo.
How do you configure auto-instrumentation for observability for Kotlin with OpenTelemetry?
The most reliable starting point for any JVM-based language is the OpenTelemetry Java Agent. Because Kotlin compiles to standard Java bytecode, the agent can intercept HTTP requests, database calls, and messaging operations without code changes. However, treating Kotlin exactly like Java leads to gaps in async execution tracking. You must explicitly enable context propagation modules designed for Kotlin coroutines.
Attaching the agent in Gradle
In 2026, the recommended approach uses the official Gradle plugin rather than raw JVM arguments. This ensures version alignment between the agent and your build tools.
plugins {
id("io.opentelemetry.instrumentation.gradle") version "2.12.0"
}
dependencies {
// Core API for manual instrumentation later
implementation("io.opentelemetry:opentelemetry-api:1.45.0")
// CRITICAL: Enables context propagation across coroutine boundaries
implementation("io.opentelemetry:opentelemetry-extension-kotlin:1.45.0")
} When running locally or in CI, specify the endpoint and service name via environment variables. Avoid hardcoding these in application.conf to maintain twelve-factor compliance.
OTEL_SERVICE_NAME=payment-service-kotlinOTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317OTEL_TRACES_EXPORTER=otlpOTEL_METRICS_EXPORTER=otlp
A common mistake in mixed Java/Kotlin projects is assuming the base agent handles suspend functions automatically. Without the opentelemetry-extension-kotlin dependency on the classpath, trace context frequently breaks at the first suspension point, resulting in orphaned spans that make debugging impossible. Always verify this dependency exists before troubleshooting missing traces.
How do you handle coroutine context propagation in Kotlin traces?
Kotlin’s structured concurrency model decouples execution threads from logical workflows. Standard ThreadLocal-based context propagation fails when a coroutine resumes on a different worker thread. To maintain valid parent-child relationships in your traces, you must bridge the OpenTelemetry context into the Kotlin CoroutineContext.
Bridging contexts manually
While newer agent versions attempt automatic bridging, explicit wrapping remains the safest pattern for complex async pipelines. Use the asContextElement() extension function to bind the current OTel context to the coroutine scope.
import io.opentelemetry.kotlin.extensions.asContextElement
import kotlinx.coroutines.withContext
suspend fun processOrder(orderId: String) {
val span = tracer.spanBuilder("process-order")
.setAttribute("order.id", orderId)
.startSpan()
try {
// Bridges OTel context into coroutine context
withContext(span.asContextElement()) {
validateInventory(orderId) // Child span linked correctly
chargePayment(orderId) // Even if suspended/resumed elsewhere
}
} finally {
span.end()
}
} If you use MDC (Mapped Diagnostic Context) for logging, remember that SLF4J MDC is also ThreadLocal-bound. Combine both bridges to ensure log correlation IDs survive suspension points alongside trace context. This alignment between logs and traces is fundamental to effective structured logging best practices.
What are the best practices for manual instrumentation in Kotlin?
Auto-instrumentation captures infrastructure interactions, but business logic visibility requires manual spans. In Kotlin, avoid creating spans inside pure functions or high-frequency loops. Focus on domain boundaries, external integrations, and decision points that affect user experience.
| Instrumentation Target | Recommended Approach | Common Pitfall |
|---|---|---|
| HTTP Controllers | Auto-instrumentation only | Adding redundant manual server spans |
| Database Queries | Auto-instrumentation + SQL comments | Manually wrapping every repository call |
| Domain Services | Manual spans for key operations | Over-tracing trivial helper methods |
| External API Calls | Auto + custom attributes for business IDs | Forgetting to capture error status codes |
| Background Jobs | Manual root spans with job metadata | Relying on thread-based auto-detection |
Enriching spans with semantic attributes
Raw timing data lacks context. Always attach semantic attributes following OpenTelemetry conventions so backends can index and query effectively. For e-commerce systems processing payments in Nepal or globally, include transaction identifiers and regional metadata.
val span = tracer.spanBuilder("payment.process")
.setAttribute("payment.gateway", "esewa")
.setAttribute("transaction.amount", amount)
.setAttribute("transaction.currency", "NPR")
.setAttribute("user.tier", customerTier)
.startSpan()
// Record exceptions properly instead of just logging
try {
gateway.charge(amount)
} catch (e: PaymentDeclinedException) {
span.setStatus(StatusCode.ERROR, e.message)
span.recordException(e)
throw e
} finally {
span.end()
} Never put PII (personally identifiable information) directly into span attributes. Use hashed references or opaque IDs that can be joined against secure data stores during investigations. This discipline supports compliance frameworks like ISO 27001 and prevents accidental data leakage through observability backends.
How do you export telemetry data efficiently from Kotlin services?
Export configuration directly impacts application performance and operational costs. The OTLP protocol over gRPC is the default choice for 2026 deployments due to its binary efficiency and streaming support. HTTP/protobuf serves as a fallback when network policies block gRPC ports.
Tuning batch exporters
Default exporter settings prioritize low latency over throughput. For high-volume Kotlin services, increase batch sizes and intervals to reduce network overhead. These values should be tuned based on your specific traffic patterns and collector capacity.
OTEL_BSPR_MAX_EXPORT_BATCH_SIZE=512(default 512, consider 1024 for high-throughput)OTEL_BSPR_SCHEDULE_DELAY=5000(milliseconds between exports)OTEL_BSPR_MAX_QUEUE_SIZE=2048(prevent memory pressure during spikes)OTEL_EXPORTER_OTLP_COMPRESSION=gzip(reduces bandwidth ~70%)
Always deploy an OpenTelemetry Collector as a sidecar or daemonset rather than exporting directly to managed backends. The collector buffers transient failures, performs tail-based sampling to reduce costs, and sanitizes sensitive headers before data leaves your infrastructure. Direct exports from application pods create tight coupling and amplify outage impact when backend endpoints experience latency.
Sampling strategies for cost control
Tracing every request becomes prohibitively expensive at scale. Implement head-based sampling for development environments and probabilistic or trace-id ratio sampling for production. For critical paths like payment processing, use parent-based sampling to ensure complete traces are retained even when overall sample rates drop to 1%.
Configure sampling decisions at the collector level whenever possible. This allows dynamic adjustment without redeploying Kotlin services and ensures consistent policies across polyglot architectures where multiple languages share the same trace pipeline.
Implementing Production-Grade Observability for Kotlin with OpenTelemetry
Successful observability for Kotlin with OpenTelemetry depends on respecting the language's concurrency model while adhering to vendor-neutral standards. Start with the Java Agent and Kotlin extensions for baseline coverage, add manual spans only where business context matters, and route all telemetry through a local collector. Validate your setup by triggering errors in staging and confirming end-to-end trace reconstruction before relying on it during incidents. If your team needs assistance designing compliant, audit-ready observability pipelines for JVM workloads, reach out to discuss your architecture.