Ktor 3.2.3 Help

Distributed tracing with OpenTelemetry in Ktor Client

Ktor integrates with OpenTelemetry — an open-source observability framework for collecting telemetry data such as traces, metrics, and logs. It provides a standard way to instrument applications and export data to monitoring and observability tools like Grafana or Jaeger.

The KtorClientTelemetry plugin allows you to automatically trace outgoing HTTP requests. It captures metadata like method, URL, and status code and propagates trace context across services. You can also customize span attributes or use your own OpenTelemetry configuration.

Add dependencies

To use KtorClientTelemetry, you need to include the opentelemetry-ktor-3.0 artifact in the build script:

implementation("io.opentelemetry.instrumentation:opentelemetry-ktor-3.0:2.18.1-alpha")
implementation "io.opentelemetry.instrumentation:opentelemetry-ktor-3.0:2.18.1-alpha"
<dependencies> <dependency> <groupId>io.opentelemetry.instrumentation</groupId> <artifactId>opentelemetry-ktor-3.0</artifactId> <version>2.18.1-alpha</version> </dependency> </dependencies>

Configure OpenTelemetry

Before installing the KtorClientTelemetry plugin in your Ktor application, you need to configure and initialize an OpenTelemetry instance. This instance is responsible for managing telemetry data, including traces and metrics.

Automatic configuration

A common way to configure OpenTelemetry is to use AutoConfiguredOpenTelemetrySdk. This simplifies setup by automatically configuring exporters and resources based on system properties and environment variables.

You can still customize the automatically detected configuration — for example, by adding a service.name resource attribute:

package com.example import io.opentelemetry.api.OpenTelemetry import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk import io.opentelemetry.semconv.ServiceAttributes fun getOpenTelemetry(serviceName: String): OpenTelemetry { return AutoConfiguredOpenTelemetrySdk.builder().addResourceCustomizer { oldResource, _ -> oldResource.toBuilder() .putAll(oldResource.attributes) .put(ServiceAttributes.SERVICE_NAME, serviceName) .build() }.build().openTelemetrySdk }

Programmatic configuration

To define exporters, processors, and propagators in code, instead of relying on environment-based configuration, you can use OpenTelemetrySdk.

The following example shows how to configure OpenTelemetry programmatically with an OTLP exporter, a span processor, and a trace context propagator:

import io.opentelemetry.api.OpenTelemetry import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator import io.opentelemetry.context.propagation.ContextPropagators import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.sdk.trace.export.BatchSpanProcessor fun configureOpenTelemetry(): OpenTelemetry { val spanExporter = OtlpGrpcSpanExporter.builder() .setEndpoint("http://localhost:4317") .build() val tracerProvider = SdkTracerProvider.builder() .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build()) .build() return OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .buildAndRegisterGlobal() }

Use this approach if you require full control over telemetry setup, or when your deployment environment cannot rely on automatic configuration.

Install KtorClientTelemetry

To install the KtorClientTelemetry plugin, pass it to the install function inside a client configuration block and set the configured OpenTelemetry instance:

import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.auth.* //... val client = HttpClient(CIO) { val openTelemetry = getOpenTelemetry(serviceName = "opentelemetry-ktor-client") install(KtorClientTelemetry) { setOpenTelemetry(openTelemetry) } }

Configure tracing

You can customize how the Ktor client records and exports OpenTelemetry spans for outgoing HTTP calls. The options below allow you to adjust which requests are traced, how spans are named, what attributes they contain, which headers are captured, and how span kinds are determined.

Trace additional HTTP methods

By default, the plugin traces standard HTTP methods (GET, POST, PUT, etc.). To trace additional or custom methods, configure the knownMethods property:

install(KtorClientTelemetry) { // ... knownMethods(HttpMethod.DefaultMethods + CUSTOM_METHOD) }

Capture headers

To include specific HTTP request headers as span attributes, use the capturedRequestHeaders property:

install(KtorClientTelemetry) { // ... capturedRequestHeaders(HttpHeaders.UserAgent) }

Capture response headers

To capture specific HTTP response headers as span attributes, use the capturedResponseHeaders property:

install(KtorClientTelemetry) { // ... capturedResponseHeaders(HttpHeaders.ContentType, CUSTOM_HEADER) }

Add custom attributes

To attach custom attributes at the start or end of a span, use the attributesExtractor property:

install(KtorClientTelemetry) { // ... attributesExtractor { onStart { attributes.put("start-time", System.currentTimeMillis()) } onEnd { attributes.put("end-time", Instant.now().toEpochMilli()) } } }

Next steps

Once you have KtorClientTelemetry installed and configured, you can verify that spans are being created and propagated by sending requests to a service that also has telemetry enabled—such as one using KtorServerTelemetry. Viewing both sides of the trace in an observability backend like Jaeger, Zipkin, or Grafana Tempo will confirm that distributed tracing is working end-to-end.

08 September 2025