Skip to content

Open Telemetry Support for KTOR#5497

Open
Subhanshu20135 wants to merge 10 commits intoktorio:mainfrom
The-Developer-Diaries:feature/open-telemetry
Open

Open Telemetry Support for KTOR#5497
Subhanshu20135 wants to merge 10 commits intoktorio:mainfrom
The-Developer-Diaries:feature/open-telemetry

Conversation

@Subhanshu20135
Copy link
Copy Markdown

Add native OpenTelemetry integration for server and client

Summary

  • Introduces ktor-server-opentelemetry and ktor-client-opentelemetry modules providing first-class OpenTelemetry distributed tracing, metrics, and context propagation - filling the gap alongside existing Micrometer and Dropwizard integrations.
  • Server plugin creates SERVER spans for incoming requests with automatic route-aware naming, W3C Trace Context extraction, coroutine-safe context propagation, and HTTP semantic convention attributes/metrics.
  • Client plugin creates CLIENT spans for outgoing requests with automatic trace context injection into headers, enabling end-to-end distributed traces when both plugins are used together.

Motivation

Ktor has Micrometer and Dropwizard metrics plugins, but no native OpenTelemetry support. OpenTelemetry is the industry-standard, vendor-neutral observability framework for distributed tracing, metrics, and logging. Users currently must rely on the external opentelemetry-java-instrumentation agent or manually instrument their applications. A native plugin provides a better developer experience with Ktor-idiomatic configuration, proper coroutine context propagation, and route-aware span naming.

Usage

// Server
install(OpenTelemetry) {
    openTelemetry = sdkOpenTelemetry
    captureRequestHeaders("X-Request-ID")
}

// Client
val client = HttpClient(CIO) {
    install(OpenTelemetry) {
        openTelemetry = sdkOpenTelemetry
    }
}

Key design decisions

  • JVM-only modules - follows the same pattern as ktor-server-metrics-micrometer since the OpenTelemetry Java SDK is JVM-only
  • Dual configuration style - accepts either a full io.opentelemetry.api.OpenTelemetry instance or individual TracerProvider/MeterProvider/TextMapPropagator components
  • Coroutine context propagation - the server plugin uses a ThreadContextElement to propagate the OTEL Context across coroutine dispatches, so Context.current() works correctly inside handlers and client spans automatically become children of server spans
  • OpenTelemetry semantic conventions - attributes and metrics follow the stable HTTP semantic conventions (http.request.method, url.path, http.route, http.server.request.duration, etc.)
  • Existing patterns - plugin structure, hooks, and lifecycle follow the established MicrometerMetrics plugin pattern (Metrics hook, ResponseSent, CallFailed, RoutingRoot.RoutingCallStarted)

Modules added

Module Location Convention plugin
ktor-server-opentelemetry ktor-server/ktor-server-plugins/ktor-server-opentelemetry/ ktorbuild.project.server-plugin
ktor-client-opentelemetry ktor-client/ktor-client-plugins/ktor-client-opentelemetry/ ktorbuild.project.client-plugin

Dependencies added

  • io.opentelemetry:opentelemetry-api:1.54.0 (API dependency for both modules)
  • io.opentelemetry:opentelemetry-sdk:1.54.0 (test only)
  • io.opentelemetry:opentelemetry-sdk-testing:1.54.0 (test only)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds native OpenTelemetry support for Ktor: new server and client plugins with public APIs, JVM implementations, tests, and READMEs; registers two new Gradle subprojects and OpenTelemetry dependency entries; updates an IDE VCS mapping.

Changes

Cohort / File(s) Summary
Build & IDE
\.idea/vcs.xml, gradle/libs.versions.toml, settings.gradle.kts
Updated IDE VCS mapping; added opentelemetry = "1.54.0" and aliases (opentelemetry-api, opentelemetry-sdk, opentelemetry-sdk-testing); registered ktor-server-opentelemetry and ktor-client-opentelemetry subprojects.
Client: API, impl, docs, build & tests
ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md, ktor-client/.../api/ktor-client-opentelemetry.api, ktor-client/.../build.gradle.kts, ktor-client/.../jvm/src/.../OpenTelemetry.kt, ktor-client/.../jvm/test/.../OpenTelemetryTest.kt
Added public API (OpenTelemetryClientConfig, plugin entry), JVM implementation (CLIENT spans, context injection, header capture, metrics, filtering, error handling), README, build script, and JVM tests.
Server: API, impl, docs, build & tests
ktor-server/ktor-server-plugins/ktor-server-opentelemetry/README.md, ktor-server/.../api/ktor-server-opentelemetry.api, ktor-server/.../build.gradle.kts, ktor-server/.../jvm/src/.../OpenTelemetry.kt, ktor-server/.../jvm/test/.../OpenTelemetryTest.kt
Added public API (OpenTelemetryConfig, plugin entry), JVM implementation (SERVER spans, context extraction, coroutine context propagation, metrics, route/span naming, header capture, error handling), README, build script, and JVM tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • bjhham
  • e5l
  • osipxd
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main addition: OpenTelemetry support for Ktor. It directly reflects the primary change across the changeset.
Description check ✅ Passed The description comprehensively covers all required template sections (Subsystem, Motivation, Solution) with detailed context, design decisions, and module/dependency information.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
.idea/vcs.xml (1)

27-27: IDE configuration change unrelated to PR objectives.

This change to the VCS directory mapping appears to be an automatic IntelliJ IDEA update and is unrelated to the OpenTelemetry feature being added. Both $PROJECT_DIR$ and an empty string are functionally equivalent. Consider excluding this file from the PR to keep the changeset focused on the OpenTelemetry integration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.idea/vcs.xml at line 27, Remove the unrelated IDE VCS mapping update by
reverting the change to the <mapping> entry (the mapping directory="" vcs="Git"
line) so the file no longer contains the empty-directory mapping; either discard
the local .idea/vcs.xml change from the commit or restore the original mapping
value (e.g., $PROJECT_DIR$) and ensure .idea/vcs.xml is excluded from the PR
going forward (add to .gitignore or use git restore --staged/checkout to unstage
the file) so only OpenTelemetry-related changes remain.
ktor-server/ktor-server-plugins/ktor-server-opentelemetry/jvm/src/io/ktor/server/plugins/opentelemetry/OpenTelemetry.kt (1)

348-351: Exception recording may be bypassed when StatusPages handles errors.

When StatusPages is installed and handles an exception, it does not rethrow, so CallFailed won't fire and trace.throwable won't be set. The span will still be ended correctly in ResponseSent with the appropriate status code, but recordException won't capture the original exception details.

This is consistent with how other Ktor observability plugins behave. If full exception capture is required even with StatusPages, users would need to manually record exceptions in their StatusPages handlers. Consider documenting this behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-server/ktor-server-plugins/ktor-server-opentelemetry/jvm/src/io/ktor/server/plugins/opentelemetry/OpenTelemetry.kt`
around lines 348 - 351, The current on(CallFailed) handler sets
call.attributes.getOrNull(traceKey)?.throwable and rethrows, but when
StatusPages intercepts and does not rethrow CallFailed won't fire and
trace.throwable won't be set; update the documentation for the OpenTelemetry
plugin explaining this behavior (mention on(CallFailed) and
traceKey/trace.throwable) and instruct users to explicitly call recordException
or set call.attributes[traceKey].throwable from their StatusPages handlers if
they need exceptions captured; include a short note that ResponseSent still ends
the span and records status codes but not the original exception when
StatusPages suppresses it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt`:
- Around line 227-236: The exception path in the catch block ends the span but
never records the requestDuration metric; update the catch in OpenTelemetry.kt
(the catch handling span.recordException and span.end) to calculate the elapsed
time using the same start timestamp used on the success path (e.g., startTime or
startTimeNs) and call requestDuration.record with the computed duration and the
same attributes/tags used for successful requests so failed requests are
included in the histogram; do this before span.end so the metric and span share
the same timing/attributes.

In `@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md`:
- Line 53: The example documents capturing sensitive headers via
captureRequestHeaders("Authorization", "X-Api-Key"); update it to avoid
capturing credentials by removing "Authorization" (and any other auth tokens)
and instead show non-sensitive correlation headers such as "X-Request-ID" or
"Correlation-Id"; modify the README example that calls captureRequestHeaders to
only list safe headers and add a brief comment or note that Authorization and
API keys must not be exported to telemetry.
- Line 91: The README link to the server plugin is using a too-short relative
path ("../../ktor-server/ktor-server-plugins/ktor-server-opentelemetry"); update
that link to go up one more directory so it points to
"../../../ktor-server/ktor-server-plugins/ktor-server-opentelemetry" from
README.md in ktor-client-plugins/ktor-client-opentelemetry so the cross-module
link resolves correctly.

In `@ktor-server/ktor-server-plugins/ktor-server-opentelemetry/README.md`:
- Line 107: The README currently links the client plugin using the markdown link
text "[ktor-client-opentelemetry]" pointing to
"../../ktor-client/ktor-client-plugins/ktor-client-opentelemetry", which breaks
navigation; update that href to the correct relative path from this module's
README to the ktor-client-opentelemetry README (or use an
absolute/monorepo-rooted path) so the link resolves correctly, then verify the
link works in rendered docs; the target reference to change is the markdown link
[ktor-client-opentelemetry](../../ktor-client/ktor-client-plugins/ktor-client-opentelemetry).

---

Nitpick comments:
In @.idea/vcs.xml:
- Line 27: Remove the unrelated IDE VCS mapping update by reverting the change
to the <mapping> entry (the mapping directory="" vcs="Git" line) so the file no
longer contains the empty-directory mapping; either discard the local
.idea/vcs.xml change from the commit or restore the original mapping value
(e.g., $PROJECT_DIR$) and ensure .idea/vcs.xml is excluded from the PR going
forward (add to .gitignore or use git restore --staged/checkout to unstage the
file) so only OpenTelemetry-related changes remain.

In
`@ktor-server/ktor-server-plugins/ktor-server-opentelemetry/jvm/src/io/ktor/server/plugins/opentelemetry/OpenTelemetry.kt`:
- Around line 348-351: The current on(CallFailed) handler sets
call.attributes.getOrNull(traceKey)?.throwable and rethrows, but when
StatusPages intercepts and does not rethrow CallFailed won't fire and
trace.throwable won't be set; update the documentation for the OpenTelemetry
plugin explaining this behavior (mention on(CallFailed) and
traceKey/trace.throwable) and instruct users to explicitly call recordException
or set call.attributes[traceKey].throwable from their StatusPages handlers if
they need exceptions captured; include a short note that ResponseSent still ends
the span and records status codes but not the original exception when
StatusPages suppresses it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 509eec03-ae22-4108-b3b9-94a9f189c13e

📥 Commits

Reviewing files that changed from the base of the PR and between f70e63e and dc4d47e.

📒 Files selected for processing (15)
  • .idea/vcs.xml
  • gradle/libs.versions.toml
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/api/ktor-client-opentelemetry.api
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/api/ktor-client-opentelemetry.klib.api
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/build.gradle.kts
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/test/io/ktor/client/plugins/opentelemetry/OpenTelemetryTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-opentelemetry/README.md
  • ktor-server/ktor-server-plugins/ktor-server-opentelemetry/api/ktor-server-opentelemetry.api
  • ktor-server/ktor-server-plugins/ktor-server-opentelemetry/api/ktor-server-opentelemetry.klib.api
  • ktor-server/ktor-server-plugins/ktor-server-opentelemetry/build.gradle.kts
  • ktor-server/ktor-server-plugins/ktor-server-opentelemetry/jvm/src/io/ktor/server/plugins/opentelemetry/OpenTelemetry.kt
  • ktor-server/ktor-server-plugins/ktor-server-opentelemetry/jvm/test/io/ktor/server/plugins/opentelemetry/OpenTelemetryTest.kt
  • settings.gradle.kts

@osipxd osipxd self-assigned this Mar 30, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt`:
- Around line 116-119: RequestBuilderHeadersSetter currently uses
carrier?.header(key, value) which appends values; change the TextMapSetter.set
implementation to replace existing header values by calling
carrier?.headers?.set(key, value) (targeting HttpRequestBuilder.headers.set) so
trace headers (e.g., "traceparent"/"tracestate") are overwritten rather than
appended when builders are reused.
- Around line 193-246: The catch block for execute(request) currently treats all
Throwables the same and loses HTTP response details when a ResponseException is
thrown (expectSuccess=true); update the catch to detect
io.ktor.client.plugins.ResponseException (or cast when cause is that type),
extract call.response.status.value and call.response.headers.getAll(headerName)
for the same responseHeaders loop, set span attributes
"http.response.status_code" and "http.response.header.<name>" and apply the same
StatusCode.ERROR + error.type logic used for statusCode>=400 before ending the
span and recording requestDuration; ensure requestDuration attributes include
http.response.status_code when available and preserve existing behavior for
non-ResponseException throwables.

In `@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md`:
- Around line 117-120: Update the trace diagram code fence to include a language
tag (use "text") so markdownlint MD040 is satisfied; locate the diagram block
containing "[Server] GET /api/users/{id} └── [Client] HTTP GET  →  user-service"
in README.md and change the opening triple-backtick to ```text to keep the
README lint-clean.
- Around line 85-91: Update the README section "Trace context injection" to
clarify that headers are injected only for requests that are actually traced and
that pass the plugin's request filter; explicitly state that when the filter
predicate rejects a request the implementation returns before calling
propagator.inject(...), so headers like traceparent/tracestate are added only to
outgoing requests with active trace context (i.e., traced requests handled by
the plugin) and not literally every outgoing request.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5417fe0a-c7b0-446a-8434-d4a91e40940d

📥 Commits

Reviewing files that changed from the base of the PR and between dc4d47e and 8faf86c.

📒 Files selected for processing (3)
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt
  • ktor-server/ktor-server-plugins/ktor-server-opentelemetry/README.md

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md (1)

117-120: ⚠️ Potential issue | 🟡 Minor

Add a language tag to the trace diagram code fence.

The fenced block is still missing a language identifier (MD040). Use text to keep markdownlint clean.

Suggested fix
-```
+```text
 [Server] GET /api/users/{id}
   └── [Client] HTTP GET  →  user-service
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md around
lines 117 - 120, The fenced code block showing the trace diagram (the lines
starting with "[Server] GET /api/users/{id}" and "└── [Client] HTTP GET →
user-service") is missing a language tag; update the opening code fence from totext so the block becomes text ... to satisfy markdownlint MD040
and keep the diagram rendered as plain text.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md:

  • Around line 117-120: The fenced code block showing the trace diagram (the
    lines starting with "[Server] GET /api/users/{id}" and "└── [Client] HTTP GET →
    user-service") is missing a language tag; update the opening code fence from totext so the block becomes text ... to satisfy markdownlint MD040
    and keep the diagram rendered as plain text.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Path: .coderabbit.yaml

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `32728fc6-814d-4c66-babf-d303bb28b9d7`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8faf86cf0fde2914c347a39783b84cf4c5c054a1 and a2a4d3fceaccdebf0dca4b24cc684adc597fcedf.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt (1)

258-266: ⚠️ Potential issue | 🟡 Minor

Duration metric missing http.response.status_code when available from ResponseException.

When a ResponseException provides a status code (e.g., 4xx/5xx with expectSuccess = true), the error-path duration metric omits http.response.status_code, unlike the success path. This inconsistency affects metric cardinality and makes it harder to aggregate request duration by status code across both successful and failed requests.

🔧 Proposed fix to include status code when available
             val durationSeconds = (System.nanoTime() - startTime) / 1_000_000_000.0
-            requestDuration.record(
-                durationSeconds,
-                OtelAttributes.builder()
-                    .put("http.request.method", request.method.value)
-                    .put("server.address", request.url.host)
-                    .put("error.type", cause::class.qualifiedName ?: "unknown")
-                    .build()
-            )
+            val metricsBuilder = OtelAttributes.builder()
+                .put("http.request.method", request.method.value)
+                .put("server.address", request.url.host)
+                .put("error.type", cause::class.qualifiedName ?: "unknown")
+            if (statusCode != null) {
+                metricsBuilder.put("http.response.status_code", statusCode.toLong())
+            }
+            requestDuration.record(durationSeconds, metricsBuilder.build())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt`
around lines 258 - 266, The error-path records requestDuration without the
response status; update the block that builds OtelAttributes (in the
requestDuration.record call) to detect when cause is a ResponseException and, if
so, add "http.response.status_code" with the response status value to the
attributes; use a safe cast (cause as? ResponseException) to read
response.status.value and include it in the OtelAttributes.builder() exactly
like the success path so metrics include status_code for failed requests as
well.
🧹 Nitpick comments (1)
ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt (1)

234-255: Redundant error.type attribute setting.

Lines 234-237 set error.type unconditionally, but it's immediately overwritten at either line 240 (when statusCode != null) or lines 251-254 (when statusCode == null). The initial assignment is unnecessary.

♻️ Proposed simplification
             span.recordException(cause)
             span.setStatus(StatusCode.ERROR, cause.message ?: "")
-            span.setAttribute(
-                OtelAttributeKey.stringKey("error.type"),
-                cause::class.qualifiedName ?: "unknown"
-            )
             if (statusCode != null) {
                 span.setAttribute(OtelAttributeKey.longKey("http.response.status_code"), statusCode.toLong())
                 span.setAttribute(OtelAttributeKey.stringKey("error.type"), statusCode.toString())
                 for (headerName in responseHeaders) {
                     val values = response.headers.getAll(headerName)
                     if (!values.isNullOrEmpty()) {
                         span.setAttribute(
                             OtelAttributeKey.stringArrayKey("http.response.header.$headerName"),
                             values
                         )
                     }
                 }
             } else {
                 span.setAttribute(
                     OtelAttributeKey.stringKey("error.type"),
                     cause::class.qualifiedName ?: "unknown"
                 )
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt`
around lines 234 - 255, Remove the redundant unconditional span.setAttribute
call that sets "error.type" before the statusCode check: the initial assignment
(using cause::class.qualifiedName ?: "unknown") is overwritten by the branch
that sets "error.type" to statusCode.toString() when statusCode != null or the
same cause-based value in the else branch; keep the "error.type" assignments
only inside the two branches (the statusCode branch and the else branch) and
eliminate the prior unconditional call around
span/OtelAttributeKey.stringKey("error.type") to avoid duplicate writes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt`:
- Around line 258-266: The error-path records requestDuration without the
response status; update the block that builds OtelAttributes (in the
requestDuration.record call) to detect when cause is a ResponseException and, if
so, add "http.response.status_code" with the response status value to the
attributes; use a safe cast (cause as? ResponseException) to read
response.status.value and include it in the OtelAttributes.builder() exactly
like the success path so metrics include status_code for failed requests as
well.

---

Nitpick comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt`:
- Around line 234-255: Remove the redundant unconditional span.setAttribute call
that sets "error.type" before the statusCode check: the initial assignment
(using cause::class.qualifiedName ?: "unknown") is overwritten by the branch
that sets "error.type" to statusCode.toString() when statusCode != null or the
same cause-based value in the else branch; keep the "error.type" assignments
only inside the two branches (the statusCode branch and the else branch) and
eliminate the prior unconditional call around
span/OtelAttributeKey.stringKey("error.type") to avoid duplicate writes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b6a1963e-8949-4343-8ef5-ae366f7400ad

📥 Commits

Reviewing files that changed from the base of the PR and between a2a4d3f and 917c784.

📒 Files selected for processing (2)
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt
✅ Files skipped from review due to trivial changes (1)
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/README.md

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt (1)

258-265: ⚠️ Potential issue | 🟠 Major

Keep ResponseException failures queryable by status in the duration metric.

The span now records http.response.status_code, but this histogram branch still only gets error.type = ResponseException. For expectSuccess = true, that makes 4xx/5xx requests disappear from status-based http.client.request.duration breakdowns.

🔧 Suggested fix
                 requestDuration.record(
                     durationSeconds,
                     OtelAttributes.builder()
                         .put("http.request.method", request.method.value)
                         .put("server.address", request.url.host)
-                        .put("error.type", cause::class.qualifiedName ?: "unknown")
+                        .apply {
+                            if (response != null) {
+                                val statusCode = response.status.value
+                                put("http.response.status_code", statusCode.toLong())
+                                put("error.type", statusCode.toString())
+                            } else {
+                                put("error.type", cause::class.qualifiedName ?: "unknown")
+                            }
+                        }
                         .build()
                 )
#!/bin/bash
# Inspect the current ResponseException catch path and the validators that throw it.
sed -n '228,266p' \
  ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt

rg -n -C2 'ResponseException|ClientRequestException|ServerResponseException' \
  ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/DefaultResponseValidation.kt
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt`
around lines 258 - 265, The duration metric branch currently sets error.type to
the caught exception class (e.g., ResponseException) which hides response status
for expectSuccess paths; update the catch/record logic in OpenTelemetry.kt where
requestDuration.record(...) is called to detect ResponseException subclasses
(ClientRequestException, ServerResponseException) and instead set/also include
"http.response.status_code" (from the ResponseException.response.status.value)
in the OtelAttributes builder while preserving error.type when
non-ResponseException errors occur, so status-based breakdowns include 4xx/5xx
values; change the attribute population in the requestDuration.record call and
the surrounding exception handling that references ResponseException,
ClientRequestException, and ServerResponseException accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt`:
- Around line 229-243: The code incorrectly guards on statusCode but then
dereferences response; change the null-check to guard on response != null (or
check response first and then read response.status.value into statusCode inside
that block) so that when you iterate responseHeaders and access response.headers
you have a non-null Response; update the condition around the block that sets
the http.response.status_code and loops response.headers (referencing response,
statusCode, responseHeaders, span.setAttribute, OtelAttributeKey) to ensure
response is non-null before use.

---

Duplicate comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt`:
- Around line 258-265: The duration metric branch currently sets error.type to
the caught exception class (e.g., ResponseException) which hides response status
for expectSuccess paths; update the catch/record logic in OpenTelemetry.kt where
requestDuration.record(...) is called to detect ResponseException subclasses
(ClientRequestException, ServerResponseException) and instead set/also include
"http.response.status_code" (from the ResponseException.response.status.value)
in the OtelAttributes builder while preserving error.type when
non-ResponseException errors occur, so status-based breakdowns include 4xx/5xx
values; change the attribute population in the requestDuration.record call and
the surrounding exception handling that references ResponseException,
ClientRequestException, and ServerResponseException accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8f27084d-8b1d-4a2d-9092-affc41832b3b

📥 Commits

Reviewing files that changed from the base of the PR and between 917c784 and e5d403c.

📒 Files selected for processing (1)
  • ktor-client/ktor-client-plugins/ktor-client-opentelemetry/jvm/src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt

Subhanshu20135 and others added 10 commits March 31, 2026 00:32
…src/io/ktor/client/plugins/opentelemetry/OpenTelemetry.kt

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ME.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ME.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ME.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ME.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…dant variable and streamline status code extraction.
@Subhanshu20135 Subhanshu20135 force-pushed the feature/open-telemetry branch from 4840a1c to d5aa626 Compare March 30, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants