Skip to content

Commit 76c2d2d

Browse files
authored
Add ktor 3 instrumentation (#12562)
1 parent fdfb764 commit 76c2d2d

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 22 additions & 0 deletions
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+
}
Lines changed: 1 addition & 1 deletion
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.*
Lines changed: 1 addition & 1 deletion
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
Lines changed: 7 additions & 5 deletions
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)