Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ logback = "1.5.24"

dropwizard = "4.2.37"
micrometer = "1.16.2"
opentelemetry = "1.54.0"

jansi = "2.4.2"
typesafe = "1.4.5"
Expand Down Expand Up @@ -210,6 +211,9 @@ okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
dropwizard-core = { module = "io.dropwizard.metrics:metrics-core", version.ref = "dropwizard" }
dropwizard-jvm = { module = "io.dropwizard.metrics:metrics-jvm", version.ref = "dropwizard" }
micrometer = { module = "io.micrometer:micrometer-core", version.ref = "micrometer" }
opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api", version.ref = "opentelemetry" }
opentelemetry-sdk = { module = "io.opentelemetry:opentelemetry-sdk", version.ref = "opentelemetry" }
opentelemetry-sdk-testing = { module = "io.opentelemetry:opentelemetry-sdk-testing", version.ref = "opentelemetry" }

mustache = { module = "com.github.spullara.mustache.java:compiler", version.ref = "mustache" }
freemarker = { module = "org.freemarker:freemarker", version.ref = "freemarker" }
Expand Down
135 changes: 135 additions & 0 deletions ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# ktor-client-opentelemetry

Native OpenTelemetry integration for Ktor HTTP client. Provides distributed tracing, metrics, and trace context injection for outgoing requests following [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/http-spans/).

## Installation

Add the dependency to your project:

```kotlin
dependencies {
implementation("io.ktor:ktor-client-opentelemetry:$ktor_version")
}
```

## Usage

### Basic setup

```kotlin
import io.ktor.client.plugins.opentelemetry.*
import io.opentelemetry.sdk.OpenTelemetrySdk

val openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(sdkTracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.build()

val client = HttpClient(CIO) {
install(OpenTelemetry) {
openTelemetry = openTelemetry
}
}
```

### Individual component configuration

```kotlin
val client = HttpClient(CIO) {
install(OpenTelemetry) {
tracerProvider = sdkTracerProvider
meterProvider = sdkMeterProvider
propagators = W3CTraceContextPropagator.getInstance()
}
}
```

### Header capture

```kotlin
val client = HttpClient(CIO) {
install(OpenTelemetry) {
openTelemetry = otel
captureRequestHeaders("X-Request-ID", "X-Correlation-ID")
captureResponseHeaders("X-RateLimit-Remaining")
}
}
```

### Filtering

```kotlin
val client = HttpClient(CIO) {
install(OpenTelemetry) {
openTelemetry = otel
filter { request -> !request.url.encodedPath.startsWith("/internal") }
}
}
```

## Features

### Client spans

Creates a `CLIENT` span for each outgoing HTTP request with attributes:

| Attribute | Description |
|---|---|
| `http.request.method` | HTTP method (GET, POST, etc.) |
| `server.address` | Target server hostname |
| `server.port` | Target server port |
| `url.full` | Full request URL |
| `http.response.status_code` | Response status code |
| `error.type` | Exception class name or HTTP status code (on error) |

### Trace context injection

Automatically injects trace context headers into outgoing requests that pass the configured `filter`, using the configured `TextMapPropagator`. With W3C Trace Context (default), the `traceparent` and `tracestate` headers are added to each traced request.

### Server-client context propagation

When used together with [ktor-server-opentelemetry](../../../ktor-server/ktor-server-plugins/ktor-server-opentelemetry), trace context flows automatically from incoming server requests to outgoing client requests. The server plugin propagates the OTEL context through Kotlin coroutines, so client spans created inside a request handler automatically become children of the server span:

```kotlin
fun Application.module() {
install(io.ktor.server.plugins.opentelemetry.OpenTelemetry) {
openTelemetry = otel
}

val client = HttpClient(CIO) {
install(io.ktor.client.plugins.opentelemetry.OpenTelemetry) {
openTelemetry = otel
}
}

routing {
get("/api/users/{id}") {
// The client span for this call is automatically a child of the server span
val user = client.get("http://user-service/users/${call.parameters["id"]}")
call.respond(user.body())
}
}
}
```

This produces a distributed trace:

```text
[Server] GET /api/users/{id}
└── [Client] HTTP GET → user-service
```

### Metrics

| Metric | Type | Description |
|---|---|---|
| `http.client.request.duration` | Histogram (seconds) | Duration of HTTP client requests |

Metric attributes include `http.request.method`, `http.response.status_code`, and `server.address`.

### Error handling

- Network errors and exceptions are recorded on the span via `Span.recordException()`
- HTTP 4xx/5xx responses set the span status to `ERROR`
- The `error.type` attribute is set to the exception class name or HTTP status code
- Spans are always ended, even when exceptions occur, preventing span leaks
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public final class io/ktor/client/plugins/opentelemetry/OpenTelemetryClientConfig {
public fun <init> ()V
public final fun captureRequestHeaders ([Ljava/lang/String;)V
public final fun captureResponseHeaders ([Ljava/lang/String;)V
public final fun filter (Lkotlin/jvm/functions/Function1;)V
public final fun getInstrumentationName ()Ljava/lang/String;
public final fun getInstrumentationVersion ()Ljava/lang/String;
public final fun getMeterProvider ()Lio/opentelemetry/api/metrics/MeterProvider;
public final fun getOpenTelemetry ()Lio/opentelemetry/api/OpenTelemetry;
public final fun getPropagators ()Lio/opentelemetry/context/propagation/TextMapPropagator;
public final fun getTracerProvider ()Lio/opentelemetry/api/trace/TracerProvider;
public final fun setInstrumentationName (Ljava/lang/String;)V
public final fun setInstrumentationVersion (Ljava/lang/String;)V
public final fun setMeterProvider (Lio/opentelemetry/api/metrics/MeterProvider;)V
public final fun setOpenTelemetry (Lio/opentelemetry/api/OpenTelemetry;)V
public final fun setPropagators (Lio/opentelemetry/context/propagation/TextMapPropagator;)V
public final fun setTracerProvider (Lio/opentelemetry/api/trace/TracerProvider;)V
}

public final class io/ktor/client/plugins/opentelemetry/OpenTelemetryKt {
public static final fun getOpenTelemetry ()Lio/ktor/client/plugins/api/ClientPlugin;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

plugins {
id("ktorbuild.project.client-plugin")
}

kotlin {
sourceSets {
jvmMain.dependencies {
api(libs.opentelemetry.api)
}
jvmTest.dependencies {
implementation(libs.opentelemetry.sdk)
implementation(libs.opentelemetry.sdk.testing)
implementation(projects.ktorServerTestHost)
}
}
}
Loading
Loading