Skip to content

Commit 0caa544

Browse files
lauritAlex Kats
authored and
Alex Kats
committed
Add ktor 3 instrumentation (open-telemetry#12562)
1 parent cfa1bf1 commit 0caa544

File tree

46 files changed

+1577
-585
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1577
-585
lines changed

docs/supported-libraries.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ These are the supported libraries and frameworks:
9292
| [Jodd Http](https://http.jodd.org/) | 4.2+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
9393
| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | N/A | Controller Spans [3] |
9494
| [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation |
95-
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
95+
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library),<br>[opentelemetry-ktor-3.0](../instrumentation/ktor/ktor-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
9696
| [Kubernetes Client](https://github.com/kubernetes-client/java) | 7.0+ | N/A | [HTTP Client Spans] |
9797
| [Lettuce](https://github.com/lettuce-io/lettuce-core) | 4.0+ | [opentelemetry-lettuce-5.1](../instrumentation/lettuce/lettuce-5.1/library) | [Database Client Spans] |
9898
| [Log4j 1](https://logging.apache.org/log4j/1.2/) | 1.2+ | N/A | none |

instrumentation/ktor/ktor-1.0/library/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Library Instrumentation for Ktor versions 1.x
1+
# Library Instrumentation for Ktor version 1.x
22

33
This package contains libraries to help instrument Ktor. Currently, only server instrumentation is supported.
44

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
3+
4+
plugins {
5+
id("otel.library-instrumentation")
6+
id("org.jetbrains.kotlin.jvm")
7+
}
8+
dependencies {
9+
implementation(project(":instrumentation:ktor:ktor-common:library"))
10+
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
11+
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
12+
compileOnly("io.ktor:ktor-client-core:2.0.0")
13+
compileOnly("io.ktor:ktor-server-core:2.0.0")
14+
}
15+
16+
kotlin {
17+
compilerOptions {
18+
jvmTarget.set(JvmTarget.JVM_1_8)
19+
@Suppress("deprecation")
20+
languageVersion.set(KotlinVersion.KOTLIN_1_4)
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.ktor.client
7+
8+
import io.ktor.client.call.*
9+
import io.ktor.client.request.*
10+
import io.ktor.client.statement.*
11+
import io.opentelemetry.context.Context
12+
import io.opentelemetry.context.propagation.ContextPropagators
13+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
14+
15+
abstract class AbstractKtorClientTracing(
16+
private val instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
17+
private val propagators: ContextPropagators,
18+
) {
19+
20+
internal fun createSpan(requestBuilder: HttpRequestBuilder): Context? {
21+
val parentContext = Context.current()
22+
val requestData = requestBuilder.build()
23+
24+
return if (instrumenter.shouldStart(parentContext, requestData)) {
25+
instrumenter.start(parentContext, requestData)
26+
} else {
27+
null
28+
}
29+
}
30+
31+
internal fun populateRequestHeaders(requestBuilder: HttpRequestBuilder, context: Context) {
32+
propagators.textMapPropagator.inject(context, requestBuilder, KtorHttpHeadersSetter)
33+
}
34+
35+
internal fun endSpan(context: Context, call: HttpClientCall, error: Throwable?) {
36+
endSpan(context, HttpRequestBuilder().takeFrom(call.request), call.response, error)
37+
}
38+
39+
internal fun endSpan(context: Context, requestBuilder: HttpRequestBuilder, response: HttpResponse?, error: Throwable?) {
40+
instrumenter.end(context, requestBuilder.build(), response, error)
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.ktor.client
7+
8+
import io.ktor.client.request.*
9+
import io.ktor.client.statement.*
10+
import io.ktor.http.*
11+
import io.opentelemetry.api.OpenTelemetry
12+
import io.opentelemetry.api.common.AttributesBuilder
13+
import io.opentelemetry.context.Context
14+
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
15+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
16+
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil
17+
18+
abstract class AbstractKtorClientTracingBuilder(
19+
private val instrumentationName: String
20+
) {
21+
companion object {
22+
init {
23+
KtorBuilderUtil.clientBuilderExtractor = { it.clientBuilder }
24+
}
25+
}
26+
27+
internal lateinit var openTelemetry: OpenTelemetry
28+
protected lateinit var clientBuilder: DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
29+
30+
fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
31+
this.openTelemetry = openTelemetry
32+
this.clientBuilder = DefaultHttpClientInstrumenterBuilder.create(
33+
instrumentationName,
34+
openTelemetry,
35+
KtorHttpClientAttributesGetter
36+
)
37+
}
38+
39+
protected fun getOpenTelemetry(): OpenTelemetry {
40+
return openTelemetry
41+
}
42+
43+
@Deprecated(
44+
"Please use method `capturedRequestHeaders`",
45+
ReplaceWith("capturedRequestHeaders(headers.asIterable())")
46+
)
47+
fun setCapturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
48+
49+
@Deprecated(
50+
"Please use method `capturedRequestHeaders`",
51+
ReplaceWith("capturedRequestHeaders(headers)")
52+
)
53+
fun setCapturedRequestHeaders(headers: List<String>) = capturedRequestHeaders(headers)
54+
55+
fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
56+
57+
fun capturedRequestHeaders(headers: Iterable<String>) {
58+
clientBuilder.setCapturedRequestHeaders(headers.toList())
59+
}
60+
61+
@Deprecated(
62+
"Please use method `capturedResponseHeaders`",
63+
ReplaceWith("capturedResponseHeaders(headers.asIterable())")
64+
)
65+
fun setCapturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
66+
67+
@Deprecated(
68+
"Please use method `capturedResponseHeaders`",
69+
ReplaceWith("capturedResponseHeaders(headers)")
70+
)
71+
fun setCapturedResponseHeaders(headers: List<String>) = capturedResponseHeaders(headers)
72+
73+
fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
74+
75+
fun capturedResponseHeaders(headers: Iterable<String>) {
76+
clientBuilder.setCapturedResponseHeaders(headers.toList())
77+
}
78+
79+
@Deprecated(
80+
"Please use method `knownMethods`",
81+
ReplaceWith("knownMethods(knownMethods)")
82+
)
83+
fun setKnownMethods(knownMethods: Set<String>) = knownMethods(knownMethods)
84+
85+
fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
86+
87+
fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
88+
89+
@JvmName("knownMethodsJvm")
90+
fun knownMethods(methods: Iterable<HttpMethod>) = knownMethods(methods.map { it.value })
91+
92+
fun knownMethods(methods: Iterable<String>) {
93+
clientBuilder.setKnownMethods(methods.toSet())
94+
}
95+
96+
@Deprecated("Please use method `attributeExtractor`")
97+
fun addAttributesExtractors(vararg extractors: AttributesExtractor<in HttpRequestData, in HttpResponse>) = addAttributesExtractors(extractors.asList())
98+
99+
@Deprecated("Please use method `attributeExtractor`")
100+
fun addAttributesExtractors(extractors: Iterable<AttributesExtractor<in HttpRequestData, in HttpResponse>>) {
101+
extractors.forEach {
102+
attributeExtractor {
103+
onStart { it.onStart(attributes, parentContext, request) }
104+
onEnd { it.onEnd(attributes, parentContext, request, response, error) }
105+
}
106+
}
107+
}
108+
109+
fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
110+
val builder = ExtractorBuilder().apply(extractorBuilder).build()
111+
this.clientBuilder.addAttributeExtractor(
112+
object : AttributesExtractor<HttpRequestData, HttpResponse> {
113+
override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: HttpRequestData) {
114+
builder.onStart(OnStartData(attributes, parentContext, request))
115+
}
116+
117+
override fun onEnd(attributes: AttributesBuilder, context: Context, request: HttpRequestData, response: HttpResponse?, error: Throwable?) {
118+
builder.onEnd(OnEndData(attributes, context, request, response, error))
119+
}
120+
}
121+
)
122+
}
123+
124+
class ExtractorBuilder {
125+
private var onStart: OnStartData.() -> Unit = {}
126+
private var onEnd: OnEndData.() -> Unit = {}
127+
128+
fun onStart(block: OnStartData.() -> Unit) {
129+
onStart = block
130+
}
131+
132+
fun onEnd(block: OnEndData.() -> Unit) {
133+
onEnd = block
134+
}
135+
136+
internal fun build(): Extractor {
137+
return Extractor(onStart, onEnd)
138+
}
139+
}
140+
141+
internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
142+
143+
data class OnStartData(
144+
val attributes: AttributesBuilder,
145+
val parentContext: Context,
146+
val request: HttpRequestData
147+
)
148+
149+
data class OnEndData(
150+
val attributes: AttributesBuilder,
151+
val parentContext: Context,
152+
val request: HttpRequestData,
153+
val response: HttpResponse?,
154+
val error: Throwable?
155+
)
156+
157+
/**
158+
* Configures the instrumentation to emit experimental HTTP client metrics.
159+
*
160+
* @param emitExperimentalHttpClientMetrics `true` if the experimental HTTP client metrics are to be emitted.
161+
*/
162+
@Deprecated("Please use method `emitExperimentalHttpClientMetrics`")
163+
fun setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics: Boolean) {
164+
if (emitExperimentalHttpClientMetrics) {
165+
emitExperimentalHttpClientMetrics()
166+
}
167+
}
168+
169+
fun emitExperimentalHttpClientMetrics() {
170+
clientBuilder.setEmitExperimentalHttpClientMetrics(true)
171+
}
172+
}
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
package io.opentelemetry.instrumentation.ktor.v2_0.client
6+
package io.opentelemetry.instrumentation.ktor.client
77

88
import io.ktor.client.request.*
99
import io.ktor.client.statement.*
+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
package io.opentelemetry.instrumentation.ktor.v2_0.client
6+
package io.opentelemetry.instrumentation.ktor.client
77

88
import io.ktor.client.request.HttpRequestBuilder
99
import io.opentelemetry.context.propagation.TextMapSetter
+7-5
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,24 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
package io.opentelemetry.instrumentation.ktor.v2_0.internal
6+
package io.opentelemetry.instrumentation.ktor.internal
77

88
import io.ktor.client.request.*
99
import io.ktor.client.statement.*
1010
import io.ktor.server.request.*
1111
import io.ktor.server.response.*
1212
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
1313
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder
14-
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder
15-
import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing
14+
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
15+
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
1616

1717
/**
1818
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
1919
* any time.
2020
*/
2121
object KtorBuilderUtil {
22-
lateinit var clientBuilderExtractor: (KtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
23-
lateinit var serverBuilderExtractor: (KtorServerTracing.Configuration) -> DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
22+
lateinit var clientBuilderExtractor: (AbstractKtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
23+
lateinit var serverBuilderExtractor: (
24+
AbstractKtorServerTracingBuilder
25+
) -> DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
2426
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.ktor.internal
7+
8+
import io.ktor.client.*
9+
import io.ktor.client.request.*
10+
import io.ktor.client.statement.*
11+
import io.ktor.util.*
12+
import io.ktor.util.pipeline.*
13+
import io.opentelemetry.context.Context
14+
import io.opentelemetry.extension.kotlin.asContextElement
15+
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount
16+
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
17+
import kotlinx.coroutines.InternalCoroutinesApi
18+
import kotlinx.coroutines.job
19+
import kotlinx.coroutines.launch
20+
import kotlinx.coroutines.withContext
21+
22+
/**
23+
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
24+
* any time.
25+
*/
26+
object KtorClientTracingUtil {
27+
private val openTelemetryContextKey = AttributeKey<Context>("OpenTelemetry")
28+
29+
fun install(plugin: AbstractKtorClientTracing, scope: HttpClient) {
30+
installSpanCreation(plugin, scope)
31+
installSpanEnd(plugin, scope)
32+
}
33+
34+
private fun installSpanCreation(plugin: AbstractKtorClientTracing, scope: HttpClient) {
35+
val initializeRequestPhase = PipelinePhase("OpenTelemetryInitializeRequest")
36+
scope.requestPipeline.insertPhaseAfter(HttpRequestPipeline.State, initializeRequestPhase)
37+
38+
scope.requestPipeline.intercept(initializeRequestPhase) {
39+
val openTelemetryContext = HttpClientRequestResendCount.initialize(Context.current())
40+
withContext(openTelemetryContext.asContextElement()) { proceed() }
41+
}
42+
43+
val createSpanPhase = PipelinePhase("OpenTelemetryCreateSpan")
44+
scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, createSpanPhase)
45+
46+
scope.sendPipeline.intercept(createSpanPhase) {
47+
val requestBuilder = context
48+
val openTelemetryContext = plugin.createSpan(requestBuilder)
49+
50+
if (openTelemetryContext != null) {
51+
try {
52+
requestBuilder.attributes.put(openTelemetryContextKey, openTelemetryContext)
53+
plugin.populateRequestHeaders(requestBuilder, openTelemetryContext)
54+
55+
withContext(openTelemetryContext.asContextElement()) { proceed() }
56+
} catch (e: Throwable) {
57+
plugin.endSpan(openTelemetryContext, requestBuilder, null, e)
58+
throw e
59+
}
60+
} else {
61+
proceed()
62+
}
63+
}
64+
}
65+
66+
@OptIn(InternalCoroutinesApi::class)
67+
private fun installSpanEnd(plugin: AbstractKtorClientTracing, scope: HttpClient) {
68+
val endSpanPhase = PipelinePhase("OpenTelemetryEndSpan")
69+
scope.receivePipeline.insertPhaseBefore(HttpReceivePipeline.State, endSpanPhase)
70+
71+
scope.receivePipeline.intercept(endSpanPhase) {
72+
val openTelemetryContext = it.call.attributes.getOrNull(openTelemetryContextKey)
73+
openTelemetryContext ?: return@intercept
74+
75+
scope.launch {
76+
val job = it.call.coroutineContext.job
77+
job.join()
78+
val cause = if (!job.isCancelled) {
79+
null
80+
} else {
81+
kotlin.runCatching { job.getCancellationException() }.getOrNull()
82+
}
83+
84+
plugin.endSpan(openTelemetryContext, it.call, cause)
85+
}
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)