diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md
index 36d5c334f1d6..74bea2ded278 100644
--- a/docs/supported-libraries.md
+++ b/docs/supported-libraries.md
@@ -92,7 +92,7 @@ These are the supported libraries and frameworks:
| [Jodd Http](https://http.jodd.org/) | 4.2+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | N/A | Controller Spans [3] |
| [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation |
-| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),
[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
+| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),
[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library),
[opentelemetry-ktor-3.0](../instrumentation/ktor/ktor-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [Kubernetes Client](https://github.com/kubernetes-client/java) | 7.0+ | N/A | [HTTP Client Spans] |
| [Lettuce](https://github.com/lettuce-io/lettuce-core) | 4.0+ | [opentelemetry-lettuce-5.1](../instrumentation/lettuce/lettuce-5.1/library) | [Database Client Spans] |
| [Log4j 1](https://logging.apache.org/log4j/1.2/) | 1.2+ | N/A | none |
diff --git a/instrumentation/ktor/ktor-1.0/library/README.md b/instrumentation/ktor/ktor-1.0/library/README.md
index 9fa477f1d825..2701fd8c5125 100644
--- a/instrumentation/ktor/ktor-1.0/library/README.md
+++ b/instrumentation/ktor/ktor-1.0/library/README.md
@@ -1,4 +1,4 @@
-# Library Instrumentation for Ktor versions 1.x
+# Library Instrumentation for Ktor version 1.x
This package contains libraries to help instrument Ktor. Currently, only server instrumentation is supported.
diff --git a/instrumentation/ktor/ktor-2-common/library/build.gradle.kts b/instrumentation/ktor/ktor-2-common/library/build.gradle.kts
new file mode 100644
index 000000000000..03206ba46b59
--- /dev/null
+++ b/instrumentation/ktor/ktor-2-common/library/build.gradle.kts
@@ -0,0 +1,22 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+
+plugins {
+ id("otel.library-instrumentation")
+ id("org.jetbrains.kotlin.jvm")
+}
+dependencies {
+ implementation(project(":instrumentation:ktor:ktor-common:library"))
+ implementation("io.opentelemetry:opentelemetry-extension-kotlin")
+ compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+ compileOnly("io.ktor:ktor-client-core:2.0.0")
+ compileOnly("io.ktor:ktor-server-core:2.0.0")
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ @Suppress("deprecation")
+ languageVersion.set(KotlinVersion.KOTLIN_1_4)
+ }
+}
diff --git a/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/AbstractKtorClientTracing.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/AbstractKtorClientTracing.kt
new file mode 100644
index 000000000000..925cb6cf3156
--- /dev/null
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/AbstractKtorClientTracing.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.client
+
+import io.ktor.client.call.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.opentelemetry.context.Context
+import io.opentelemetry.context.propagation.ContextPropagators
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
+
+abstract class AbstractKtorClientTracing(
+ private val instrumenter: Instrumenter,
+ private val propagators: ContextPropagators,
+) {
+
+ internal fun createSpan(requestBuilder: HttpRequestBuilder): Context? {
+ val parentContext = Context.current()
+ val requestData = requestBuilder.build()
+
+ return if (instrumenter.shouldStart(parentContext, requestData)) {
+ instrumenter.start(parentContext, requestData)
+ } else {
+ null
+ }
+ }
+
+ internal fun populateRequestHeaders(requestBuilder: HttpRequestBuilder, context: Context) {
+ propagators.textMapPropagator.inject(context, requestBuilder, KtorHttpHeadersSetter)
+ }
+
+ internal fun endSpan(context: Context, call: HttpClientCall, error: Throwable?) {
+ endSpan(context, HttpRequestBuilder().takeFrom(call.request), call.response, error)
+ }
+
+ internal fun endSpan(context: Context, requestBuilder: HttpRequestBuilder, response: HttpResponse?, error: Throwable?) {
+ instrumenter.end(context, requestBuilder.build(), response, error)
+ }
+}
diff --git a/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/AbstractKtorClientTracingBuilder.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/AbstractKtorClientTracingBuilder.kt
new file mode 100644
index 000000000000..3b99bba1f6af
--- /dev/null
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/AbstractKtorClientTracingBuilder.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.client
+
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.opentelemetry.api.OpenTelemetry
+import io.opentelemetry.api.common.AttributesBuilder
+import io.opentelemetry.context.Context
+import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
+import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
+import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil
+
+abstract class AbstractKtorClientTracingBuilder(
+ private val instrumentationName: String
+) {
+ companion object {
+ init {
+ KtorBuilderUtil.clientBuilderExtractor = { it.clientBuilder }
+ }
+ }
+
+ internal lateinit var openTelemetry: OpenTelemetry
+ protected lateinit var clientBuilder: DefaultHttpClientInstrumenterBuilder
+
+ fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
+ this.openTelemetry = openTelemetry
+ this.clientBuilder = DefaultHttpClientInstrumenterBuilder.create(
+ instrumentationName,
+ openTelemetry,
+ KtorHttpClientAttributesGetter
+ )
+ }
+
+ protected fun getOpenTelemetry(): OpenTelemetry {
+ return openTelemetry
+ }
+
+ @Deprecated(
+ "Please use method `capturedRequestHeaders`",
+ ReplaceWith("capturedRequestHeaders(headers.asIterable())")
+ )
+ fun setCapturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
+
+ @Deprecated(
+ "Please use method `capturedRequestHeaders`",
+ ReplaceWith("capturedRequestHeaders(headers)")
+ )
+ fun setCapturedRequestHeaders(headers: List) = capturedRequestHeaders(headers)
+
+ fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
+
+ fun capturedRequestHeaders(headers: Iterable) {
+ clientBuilder.setCapturedRequestHeaders(headers.toList())
+ }
+
+ @Deprecated(
+ "Please use method `capturedResponseHeaders`",
+ ReplaceWith("capturedResponseHeaders(headers.asIterable())")
+ )
+ fun setCapturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
+
+ @Deprecated(
+ "Please use method `capturedResponseHeaders`",
+ ReplaceWith("capturedResponseHeaders(headers)")
+ )
+ fun setCapturedResponseHeaders(headers: List) = capturedResponseHeaders(headers)
+
+ fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
+
+ fun capturedResponseHeaders(headers: Iterable) {
+ clientBuilder.setCapturedResponseHeaders(headers.toList())
+ }
+
+ @Deprecated(
+ "Please use method `knownMethods`",
+ ReplaceWith("knownMethods(knownMethods)")
+ )
+ fun setKnownMethods(knownMethods: Set) = knownMethods(knownMethods)
+
+ fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
+
+ fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
+
+ @JvmName("knownMethodsJvm")
+ fun knownMethods(methods: Iterable) = knownMethods(methods.map { it.value })
+
+ fun knownMethods(methods: Iterable) {
+ clientBuilder.setKnownMethods(methods.toSet())
+ }
+
+ @Deprecated("Please use method `attributeExtractor`")
+ fun addAttributesExtractors(vararg extractors: AttributesExtractor) = addAttributesExtractors(extractors.asList())
+
+ @Deprecated("Please use method `attributeExtractor`")
+ fun addAttributesExtractors(extractors: Iterable>) {
+ extractors.forEach {
+ attributeExtractor {
+ onStart { it.onStart(attributes, parentContext, request) }
+ onEnd { it.onEnd(attributes, parentContext, request, response, error) }
+ }
+ }
+ }
+
+ fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
+ val builder = ExtractorBuilder().apply(extractorBuilder).build()
+ this.clientBuilder.addAttributeExtractor(
+ object : AttributesExtractor {
+ override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: HttpRequestData) {
+ builder.onStart(OnStartData(attributes, parentContext, request))
+ }
+
+ override fun onEnd(attributes: AttributesBuilder, context: Context, request: HttpRequestData, response: HttpResponse?, error: Throwable?) {
+ builder.onEnd(OnEndData(attributes, context, request, response, error))
+ }
+ }
+ )
+ }
+
+ class ExtractorBuilder {
+ private var onStart: OnStartData.() -> Unit = {}
+ private var onEnd: OnEndData.() -> Unit = {}
+
+ fun onStart(block: OnStartData.() -> Unit) {
+ onStart = block
+ }
+
+ fun onEnd(block: OnEndData.() -> Unit) {
+ onEnd = block
+ }
+
+ internal fun build(): Extractor {
+ return Extractor(onStart, onEnd)
+ }
+ }
+
+ internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
+
+ data class OnStartData(
+ val attributes: AttributesBuilder,
+ val parentContext: Context,
+ val request: HttpRequestData
+ )
+
+ data class OnEndData(
+ val attributes: AttributesBuilder,
+ val parentContext: Context,
+ val request: HttpRequestData,
+ val response: HttpResponse?,
+ val error: Throwable?
+ )
+
+ /**
+ * Configures the instrumentation to emit experimental HTTP client metrics.
+ *
+ * @param emitExperimentalHttpClientMetrics `true` if the experimental HTTP client metrics are to be emitted.
+ */
+ @Deprecated("Please use method `emitExperimentalHttpClientMetrics`")
+ fun setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics: Boolean) {
+ if (emitExperimentalHttpClientMetrics) {
+ emitExperimentalHttpClientMetrics()
+ }
+ }
+
+ fun emitExperimentalHttpClientMetrics() {
+ clientBuilder.setEmitExperimentalHttpClientMetrics(true)
+ }
+}
diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/KtorHttpClientAttributesGetter.kt
similarity index 96%
rename from instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt
rename to instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/KtorHttpClientAttributesGetter.kt
index 62e61090b927..db1a9d1a0f0b 100644
--- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/KtorHttpClientAttributesGetter.kt
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.instrumentation.ktor.v2_0.client
+package io.opentelemetry.instrumentation.ktor.client
import io.ktor.client.request.*
import io.ktor.client.statement.*
diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpHeadersSetter.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/KtorHttpHeadersSetter.kt
similarity index 87%
rename from instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpHeadersSetter.kt
rename to instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/KtorHttpHeadersSetter.kt
index a98567c0f440..d6be22087a26 100644
--- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpHeadersSetter.kt
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/KtorHttpHeadersSetter.kt
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.instrumentation.ktor.v2_0.client
+package io.opentelemetry.instrumentation.ktor.client
import io.ktor.client.request.HttpRequestBuilder
import io.opentelemetry.context.propagation.TextMapSetter
diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/internal/KtorBuilderUtil.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorBuilderUtil.kt
similarity index 52%
rename from instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/internal/KtorBuilderUtil.kt
rename to instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorBuilderUtil.kt
index 10bb56d5a720..ec074cca66bd 100644
--- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/internal/KtorBuilderUtil.kt
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorBuilderUtil.kt
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.instrumentation.ktor.v2_0.internal
+package io.opentelemetry.instrumentation.ktor.internal
import io.ktor.client.request.*
import io.ktor.client.statement.*
@@ -11,14 +11,16 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder
-import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder
-import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing
+import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
+import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
object KtorBuilderUtil {
- lateinit var clientBuilderExtractor: (KtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder
- lateinit var serverBuilderExtractor: (KtorServerTracing.Configuration) -> DefaultHttpServerInstrumenterBuilder
+ lateinit var clientBuilderExtractor: (AbstractKtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder
+ lateinit var serverBuilderExtractor: (
+ AbstractKtorServerTracingBuilder
+ ) -> DefaultHttpServerInstrumenterBuilder
}
diff --git a/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorClientTracingUtil.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorClientTracingUtil.kt
new file mode 100644
index 000000000000..b1735217a99a
--- /dev/null
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorClientTracingUtil.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.internal
+
+import io.ktor.client.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.util.*
+import io.ktor.util.pipeline.*
+import io.opentelemetry.context.Context
+import io.opentelemetry.extension.kotlin.asContextElement
+import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount
+import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+object KtorClientTracingUtil {
+ private val openTelemetryContextKey = AttributeKey("OpenTelemetry")
+
+ fun install(plugin: AbstractKtorClientTracing, scope: HttpClient) {
+ installSpanCreation(plugin, scope)
+ installSpanEnd(plugin, scope)
+ }
+
+ private fun installSpanCreation(plugin: AbstractKtorClientTracing, scope: HttpClient) {
+ val initializeRequestPhase = PipelinePhase("OpenTelemetryInitializeRequest")
+ scope.requestPipeline.insertPhaseAfter(HttpRequestPipeline.State, initializeRequestPhase)
+
+ scope.requestPipeline.intercept(initializeRequestPhase) {
+ val openTelemetryContext = HttpClientRequestResendCount.initialize(Context.current())
+ withContext(openTelemetryContext.asContextElement()) { proceed() }
+ }
+
+ val createSpanPhase = PipelinePhase("OpenTelemetryCreateSpan")
+ scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, createSpanPhase)
+
+ scope.sendPipeline.intercept(createSpanPhase) {
+ val requestBuilder = context
+ val openTelemetryContext = plugin.createSpan(requestBuilder)
+
+ if (openTelemetryContext != null) {
+ try {
+ requestBuilder.attributes.put(openTelemetryContextKey, openTelemetryContext)
+ plugin.populateRequestHeaders(requestBuilder, openTelemetryContext)
+
+ withContext(openTelemetryContext.asContextElement()) { proceed() }
+ } catch (e: Throwable) {
+ plugin.endSpan(openTelemetryContext, requestBuilder, null, e)
+ throw e
+ }
+ } else {
+ proceed()
+ }
+ }
+ }
+
+ @OptIn(InternalCoroutinesApi::class)
+ private fun installSpanEnd(plugin: AbstractKtorClientTracing, scope: HttpClient) {
+ val endSpanPhase = PipelinePhase("OpenTelemetryEndSpan")
+ scope.receivePipeline.insertPhaseBefore(HttpReceivePipeline.State, endSpanPhase)
+
+ scope.receivePipeline.intercept(endSpanPhase) {
+ val openTelemetryContext = it.call.attributes.getOrNull(openTelemetryContextKey)
+ openTelemetryContext ?: return@intercept
+
+ scope.launch {
+ val job = it.call.coroutineContext.job
+ job.join()
+ val cause = if (!job.isCancelled) {
+ null
+ } else {
+ kotlin.runCatching { job.getCancellationException() }.getOrNull()
+ }
+
+ plugin.endSpan(openTelemetryContext, it.call, cause)
+ }
+ }
+ }
+}
diff --git a/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorServerTracer.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorServerTracer.kt
new file mode 100644
index 000000000000..2226f4169a94
--- /dev/null
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorServerTracer.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.internal
+
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.opentelemetry.context.Context
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
+
+class KtorServerTracer(
+ private val instrumenter: Instrumenter,
+) {
+ fun start(call: ApplicationCall): Context? {
+ val parentContext = Context.current()
+ if (!instrumenter.shouldStart(parentContext, call.request)) {
+ return null
+ }
+
+ return instrumenter.start(parentContext, call.request)
+ }
+
+ fun end(context: Context, call: ApplicationCall, error: Throwable?) {
+ instrumenter.end(context, call.request, call.response, error)
+ }
+}
diff --git a/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorServerTracingUtil.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorServerTracingUtil.kt
new file mode 100644
index 000000000000..e5e3ea4fc908
--- /dev/null
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorServerTracingUtil.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.internal
+
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.util.*
+import io.ktor.util.pipeline.*
+import io.opentelemetry.context.Context
+import io.opentelemetry.extension.kotlin.asContextElement
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor
+import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil
+import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
+import io.opentelemetry.instrumentation.ktor.server.ApplicationRequestGetter
+import kotlinx.coroutines.withContext
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+object KtorServerTracingUtil {
+
+ fun configureTracing(builder: AbstractKtorServerTracingBuilder, application: Application) {
+ val contextKey = AttributeKey("OpenTelemetry")
+ val errorKey = AttributeKey("OpenTelemetryException")
+
+ val instrumenter = instrumenter(builder)
+ val tracer = KtorServerTracer(instrumenter)
+ val startPhase = PipelinePhase("OpenTelemetry")
+
+ application.insertPhaseBefore(ApplicationCallPipeline.Monitoring, startPhase)
+ application.intercept(startPhase) {
+ val context = tracer.start(call)
+
+ if (context != null) {
+ call.attributes.put(contextKey, context)
+ withContext(context.asContextElement()) {
+ try {
+ proceed()
+ } catch (err: Throwable) {
+ // Stash error for reporting later since need ktor to finish setting up the response
+ call.attributes.put(errorKey, err)
+ throw err
+ }
+ }
+ } else {
+ proceed()
+ }
+ }
+
+ val postSendPhase = PipelinePhase("OpenTelemetryPostSend")
+ application.sendPipeline.insertPhaseAfter(ApplicationSendPipeline.After, postSendPhase)
+ application.sendPipeline.intercept(postSendPhase) {
+ val context = call.attributes.getOrNull(contextKey)
+ if (context != null) {
+ var error: Throwable? = call.attributes.getOrNull(errorKey)
+ try {
+ proceed()
+ } catch (t: Throwable) {
+ error = t
+ throw t
+ } finally {
+ tracer.end(context, call, error)
+ }
+ } else {
+ proceed()
+ }
+ }
+ }
+
+ private fun instrumenter(builder: AbstractKtorServerTracingBuilder): Instrumenter {
+ return InstrumenterUtil.buildUpstreamInstrumenter(
+ builder.serverBuilder.instrumenterBuilder(),
+ ApplicationRequestGetter,
+ builder.spanKindExtractor(SpanKindExtractor.alwaysServer())
+ )
+ }
+}
diff --git a/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/server/AbstractKtorServerTracingBuilder.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/server/AbstractKtorServerTracingBuilder.kt
new file mode 100644
index 000000000000..65abba99460c
--- /dev/null
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/server/AbstractKtorServerTracingBuilder.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.server
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.opentelemetry.api.OpenTelemetry
+import io.opentelemetry.api.common.AttributesBuilder
+import io.opentelemetry.api.trace.SpanKind
+import io.opentelemetry.context.Context
+import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder
+import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor
+import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder
+import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor
+import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil
+
+abstract class AbstractKtorServerTracingBuilder(private val instrumentationName: String) {
+ companion object {
+ init {
+ KtorBuilderUtil.serverBuilderExtractor = { it.serverBuilder }
+ }
+ }
+
+ internal lateinit var serverBuilder: DefaultHttpServerInstrumenterBuilder
+
+ internal var spanKindExtractor:
+ (SpanKindExtractor) -> SpanKindExtractor = { a -> a }
+
+ fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
+ this.serverBuilder =
+ DefaultHttpServerInstrumenterBuilder.create(
+ instrumentationName,
+ openTelemetry,
+ KtorHttpServerAttributesGetter.INSTANCE
+ )
+ }
+
+ @Deprecated("Please use method `spanStatusExtractor`")
+ fun setStatusExtractor(
+ extractor: (SpanStatusExtractor) -> SpanStatusExtractor
+ ) {
+ spanStatusExtractor { prevStatusExtractor ->
+ extractor(prevStatusExtractor).extract(spanStatusBuilder, request, response, error)
+ }
+ }
+
+ fun spanStatusExtractor(extract: SpanStatusData.(SpanStatusExtractor) -> Unit) {
+ serverBuilder.setStatusExtractor { prevExtractor ->
+ SpanStatusExtractor { spanStatusBuilder: SpanStatusBuilder,
+ request: ApplicationRequest,
+ response: ApplicationResponse?,
+ throwable: Throwable? ->
+ extract(
+ SpanStatusData(spanStatusBuilder, request, response, throwable),
+ prevExtractor
+ )
+ }
+ }
+ }
+
+ data class SpanStatusData(
+ val spanStatusBuilder: SpanStatusBuilder,
+ val request: ApplicationRequest,
+ val response: ApplicationResponse?,
+ val error: Throwable?
+ )
+
+ @Deprecated("Please use method `spanKindExtractor`")
+ fun setSpanKindExtractor(extractor: (SpanKindExtractor) -> SpanKindExtractor) {
+ spanKindExtractor { prevSpanKindExtractor ->
+ extractor(prevSpanKindExtractor).extract(this)
+ }
+ }
+
+ fun spanKindExtractor(extract: ApplicationRequest.(SpanKindExtractor) -> SpanKind) {
+ spanKindExtractor = { prevExtractor ->
+ SpanKindExtractor { request: ApplicationRequest ->
+ extract(request, prevExtractor)
+ }
+ }
+ }
+
+ @Deprecated("Please use method `attributeExtractor`")
+ fun addAttributeExtractor(extractor: AttributesExtractor) {
+ attributeExtractor {
+ onStart {
+ extractor.onStart(attributes, parentContext, request)
+ }
+ onEnd {
+ extractor.onEnd(attributes, parentContext, request, response, error)
+ }
+ }
+ }
+
+ fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
+ val builder = ExtractorBuilder().apply(extractorBuilder).build()
+ serverBuilder.addAttributesExtractor(
+ object : AttributesExtractor {
+ override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: ApplicationRequest) {
+ builder.onStart(OnStartData(attributes, parentContext, request))
+ }
+
+ override fun onEnd(attributes: AttributesBuilder, context: Context, request: ApplicationRequest, response: ApplicationResponse?, error: Throwable?) {
+ builder.onEnd(OnEndData(attributes, context, request, response, error))
+ }
+ }
+ )
+ }
+
+ class ExtractorBuilder {
+ private var onStart: OnStartData.() -> Unit = {}
+ private var onEnd: OnEndData.() -> Unit = {}
+
+ fun onStart(block: OnStartData.() -> Unit) {
+ onStart = block
+ }
+
+ fun onEnd(block: OnEndData.() -> Unit) {
+ onEnd = block
+ }
+
+ internal fun build(): Extractor {
+ return Extractor(onStart, onEnd)
+ }
+ }
+
+ internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
+
+ data class OnStartData(
+ val attributes: AttributesBuilder,
+ val parentContext: Context,
+ val request: ApplicationRequest
+ )
+
+ data class OnEndData(
+ val attributes: AttributesBuilder,
+ val parentContext: Context,
+ val request: ApplicationRequest,
+ val response: ApplicationResponse?,
+ val error: Throwable?
+ )
+
+ @Deprecated(
+ "Please use method `capturedRequestHeaders`",
+ ReplaceWith("capturedRequestHeaders(headers)")
+ )
+ fun setCapturedRequestHeaders(headers: List) = capturedRequestHeaders(headers)
+
+ fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
+
+ fun capturedRequestHeaders(headers: Iterable) {
+ serverBuilder.setCapturedRequestHeaders(headers.toList())
+ }
+
+ @Deprecated(
+ "Please use method `capturedResponseHeaders`",
+ ReplaceWith("capturedResponseHeaders(headers)")
+ )
+ fun setCapturedResponseHeaders(headers: List) = capturedResponseHeaders(headers)
+
+ fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
+
+ fun capturedResponseHeaders(headers: Iterable) {
+ serverBuilder.setCapturedResponseHeaders(headers.toList())
+ }
+
+ @Deprecated(
+ "Please use method `knownMethods`",
+ ReplaceWith("knownMethods(knownMethods)")
+ )
+ fun setKnownMethods(knownMethods: Set) = knownMethods(knownMethods)
+
+ fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
+
+ fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
+
+ @JvmName("knownMethodsJvm")
+ fun knownMethods(methods: Iterable) = knownMethods(methods.map { it.value })
+
+ fun knownMethods(methods: Iterable) {
+ methods.toSet().apply {
+ serverBuilder.setKnownMethods(this)
+ }
+ }
+
+ /**
+ * {@link #setOpenTelemetry(OpenTelemetry)} sets the serverBuilder to a non-null value.
+ */
+ fun isOpenTelemetryInitialized(): Boolean = this::serverBuilder.isInitialized
+}
diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/ApplicationRequestGetter.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/server/ApplicationRequestGetter.kt
similarity index 89%
rename from instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/ApplicationRequestGetter.kt
rename to instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/server/ApplicationRequestGetter.kt
index 267ea53dd7cf..053436977ef2 100644
--- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/ApplicationRequestGetter.kt
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/server/ApplicationRequestGetter.kt
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.instrumentation.ktor.v2_0.server
+package io.opentelemetry.instrumentation.ktor.server
import io.ktor.server.request.*
import io.opentelemetry.context.propagation.TextMapGetter
diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/server/KtorHttpServerAttributesGetter.kt
similarity index 96%
rename from instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt
rename to instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/server/KtorHttpServerAttributesGetter.kt
index 49bdedf85d6d..9471aa44f64b 100644
--- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt
+++ b/instrumentation/ktor/ktor-2-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/server/KtorHttpServerAttributesGetter.kt
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.instrumentation.ktor.v2_0.server
+package io.opentelemetry.instrumentation.ktor.server
import io.ktor.server.plugins.*
import io.ktor.server.request.*
@@ -13,7 +13,7 @@ import io.opentelemetry.instrumentation.ktor.isIpAddress
internal enum class KtorHttpServerAttributesGetter :
HttpServerAttributesGetter {
- INSTANCE, ;
+ INSTANCE;
override fun getHttpRequestMethod(request: ApplicationRequest): String {
return request.httpMethod.value
diff --git a/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts b/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts
index f201b43e6c69..2c0398e73b8f 100644
--- a/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts
+++ b/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts
@@ -7,10 +7,22 @@ plugins {
muzzle {
pass {
- group.set("org.jetbrains.kotlinx")
+ group.set("io.ktor")
+ module.set("ktor-client-core")
+ versions.set("[2.0.0,3.0.0)")
+ assertInverse.set(true)
+ excludeInstrumentationName("ktor-server")
+ // missing dependencies
+ skip("1.1.0", "1.1.1", "1.1.5")
+ }
+ pass {
+ group.set("io.ktor")
module.set("ktor-server-core")
- versions.set("[2.0.0,)")
+ versions.set("[2.0.0,3.0.0)")
assertInverse.set(true)
+ excludeInstrumentationName("ktor-client")
+ // missing dependencies
+ skip("1.1.0", "1.1.1")
}
}
@@ -25,6 +37,7 @@ dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
+ testInstrumentation(project(":instrumentation:ktor:ktor-3.0:javaagent"))
testImplementation(project(":instrumentation:ktor:ktor-2.0:testing"))
testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java
index f1b54532f728..79fb525fdb7a 100644
--- a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java
+++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java
@@ -13,9 +13,9 @@
import io.ktor.client.HttpClientConfig;
import io.ktor.client.engine.HttpClientEngineConfig;
import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracing;
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder;
-import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil;
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorClientInstrumentationModule.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorClientInstrumentationModule.java
index 7893fc86f893..af4f2f5f14f2 100644
--- a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorClientInstrumentationModule.java
+++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorClientInstrumentationModule.java
@@ -5,12 +5,14 @@
package io.opentelemetry.javaagent.instrumentation.ktor.v2_0;
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Collections.singletonList;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;
+import net.bytebuddy.matcher.ElementMatcher;
@AutoService(InstrumentationModule.class)
public class KtorClientInstrumentationModule extends InstrumentationModule {
@@ -24,6 +26,12 @@ public boolean isHelperClass(String className) {
return className.startsWith("io.opentelemetry.extension.kotlin.");
}
+ @Override
+ public ElementMatcher.Junction classLoaderMatcher() {
+ // removed in ktor 3
+ return hasClassesNamed("io.ktor.client.engine.HttpClientJvmEngine");
+ }
+
@Override
public List typeInstrumentations() {
return singletonList(new HttpClientInstrumentation());
diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java
index 835e247f004b..214d94d80df0 100644
--- a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java
+++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java
@@ -11,8 +11,9 @@
import io.ktor.server.application.Application;
import io.ktor.server.application.ApplicationPluginKt;
import io.opentelemetry.api.GlobalOpenTelemetry;
-import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil;
-import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing;
+import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
+import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder;
+import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracingBuilderKt;
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
@@ -39,19 +40,18 @@ public static class ConstructorAdvice {
@Advice.OnMethodExit
public static void onExit(@Advice.FieldValue("_applicationInstance") Application application) {
- ApplicationPluginKt.install(application, KtorServerTracing.Feature, new SetupFunction());
+ ApplicationPluginKt.install(
+ application, KtorServerTracingBuilderKt.getKtorServerTracing(), new SetupFunction());
}
}
public static class SetupFunction
- implements Function1 {
+ implements Function1 {
@Override
- public Unit invoke(KtorServerTracing.Configuration configuration) {
- configuration.setOpenTelemetry(GlobalOpenTelemetry.get());
- KtorBuilderUtil.serverBuilderExtractor
- .invoke(configuration)
- .configure(AgentCommonConfig.get());
+ public Unit invoke(AbstractKtorServerTracingBuilder builder) {
+ builder.setOpenTelemetry(GlobalOpenTelemetry.get());
+ KtorBuilderUtil.serverBuilderExtractor.invoke(builder).configure(AgentCommonConfig.get());
return kotlin.Unit.INSTANCE;
}
}
diff --git a/instrumentation/ktor/ktor-2.0/library/README.md b/instrumentation/ktor/ktor-2.0/library/README.md
index 46e0be300abe..f9af95e58a5b 100644
--- a/instrumentation/ktor/ktor-2.0/library/README.md
+++ b/instrumentation/ktor/ktor-2.0/library/README.md
@@ -1,4 +1,4 @@
-# Library Instrumentation for Ktor version 2.0 and higher
+# Library Instrumentation for Ktor version 2.x
This package contains libraries to help instrument Ktor. Server and client instrumentations are supported.
diff --git a/instrumentation/ktor/ktor-2.0/library/build.gradle.kts b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts
index b7817a5866eb..45f9669717f4 100644
--- a/instrumentation/ktor/ktor-2.0/library/build.gradle.kts
+++ b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts
@@ -13,7 +13,7 @@ dependencies {
library("io.ktor:ktor-client-core:$ktorVersion")
library("io.ktor:ktor-server-core:$ktorVersion")
- implementation(project(":instrumentation:ktor:ktor-common:library"))
+ api(project(":instrumentation:ktor:ktor-2-common:library"))
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt
index 25ab52608eea..429e2ccdfe0c 100644
--- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt
+++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt
@@ -6,116 +6,28 @@
package io.opentelemetry.instrumentation.ktor.v2_0.client
import io.ktor.client.*
-import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.util.*
-import io.ktor.util.pipeline.*
-import io.opentelemetry.context.Context
import io.opentelemetry.context.propagation.ContextPropagators
-import io.opentelemetry.extension.kotlin.asContextElement
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
-import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount
-import kotlinx.coroutines.InternalCoroutinesApi
-import kotlinx.coroutines.job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
+import io.opentelemetry.instrumentation.ktor.internal.KtorClientTracingUtil
class KtorClientTracing internal constructor(
- private val instrumenter: Instrumenter,
- private val propagators: ContextPropagators,
-) {
-
- private fun createSpan(requestBuilder: HttpRequestBuilder): Context? {
- val parentContext = Context.current()
- val requestData = requestBuilder.build()
-
- return if (instrumenter.shouldStart(parentContext, requestData)) {
- instrumenter.start(parentContext, requestData)
- } else {
- null
- }
- }
-
- private fun populateRequestHeaders(requestBuilder: HttpRequestBuilder, context: Context) {
- propagators.textMapPropagator.inject(context, requestBuilder, KtorHttpHeadersSetter)
- }
-
- private fun endSpan(context: Context, call: HttpClientCall, error: Throwable?) {
- endSpan(context, HttpRequestBuilder().takeFrom(call.request), call.response, error)
- }
-
- private fun endSpan(context: Context, requestBuilder: HttpRequestBuilder, response: HttpResponse?, error: Throwable?) {
- instrumenter.end(context, requestBuilder.build(), response, error)
- }
+ instrumenter: Instrumenter,
+ propagators: ContextPropagators
+) : AbstractKtorClientTracing(instrumenter, propagators) {
companion object : HttpClientPlugin {
- private val openTelemetryContextKey = AttributeKey("OpenTelemetry")
-
override val key = AttributeKey("OpenTelemetry")
override fun prepare(block: KtorClientTracingBuilder.() -> Unit) = KtorClientTracingBuilder().apply(block).build()
override fun install(plugin: KtorClientTracing, scope: HttpClient) {
- installSpanCreation(plugin, scope)
- installSpanEnd(plugin, scope)
- }
-
- private fun installSpanCreation(plugin: KtorClientTracing, scope: HttpClient) {
- val initializeRequestPhase = PipelinePhase("OpenTelemetryInitializeRequest")
- scope.requestPipeline.insertPhaseAfter(HttpRequestPipeline.State, initializeRequestPhase)
-
- scope.requestPipeline.intercept(initializeRequestPhase) {
- val openTelemetryContext = HttpClientRequestResendCount.initialize(Context.current())
- withContext(openTelemetryContext.asContextElement()) { proceed() }
- }
-
- val createSpanPhase = PipelinePhase("OpenTelemetryCreateSpan")
- scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, createSpanPhase)
-
- scope.sendPipeline.intercept(createSpanPhase) {
- val requestBuilder = context
- val openTelemetryContext = plugin.createSpan(requestBuilder)
-
- if (openTelemetryContext != null) {
- try {
- requestBuilder.attributes.put(openTelemetryContextKey, openTelemetryContext)
- plugin.populateRequestHeaders(requestBuilder, openTelemetryContext)
-
- withContext(openTelemetryContext.asContextElement()) { proceed() }
- } catch (e: Throwable) {
- plugin.endSpan(openTelemetryContext, requestBuilder, null, e)
- throw e
- }
- } else {
- proceed()
- }
- }
- }
-
- @OptIn(InternalCoroutinesApi::class)
- private fun installSpanEnd(plugin: KtorClientTracing, scope: HttpClient) {
- val endSpanPhase = PipelinePhase("OpenTelemetryEndSpan")
- scope.receivePipeline.insertPhaseBefore(HttpReceivePipeline.State, endSpanPhase)
-
- scope.receivePipeline.intercept(endSpanPhase) {
- val openTelemetryContext = it.call.attributes.getOrNull(openTelemetryContextKey)
- openTelemetryContext ?: return@intercept
-
- scope.launch {
- val job = it.call.coroutineContext.job
- job.join()
- val cause = if (!job.isCancelled) {
- null
- } else {
- kotlin.runCatching { job.getCancellationException() }.getOrNull()
- }
-
- plugin.endSpan(openTelemetryContext, it.call, cause)
- }
- }
+ KtorClientTracingUtil.install(plugin, scope)
}
}
}
diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt
index f03128224b07..b93fb82f02da 100644
--- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt
+++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt
@@ -5,168 +5,13 @@
package io.opentelemetry.instrumentation.ktor.v2_0.client
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.opentelemetry.api.OpenTelemetry
-import io.opentelemetry.api.common.AttributesBuilder
-import io.opentelemetry.context.Context
-import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
-import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
+import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
-import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil
-class KtorClientTracingBuilder {
- companion object {
- init {
- KtorBuilderUtil.clientBuilderExtractor = { it.clientBuilder }
- }
- }
-
- private lateinit var openTelemetry: OpenTelemetry
- private lateinit var clientBuilder: DefaultHttpClientInstrumenterBuilder
-
- fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
- this.openTelemetry = openTelemetry
- this.clientBuilder = DefaultHttpClientInstrumenterBuilder.create(
- INSTRUMENTATION_NAME,
- openTelemetry,
- KtorHttpClientAttributesGetter
- )
- }
-
- @Deprecated(
- "Please use method `capturedRequestHeaders`",
- ReplaceWith("capturedRequestHeaders(headers.asIterable())")
- )
- fun setCapturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
-
- @Deprecated(
- "Please use method `capturedRequestHeaders`",
- ReplaceWith("capturedRequestHeaders(headers)")
- )
- fun setCapturedRequestHeaders(headers: List) = capturedRequestHeaders(headers)
-
- fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
-
- fun capturedRequestHeaders(headers: Iterable) {
- clientBuilder.setCapturedRequestHeaders(headers.toList())
- }
-
- @Deprecated(
- "Please use method `capturedResponseHeaders`",
- ReplaceWith("capturedResponseHeaders(headers.asIterable())")
- )
- fun setCapturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
-
- @Deprecated(
- "Please use method `capturedResponseHeaders`",
- ReplaceWith("capturedResponseHeaders(headers)")
- )
- fun setCapturedResponseHeaders(headers: List) = capturedResponseHeaders(headers)
-
- fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
-
- fun capturedResponseHeaders(headers: Iterable) {
- clientBuilder.setCapturedResponseHeaders(headers.toList())
- }
-
- @Deprecated(
- "Please use method `knownMethods`",
- ReplaceWith("knownMethods(knownMethods)")
- )
- fun setKnownMethods(knownMethods: Set) = knownMethods(knownMethods)
-
- fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
-
- fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
-
- @JvmName("knownMethodsJvm")
- fun knownMethods(methods: Iterable) = knownMethods(methods.map { it.value })
-
- fun knownMethods(methods: Iterable) {
- clientBuilder.setKnownMethods(methods.toSet())
- }
-
- @Deprecated("Please use method `attributeExtractor`")
- fun addAttributesExtractors(vararg extractors: AttributesExtractor) = addAttributesExtractors(extractors.asList())
-
- @Deprecated("Please use method `attributeExtractor`")
- fun addAttributesExtractors(extractors: Iterable>) {
- extractors.forEach {
- attributeExtractor {
- onStart { it.onStart(attributes, parentContext, request) }
- onEnd { it.onEnd(attributes, parentContext, request, response, error) }
- }
- }
- }
-
- fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
- val builder = ExtractorBuilder().apply(extractorBuilder).build()
- this.clientBuilder.addAttributeExtractor(
- object : AttributesExtractor {
- override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: HttpRequestData) {
- builder.onStart(OnStartData(attributes, parentContext, request))
- }
-
- override fun onEnd(attributes: AttributesBuilder, context: Context, request: HttpRequestData, response: HttpResponse?, error: Throwable?) {
- builder.onEnd(OnEndData(attributes, context, request, response, error))
- }
- }
- )
- }
-
- class ExtractorBuilder {
- private var onStart: OnStartData.() -> Unit = {}
- private var onEnd: OnEndData.() -> Unit = {}
-
- fun onStart(block: OnStartData.() -> Unit) {
- onStart = block
- }
-
- fun onEnd(block: OnEndData.() -> Unit) {
- onEnd = block
- }
-
- internal fun build(): Extractor {
- return Extractor(onStart, onEnd)
- }
- }
-
- internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
-
- data class OnStartData(
- val attributes: AttributesBuilder,
- val parentContext: Context,
- val request: HttpRequestData
- )
-
- data class OnEndData(
- val attributes: AttributesBuilder,
- val parentContext: Context,
- val request: HttpRequestData,
- val response: HttpResponse?,
- val error: Throwable?
- )
-
- /**
- * Configures the instrumentation to emit experimental HTTP client metrics.
- *
- * @param emitExperimentalHttpClientMetrics `true` if the experimental HTTP client metrics are to be emitted.
- */
- @Deprecated("Please use method `emitExperimentalHttpClientMetrics`")
- fun setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics: Boolean) {
- if (emitExperimentalHttpClientMetrics) {
- emitExperimentalHttpClientMetrics()
- }
- }
-
- fun emitExperimentalHttpClientMetrics() {
- clientBuilder.setEmitExperimentalHttpClientMetrics(true)
- }
+class KtorClientTracingBuilder : AbstractKtorClientTracingBuilder(INSTRUMENTATION_NAME) {
internal fun build(): KtorClientTracing = KtorClientTracing(
instrumenter = clientBuilder.build(),
- propagators = openTelemetry.propagators,
+ propagators = getOpenTelemetry().propagators,
)
}
diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt
deleted file mode 100644
index 66d0324a60e1..000000000000
--- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.instrumentation.ktor.v2_0.server
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import io.ktor.util.*
-import io.ktor.util.pipeline.*
-import io.opentelemetry.api.OpenTelemetry
-import io.opentelemetry.api.common.AttributesBuilder
-import io.opentelemetry.api.trace.SpanKind
-import io.opentelemetry.context.Context
-import io.opentelemetry.extension.kotlin.asContextElement
-import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder
-import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
-import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor
-import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder
-import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor
-import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil
-import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute
-import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource
-import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
-import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil
-import kotlinx.coroutines.withContext
-
-class KtorServerTracing private constructor(
- private val instrumenter: Instrumenter,
-) {
-
- class Configuration {
- companion object {
- init {
- KtorBuilderUtil.serverBuilderExtractor = { it.serverBuilder }
- }
- }
-
- internal lateinit var serverBuilder: DefaultHttpServerInstrumenterBuilder
-
- internal var spanKindExtractor:
- (SpanKindExtractor) -> SpanKindExtractor = { a -> a }
-
- fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
- this.serverBuilder =
- DefaultHttpServerInstrumenterBuilder.create(
- INSTRUMENTATION_NAME,
- openTelemetry,
- KtorHttpServerAttributesGetter.INSTANCE
- )
- }
-
- @Deprecated("Please use method `spanStatusExtractor`")
- fun setStatusExtractor(
- extractor: (SpanStatusExtractor) -> SpanStatusExtractor
- ) {
- spanStatusExtractor { prevStatusExtractor ->
- extractor(prevStatusExtractor).extract(spanStatusBuilder, request, response, error)
- }
- }
-
- fun spanStatusExtractor(extract: SpanStatusData.(SpanStatusExtractor) -> Unit) {
- serverBuilder.setStatusExtractor { prevExtractor ->
- SpanStatusExtractor { spanStatusBuilder: SpanStatusBuilder,
- request: ApplicationRequest,
- response: ApplicationResponse?,
- throwable: Throwable? ->
- extract(
- SpanStatusData(spanStatusBuilder, request, response, throwable),
- prevExtractor
- )
- }
- }
- }
-
- data class SpanStatusData(
- val spanStatusBuilder: SpanStatusBuilder,
- val request: ApplicationRequest,
- val response: ApplicationResponse?,
- val error: Throwable?
- )
-
- @Deprecated("Please use method `spanKindExtractor`")
- fun setSpanKindExtractor(extractor: (SpanKindExtractor) -> SpanKindExtractor) {
- spanKindExtractor { prevSpanKindExtractor ->
- extractor(prevSpanKindExtractor).extract(this)
- }
- }
-
- fun spanKindExtractor(extract: ApplicationRequest.(SpanKindExtractor) -> SpanKind) {
- spanKindExtractor = { prevExtractor ->
- SpanKindExtractor { request: ApplicationRequest ->
- extract(request, prevExtractor)
- }
- }
- }
-
- @Deprecated("Please use method `attributeExtractor`")
- fun addAttributeExtractor(extractor: AttributesExtractor) {
- attributeExtractor {
- onStart {
- extractor.onStart(attributes, parentContext, request)
- }
- onEnd {
- extractor.onEnd(attributes, parentContext, request, response, error)
- }
- }
- }
-
- fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
- val builder = ExtractorBuilder().apply(extractorBuilder).build()
- serverBuilder.addAttributesExtractor(
- object : AttributesExtractor {
- override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: ApplicationRequest) {
- builder.onStart(OnStartData(attributes, parentContext, request))
- }
-
- override fun onEnd(attributes: AttributesBuilder, context: Context, request: ApplicationRequest, response: ApplicationResponse?, error: Throwable?) {
- builder.onEnd(OnEndData(attributes, context, request, response, error))
- }
- }
- )
- }
-
- class ExtractorBuilder {
- private var onStart: OnStartData.() -> Unit = {}
- private var onEnd: OnEndData.() -> Unit = {}
-
- fun onStart(block: OnStartData.() -> Unit) {
- onStart = block
- }
-
- fun onEnd(block: OnEndData.() -> Unit) {
- onEnd = block
- }
-
- internal fun build(): Extractor {
- return Extractor(onStart, onEnd)
- }
- }
-
- internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
-
- data class OnStartData(
- val attributes: AttributesBuilder,
- val parentContext: Context,
- val request: ApplicationRequest
- )
-
- data class OnEndData(
- val attributes: AttributesBuilder,
- val parentContext: Context,
- val request: ApplicationRequest,
- val response: ApplicationResponse?,
- val error: Throwable?
- )
-
- @Deprecated(
- "Please use method `capturedRequestHeaders`",
- ReplaceWith("capturedRequestHeaders(headers)")
- )
- fun setCapturedRequestHeaders(headers: List) = capturedRequestHeaders(headers)
-
- fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
-
- fun capturedRequestHeaders(headers: Iterable) {
- serverBuilder.setCapturedRequestHeaders(headers.toList())
- }
-
- @Deprecated(
- "Please use method `capturedResponseHeaders`",
- ReplaceWith("capturedResponseHeaders(headers)")
- )
- fun setCapturedResponseHeaders(headers: List) = capturedResponseHeaders(headers)
-
- fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
-
- fun capturedResponseHeaders(headers: Iterable) {
- serverBuilder.setCapturedResponseHeaders(headers.toList())
- }
-
- @Deprecated(
- "Please use method `knownMethods`",
- ReplaceWith("knownMethods(knownMethods)")
- )
- fun setKnownMethods(knownMethods: Set) = knownMethods(knownMethods)
-
- fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
-
- fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
-
- @JvmName("knownMethodsJvm")
- fun knownMethods(methods: Iterable) = knownMethods(methods.map { it.value })
-
- fun knownMethods(methods: Iterable) {
- methods.toSet().apply {
- serverBuilder.setKnownMethods(this)
- }
- }
-
- /**
- * {@link #setOpenTelemetry(OpenTelemetry)} sets the serverBuilder to a non-null value.
- */
- internal fun isOpenTelemetryInitialized(): Boolean = this::serverBuilder.isInitialized
- }
-
- private fun start(call: ApplicationCall): Context? {
- val parentContext = Context.current()
- if (!instrumenter.shouldStart(parentContext, call.request)) {
- return null
- }
-
- return instrumenter.start(parentContext, call.request)
- }
-
- private fun end(context: Context, call: ApplicationCall, error: Throwable?) {
- instrumenter.end(context, call.request, call.response, error)
- }
-
- companion object Feature : BaseApplicationPlugin {
-
- private val contextKey = AttributeKey("OpenTelemetry")
- private val errorKey = AttributeKey("OpenTelemetryException")
-
- override val key: AttributeKey = AttributeKey("OpenTelemetry")
-
- override fun install(pipeline: Application, configure: Configuration.() -> Unit): KtorServerTracing {
- val configuration = Configuration().apply(configure)
-
- require(configuration.isOpenTelemetryInitialized()) { "OpenTelemetry must be set" }
-
- val instrumenter = InstrumenterUtil.buildUpstreamInstrumenter(
- configuration.serverBuilder.instrumenterBuilder(),
- ApplicationRequestGetter,
- configuration.spanKindExtractor(SpanKindExtractor.alwaysServer())
- )
-
- val feature = KtorServerTracing(instrumenter)
-
- val startPhase = PipelinePhase("OpenTelemetry")
- pipeline.insertPhaseBefore(ApplicationCallPipeline.Monitoring, startPhase)
- pipeline.intercept(startPhase) {
- val context = feature.start(call)
-
- if (context != null) {
- call.attributes.put(contextKey, context)
- withContext(context.asContextElement()) {
- try {
- proceed()
- } catch (err: Throwable) {
- // Stash error for reporting later since need ktor to finish setting up the response
- call.attributes.put(errorKey, err)
- throw err
- }
- }
- } else {
- proceed()
- }
- }
-
- val postSendPhase = PipelinePhase("OpenTelemetryPostSend")
- pipeline.sendPipeline.insertPhaseAfter(ApplicationSendPipeline.After, postSendPhase)
- pipeline.sendPipeline.intercept(postSendPhase) {
- val context = call.attributes.getOrNull(contextKey)
- if (context != null) {
- var error: Throwable? = call.attributes.getOrNull(errorKey)
- try {
- proceed()
- } catch (t: Throwable) {
- error = t
- throw t
- } finally {
- feature.end(context, call, error)
- }
- } else {
- proceed()
- }
- }
-
- pipeline.environment.monitor.subscribe(Routing.RoutingCallStarted) { call ->
- HttpServerRoute.update(Context.current(), HttpServerRouteSource.SERVER, { _, arg -> arg.route.parent.toString() }, call)
- }
-
- return feature
- }
- }
-}
diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracingBuilder.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracingBuilder.kt
new file mode 100644
index 000000000000..b2c4be9b86db
--- /dev/null
+++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracingBuilder.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v2_0.server
+
+import io.ktor.server.application.*
+import io.ktor.server.routing.*
+import io.opentelemetry.context.Context
+import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute
+import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource
+import io.opentelemetry.instrumentation.ktor.internal.KtorServerTracingUtil
+import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
+import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
+
+class KtorServerTracingBuilder internal constructor(
+ instrumentationName: String
+) : AbstractKtorServerTracingBuilder(instrumentationName)
+
+val KtorServerTracing = createRouteScopedPlugin("OpenTelemetry", { KtorServerTracingBuilder(INSTRUMENTATION_NAME) }) {
+ require(pluginConfig.isOpenTelemetryInitialized()) { "OpenTelemetry must be set" }
+
+ KtorServerTracingUtil.configureTracing(pluginConfig, application)
+
+ application.environment.monitor.subscribe(Routing.RoutingCallStarted) { call ->
+ HttpServerRoute.update(Context.current(), HttpServerRouteSource.SERVER, { _, arg -> arg.route.parent.toString() }, call)
+ }
+}
diff --git a/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt
index 5d7619dcf3a7..991e32f347f6 100644
--- a/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt
+++ b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt
@@ -18,6 +18,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES
import io.opentelemetry.semconv.NetworkAttributes
import kotlinx.coroutines.*
+import org.junit.jupiter.api.AfterAll
import java.net.URI
abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest() {
@@ -27,6 +28,19 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest.installTracing()
@@ -67,7 +81,7 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest
- KtorHttpClientSingleConnection(host, port) { installTracing() }
+ KtorHttpClientSingleConnection(singleConnectionClient, host, port)
}
}
}
diff --git a/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt
index 1a1f00a8fc58..2396d62bb6b1 100644
--- a/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt
+++ b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt
@@ -12,23 +12,11 @@ import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection
import kotlinx.coroutines.runBlocking
class KtorHttpClientSingleConnection(
+ private val client: HttpClient,
private val host: String,
- private val port: Int,
- private val installTracing: HttpClientConfig<*>.() -> Unit,
+ private val port: Int
) : SingleConnection {
- private val client: HttpClient
-
- init {
- val engine = CIO.create {
- maxConnectionsCount = 1
- }
-
- client = HttpClient(engine) {
- installTracing()
- }
- }
-
override fun doRequest(path: String, requestHeaders: MutableMap) = runBlocking {
val request = HttpRequestBuilder(
scheme = "http",
diff --git a/instrumentation/ktor/ktor-3.0/javaagent/build.gradle.kts b/instrumentation/ktor/ktor-3.0/javaagent/build.gradle.kts
new file mode 100644
index 000000000000..7f2aa66d5dcc
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/javaagent/build.gradle.kts
@@ -0,0 +1,56 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ id("org.jetbrains.kotlin.jvm")
+ id("otel.javaagent-instrumentation")
+}
+
+muzzle {
+ pass {
+ group.set("io.ktor")
+ module.set("ktor-client-core")
+ versions.set("[3.0.0,)")
+ assertInverse.set(true)
+ excludeInstrumentationName("ktor-server")
+ // missing dependencies
+ skip("1.1.0", "1.1.1", "1.1.5")
+ }
+ pass {
+ group.set("io.ktor")
+ module.set("ktor-server-core")
+ versions.set("[3.0.0,)")
+ assertInverse.set(true)
+ excludeInstrumentationName("ktor-client")
+ // missing dependencies
+ skip("1.1.0", "1.1.1")
+ }
+}
+
+val ktorVersion = "3.0.0"
+
+dependencies {
+ library("io.ktor:ktor-client-core:$ktorVersion")
+ library("io.ktor:ktor-server-core:$ktorVersion")
+
+ implementation(project(":instrumentation:ktor:ktor-3.0:library"))
+
+ compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+
+ testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
+ testInstrumentation(project(":instrumentation:ktor:ktor-2.0:javaagent"))
+
+ testImplementation(project(":instrumentation:ktor:ktor-3.0:testing"))
+ testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+ testImplementation("io.opentelemetry:opentelemetry-extension-kotlin")
+
+ testLibrary("io.ktor:ktor-server-netty:$ktorVersion")
+ testLibrary("io.ktor:ktor-client-cio:$ktorVersion")
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ // generate metadata for Java 1.8 reflection on method parameters, used in @WithSpan tests
+ javaParameters = true
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/HttpClientInstrumentation.java b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/HttpClientInstrumentation.java
new file mode 100644
index 000000000000..09dfcca67e20
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/HttpClientInstrumentation.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.ktor.v3_0;
+
+import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.ktor.client.HttpClientConfig;
+import io.ktor.client.engine.HttpClientEngineConfig;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
+import io.opentelemetry.instrumentation.ktor.v3_0.client.KtorClientTracing;
+import io.opentelemetry.instrumentation.ktor.v3_0.client.KtorClientTracingBuilder;
+import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class HttpClientInstrumentation implements TypeInstrumentation {
+ @Override
+ public ElementMatcher typeMatcher() {
+ return named("io.ktor.client.HttpClient");
+ }
+
+ @Override
+ public void transform(TypeTransformer transformer) {
+ transformer.applyAdviceToMethod(
+ isConstructor()
+ .and(takesArguments(2))
+ .and(takesArgument(1, named("io.ktor.client.HttpClientConfig"))),
+ this.getClass().getName() + "$ConstructorAdvice");
+ }
+
+ @SuppressWarnings("unused")
+ public static class ConstructorAdvice {
+
+ @Advice.OnMethodEnter
+ public static void onEnter(
+ @Advice.Argument(1) HttpClientConfig httpClientConfig) {
+ httpClientConfig.install(KtorClientTracing.Companion, new SetupFunction());
+ }
+ }
+
+ public static class SetupFunction implements Function1 {
+
+ @Override
+ public Unit invoke(KtorClientTracingBuilder builder) {
+ builder.setOpenTelemetry(GlobalOpenTelemetry.get());
+ KtorBuilderUtil.clientBuilderExtractor.invoke(builder).configure(AgentCommonConfig.get());
+ return Unit.INSTANCE;
+ }
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/KtorClientInstrumentationModule.java b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/KtorClientInstrumentationModule.java
new file mode 100644
index 000000000000..666a83d8e59d
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/KtorClientInstrumentationModule.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.ktor.v3_0;
+
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+import static java.util.Collections.singletonList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import java.util.List;
+import net.bytebuddy.matcher.ElementMatcher;
+
+@AutoService(InstrumentationModule.class)
+public class KtorClientInstrumentationModule extends InstrumentationModule {
+
+ public KtorClientInstrumentationModule() {
+ super("ktor", "ktor-client", "ktor-3.0", "ktor-client-3.0");
+ }
+
+ @Override
+ public boolean isHelperClass(String className) {
+ return className.startsWith("io.opentelemetry.extension.kotlin.");
+ }
+
+ @Override
+ public ElementMatcher.Junction classLoaderMatcher() {
+ // added in ktor 3
+ return hasClassesNamed("io.ktor.client.content.ProgressListener");
+ }
+
+ @Override
+ public List typeInstrumentations() {
+ return singletonList(new HttpClientInstrumentation());
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/KtorServerInstrumentationModule.java b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/KtorServerInstrumentationModule.java
new file mode 100644
index 000000000000..11152c53526d
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/KtorServerInstrumentationModule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.ktor.v3_0;
+
+import static java.util.Collections.singletonList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import java.util.List;
+
+@AutoService(InstrumentationModule.class)
+public class KtorServerInstrumentationModule extends InstrumentationModule {
+
+ public KtorServerInstrumentationModule() {
+ super("ktor", "ktor-server", "ktor-3.0", "ktor-server-3.0");
+ }
+
+ @Override
+ public boolean isHelperClass(String className) {
+ return className.startsWith("io.opentelemetry.extension.kotlin.");
+ }
+
+ @Override
+ public List typeInstrumentations() {
+ return singletonList(new ServerInstrumentation());
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/ServerInstrumentation.java b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/ServerInstrumentation.java
new file mode 100644
index 000000000000..df4335fa686d
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/ServerInstrumentation.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.ktor.v3_0;
+
+import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import io.ktor.server.application.Application;
+import io.ktor.server.application.ApplicationPluginKt;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
+import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder;
+import io.opentelemetry.instrumentation.ktor.v3_0.server.KtorServerTracingBuilderKt;
+import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class ServerInstrumentation implements TypeInstrumentation {
+ @Override
+ public ElementMatcher typeMatcher() {
+ return named("io.ktor.server.engine.EmbeddedServer");
+ }
+
+ @Override
+ public void transform(TypeTransformer transformer) {
+ transformer.applyAdviceToMethod(
+ isConstructor(), this.getClass().getName() + "$ConstructorAdvice");
+ }
+
+ @SuppressWarnings("unused")
+ public static class ConstructorAdvice {
+
+ @Advice.OnMethodExit
+ public static void onExit(@Advice.FieldValue("_applicationInstance") Application application) {
+ ApplicationPluginKt.install(
+ application, KtorServerTracingBuilderKt.getKtorServerTracing(), new SetupFunction());
+ }
+ }
+
+ public static class SetupFunction implements Function1 {
+
+ @Override
+ public Unit invoke(AbstractKtorServerTracingBuilder builder) {
+ builder.setOpenTelemetry(GlobalOpenTelemetry.get());
+ KtorBuilderUtil.serverBuilderExtractor.invoke(builder).configure(AgentCommonConfig.get());
+ return Unit.INSTANCE;
+ }
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientTest.kt b/instrumentation/ktor/ktor-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientTest.kt
new file mode 100644
index 000000000000..b9e3623e01a9
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientTest.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.client
+
+import io.ktor.client.*
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension
+import org.junit.jupiter.api.extension.RegisterExtension
+
+class KtorHttpClientTest : AbstractKtorHttpClientTest() {
+
+ companion object {
+ @JvmStatic
+ @RegisterExtension
+ private val TESTING = HttpClientInstrumentationExtension.forAgent()
+ }
+
+ override fun HttpClientConfig<*>.installTracing() {
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerTest.kt
new file mode 100644
index 000000000000..138a6911144d
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.server
+
+import io.ktor.server.application.*
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension
+import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension
+import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions
+import org.junit.jupiter.api.extension.RegisterExtension
+
+class KtorHttpServerTest : AbstractKtorHttpServerTest() {
+
+ companion object {
+ @JvmStatic
+ @RegisterExtension
+ val TESTING: InstrumentationExtension = HttpServerInstrumentationExtension.forAgent()
+ }
+
+ override fun getTesting(): InstrumentationExtension {
+ return TESTING
+ }
+
+ override fun installOpenTelemetry(application: Application) {
+ }
+
+ override fun configure(options: HttpServerTestOptions) {
+ super.configure(options)
+ options.setTestException(false)
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/library/README.md b/instrumentation/ktor/ktor-3.0/library/README.md
new file mode 100644
index 000000000000..ce6fe411b3cd
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/library/README.md
@@ -0,0 +1,60 @@
+# Library Instrumentation for Ktor version 3.0 and higher
+
+This package contains libraries to help instrument Ktor. Server and client instrumentations are supported.
+
+## Quickstart
+
+### Add these dependencies to your project
+
+Replace `OPENTELEMETRY_VERSION` with the [latest
+release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-ktor-3.0).
+
+For Maven, add to your `pom.xml` dependencies:
+
+```xml
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-ktor-3.0
+ OPENTELEMETRY_VERSION
+
+
+```
+
+For Gradle, add to your dependencies:
+
+```groovy
+implementation("io.opentelemetry.instrumentation:opentelemetry-ktor-3.0:OPENTELEMETRY_VERSION")
+```
+
+## Usage
+
+## Initializing server instrumentation
+
+Initialize instrumentation by installing the `KtorServerTracing` feature. You must set the `OpenTelemetry` to use with
+the feature.
+
+```kotlin
+val openTelemetry: OpenTelemetry = ...
+
+embeddedServer(Netty, 8080) {
+ install(KtorServerTracing) {
+ setOpenTelemetry(openTelemetry)
+ }
+}
+```
+
+## Initializing client instrumentation
+
+Initialize instrumentation by installing the `KtorClientTracing` feature. You must set the `OpenTelemetry` to use with
+the feature.
+
+```kotlin
+val openTelemetry: OpenTelemetry = ...
+
+val client = HttpClient {
+ install(KtorClientTracing) {
+ setOpenTelemetry(openTelemetry)
+ }
+}
+```
diff --git a/instrumentation/ktor/ktor-3.0/library/build.gradle.kts b/instrumentation/ktor/ktor-3.0/library/build.gradle.kts
new file mode 100644
index 000000000000..eb796b47455d
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/library/build.gradle.kts
@@ -0,0 +1,34 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+
+plugins {
+ id("otel.library-instrumentation")
+
+ id("org.jetbrains.kotlin.jvm")
+}
+
+val ktorVersion = "3.0.0"
+
+dependencies {
+ library("io.ktor:ktor-client-core:$ktorVersion")
+ library("io.ktor:ktor-server-core:$ktorVersion")
+
+ api(project(":instrumentation:ktor:ktor-2-common:library"))
+ implementation("io.opentelemetry:opentelemetry-extension-kotlin")
+
+ compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+
+ testImplementation(project(":instrumentation:ktor:ktor-3.0:testing"))
+ testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+
+ testLibrary("io.ktor:ktor-server-netty:$ktorVersion")
+ testLibrary("io.ktor:ktor-client-cio:$ktorVersion")
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ @Suppress("deprecation")
+ languageVersion.set(KotlinVersion.KOTLIN_1_6)
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/InstrumentationProperties.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/InstrumentationProperties.kt
new file mode 100644
index 000000000000..722f27ca21e7
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/InstrumentationProperties.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0
+
+/**
+ * Common properties for both client and server instrumentations
+ */
+internal object InstrumentationProperties {
+
+ internal const val INSTRUMENTATION_NAME = "io.opentelemetry.ktor-3.0"
+}
diff --git a/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracing.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracing.kt
new file mode 100644
index 000000000000..d5bf578cc56c
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracing.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.client
+
+import io.ktor.client.*
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.util.*
+import io.opentelemetry.context.propagation.ContextPropagators
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
+import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
+import io.opentelemetry.instrumentation.ktor.internal.KtorClientTracingUtil
+
+class KtorClientTracing internal constructor(
+ instrumenter: Instrumenter,
+ propagators: ContextPropagators
+) : AbstractKtorClientTracing(instrumenter, propagators) {
+
+ companion object : HttpClientPlugin {
+
+ override val key = AttributeKey("OpenTelemetry")
+
+ override fun prepare(block: KtorClientTracingBuilder.() -> Unit) = KtorClientTracingBuilder().apply(block).build()
+
+ override fun install(plugin: KtorClientTracing, scope: HttpClient) {
+ KtorClientTracingUtil.install(plugin, scope)
+ }
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracingBuilder.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracingBuilder.kt
new file mode 100644
index 000000000000..6f68939d9f06
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracingBuilder.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.client
+
+import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
+import io.opentelemetry.instrumentation.ktor.v3_0.InstrumentationProperties.INSTRUMENTATION_NAME
+
+class KtorClientTracingBuilder : AbstractKtorClientTracingBuilder(INSTRUMENTATION_NAME) {
+
+ internal fun build(): KtorClientTracing = KtorClientTracing(
+ instrumenter = clientBuilder.build(),
+ propagators = getOpenTelemetry().propagators,
+ )
+}
diff --git a/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerTracingBuilder.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerTracingBuilder.kt
new file mode 100644
index 000000000000..a086f7473fd8
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerTracingBuilder.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.server
+
+import io.ktor.server.application.*
+import io.ktor.server.routing.*
+import io.opentelemetry.context.Context
+import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute
+import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource
+import io.opentelemetry.instrumentation.ktor.internal.KtorServerTracingUtil
+import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
+import io.opentelemetry.instrumentation.ktor.v3_0.InstrumentationProperties.INSTRUMENTATION_NAME
+
+class KtorServerTracingBuilder internal constructor(
+ instrumentationName: String
+) : AbstractKtorServerTracingBuilder(instrumentationName)
+
+val KtorServerTracing = createRouteScopedPlugin("OpenTelemetry", { KtorServerTracingBuilder(INSTRUMENTATION_NAME) }) {
+ require(pluginConfig.isOpenTelemetryInitialized()) { "OpenTelemetry must be set" }
+
+ KtorServerTracingUtil.configureTracing(pluginConfig, application)
+
+ application.monitor.subscribe(RoutingRoot.RoutingCallStarted) { call ->
+ HttpServerRoute.update(Context.current(), HttpServerRouteSource.SERVER, { _, arg -> arg.route.parent.toString() }, call)
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientTest.kt b/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientTest.kt
new file mode 100644
index 000000000000..0249949e8ebb
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientTest.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.client
+
+import io.ktor.client.*
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension
+import org.junit.jupiter.api.extension.RegisterExtension
+
+class KtorHttpClientTest : AbstractKtorHttpClientTest() {
+
+ companion object {
+ @JvmStatic
+ @RegisterExtension
+ private val TESTING = HttpClientInstrumentationExtension.forLibrary()
+ }
+
+ override fun HttpClientConfig<*>.installTracing() {
+ install(KtorClientTracing) {
+ setOpenTelemetry(TESTING.openTelemetry)
+ capturedRequestHeaders(TEST_REQUEST_HEADER)
+ capturedResponseHeaders(TEST_RESPONSE_HEADER)
+ }
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerTest.kt
new file mode 100644
index 000000000000..61caec27af83
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.server
+
+import io.ktor.server.application.*
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension
+import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension
+import org.junit.jupiter.api.extension.RegisterExtension
+
+class KtorHttpServerTest : AbstractKtorHttpServerTest() {
+
+ companion object {
+ @JvmStatic
+ @RegisterExtension
+ val TESTING: InstrumentationExtension = HttpServerInstrumentationExtension.forLibrary()
+ }
+
+ override fun getTesting(): InstrumentationExtension {
+ return TESTING
+ }
+
+ override fun installOpenTelemetry(application: Application) {
+ application.apply {
+ install(KtorServerTracing) {
+ setOpenTelemetry(TESTING.openTelemetry)
+ capturedRequestHeaders(TEST_REQUEST_HEADER)
+ capturedResponseHeaders(TEST_RESPONSE_HEADER)
+ }
+ }
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/testing/build.gradle.kts b/instrumentation/ktor/ktor-3.0/testing/build.gradle.kts
new file mode 100644
index 000000000000..d3fb3f3d0acc
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/testing/build.gradle.kts
@@ -0,0 +1,28 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ id("otel.java-conventions")
+
+ id("org.jetbrains.kotlin.jvm")
+}
+
+val ktorVersion = "3.0.0"
+
+dependencies {
+ api(project(":testing-common"))
+
+ implementation("io.ktor:ktor-client-core:$ktorVersion")
+ implementation("io.ktor:ktor-server-core:$ktorVersion")
+
+ implementation("io.opentelemetry:opentelemetry-extension-kotlin")
+
+ compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+ compileOnly("io.ktor:ktor-server-netty:$ktorVersion")
+ compileOnly("io.ktor:ktor-client-cio:$ktorVersion")
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/AbstractKtorHttpClientTest.kt b/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/AbstractKtorHttpClientTest.kt
new file mode 100644
index 000000000000..2e698e34e461
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/AbstractKtorHttpClientTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.client
+
+import io.ktor.client.*
+import io.ktor.client.engine.cio.*
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.http.*
+import io.opentelemetry.context.Context
+import io.opentelemetry.extension.kotlin.asContextElement
+import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES
+import io.opentelemetry.semconv.NetworkAttributes
+import kotlinx.coroutines.*
+import org.junit.jupiter.api.AfterAll
+import java.net.URI
+
+abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest() {
+
+ private val client = HttpClient(CIO) {
+ install(HttpRedirect)
+
+ installTracing()
+ }
+ private val singleConnectionClient = HttpClient(CIO) {
+ engine {
+ maxConnectionsCount = 1
+ }
+
+ installTracing()
+ }
+
+ @AfterAll
+ fun tearDown() {
+ client.close()
+ singleConnectionClient.close()
+ }
+
+ abstract fun HttpClientConfig<*>.installTracing()
+
+ override fun buildRequest(requestMethod: String, uri: URI, requestHeaders: MutableMap) = HttpRequestBuilder(uri.toURL()).apply {
+ method = HttpMethod.parse(requestMethod)
+
+ requestHeaders.forEach { (header, value) -> headers.append(header, value) }
+ }
+
+ override fun sendRequest(request: HttpRequestBuilder, method: String, uri: URI, headers: MutableMap) = runBlocking {
+ client.request(request).status.value
+ }
+
+ override fun sendRequestWithCallback(
+ request: HttpRequestBuilder,
+ method: String,
+ uri: URI,
+ headers: MutableMap,
+ httpClientResult: HttpClientResult,
+ ) {
+ CoroutineScope(Dispatchers.Default + Context.current().asContextElement()).launch {
+ try {
+ val statusCode = client.request(request).status.value
+ httpClientResult.complete(statusCode)
+ } catch (e: Throwable) {
+ httpClientResult.complete(e)
+ }
+ }
+ }
+
+ override fun configure(optionsBuilder: HttpClientTestOptions.Builder) {
+ with(optionsBuilder) {
+ disableTestReadTimeout()
+ markAsLowLevelInstrumentation()
+ setMaxRedirects(20)
+ spanEndsAfterBody()
+
+ setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) }
+
+ setSingleConnectionFactory { host, port ->
+ KtorHttpClientSingleConnection(singleConnectionClient, host, port)
+ }
+ }
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientSingleConnection.kt b/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientSingleConnection.kt
new file mode 100644
index 000000000000..30290d4f1e0f
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientSingleConnection.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.client
+
+import io.ktor.client.*
+import io.ktor.client.engine.cio.*
+import io.ktor.client.request.*
+import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection
+import kotlinx.coroutines.runBlocking
+
+class KtorHttpClientSingleConnection(
+ private val client: HttpClient,
+ private val host: String,
+ private val port: Int
+) : SingleConnection {
+
+ override fun doRequest(path: String, requestHeaders: MutableMap) = runBlocking {
+ val request = HttpRequestBuilder(
+ scheme = "http",
+ host = host,
+ port = port,
+ path = path,
+ ).apply {
+ requestHeaders.forEach { (name, value) -> headers.append(name, value) }
+ }
+
+ client.request(request).status.value
+ }
+}
diff --git a/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/AbstractKtorHttpServerTest.kt b/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/AbstractKtorHttpServerTest.kt
new file mode 100644
index 000000000000..24d4eba8c5e4
--- /dev/null
+++ b/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/AbstractKtorHttpServerTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.ktor.v3_0.server
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.engine.*
+import io.ktor.server.netty.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import io.opentelemetry.api.trace.Span
+import io.opentelemetry.api.trace.SpanKind
+import io.opentelemetry.api.trace.StatusCode
+import io.opentelemetry.context.Context
+import io.opentelemetry.extension.kotlin.asContextElement
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension
+import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest
+import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions
+import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint
+import io.opentelemetry.semconv.ServerAttributes
+import kotlinx.coroutines.withContext
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+
+abstract class AbstractKtorHttpServerTest : AbstractHttpServerTest>() {
+
+ abstract fun getTesting(): InstrumentationExtension
+
+ abstract fun installOpenTelemetry(application: Application)
+
+ override fun setupServer(): EmbeddedServer<*, *> {
+ return embeddedServer(Netty, port = port) {
+ installOpenTelemetry(this)
+
+ routing {
+ get(ServerEndpoint.SUCCESS.path) {
+ controller(ServerEndpoint.SUCCESS) {
+ call.respondText(ServerEndpoint.SUCCESS.body, status = HttpStatusCode.fromValue(ServerEndpoint.SUCCESS.status))
+ }
+ }
+
+ get(ServerEndpoint.REDIRECT.path) {
+ controller(ServerEndpoint.REDIRECT) {
+ call.respondRedirect(ServerEndpoint.REDIRECT.body)
+ }
+ }
+
+ get(ServerEndpoint.ERROR.path) {
+ controller(ServerEndpoint.ERROR) {
+ call.respondText(ServerEndpoint.ERROR.body, status = HttpStatusCode.fromValue(ServerEndpoint.ERROR.status))
+ }
+ }
+
+ get(ServerEndpoint.EXCEPTION.path) {
+ controller(ServerEndpoint.EXCEPTION) {
+ throw IllegalStateException(ServerEndpoint.EXCEPTION.body)
+ }
+ }
+
+ get("/query") {
+ controller(ServerEndpoint.QUERY_PARAM) {
+ call.respondText("some=${call.request.queryParameters["some"]}", status = HttpStatusCode.fromValue(ServerEndpoint.QUERY_PARAM.status))
+ }
+ }
+
+ get("/path/{id}/param") {
+ controller(ServerEndpoint.PATH_PARAM) {
+ call.respondText(
+ call.parameters["id"]
+ ?: "",
+ status = HttpStatusCode.fromValue(ServerEndpoint.PATH_PARAM.status),
+ )
+ }
+ }
+
+ get("/child") {
+ controller(ServerEndpoint.INDEXED_CHILD) {
+ ServerEndpoint.INDEXED_CHILD.collectSpanAttributes { call.request.queryParameters[it] }
+ call.respondText(ServerEndpoint.INDEXED_CHILD.body, status = HttpStatusCode.fromValue(ServerEndpoint.INDEXED_CHILD.status))
+ }
+ }
+
+ get("/captureHeaders") {
+ controller(ServerEndpoint.CAPTURE_HEADERS) {
+ call.response.header("X-Test-Response", call.request.header("X-Test-Request") ?: "")
+ call.respondText(ServerEndpoint.CAPTURE_HEADERS.body, status = HttpStatusCode.fromValue(ServerEndpoint.CAPTURE_HEADERS.status))
+ }
+ }
+ }
+ }.start()
+ }
+
+ override fun stopServer(server: EmbeddedServer<*, *>) {
+ server.stop(0, 10, TimeUnit.SECONDS)
+ }
+
+ // Copy in HttpServerTest.controller but make it a suspending function
+ private suspend fun controller(endpoint: ServerEndpoint, wrapped: suspend () -> Unit) {
+ assert(Span.current().spanContext.isValid, { "Controller should have a parent span. " })
+ if (endpoint == ServerEndpoint.NOT_FOUND) {
+ wrapped()
+ }
+ val span = getTesting().openTelemetry.getTracer("test").spanBuilder("controller").setSpanKind(SpanKind.INTERNAL).startSpan()
+ try {
+ withContext(Context.current().with(span).asContextElement()) {
+ wrapped()
+ }
+ span.end()
+ } catch (e: Exception) {
+ span.setStatus(StatusCode.ERROR)
+ span.recordException(if (e is ExecutionException) e.cause ?: e else e)
+ span.end()
+ throw e
+ }
+ }
+
+ override fun configure(options: HttpServerTestOptions) {
+ options.setTestPathParam(true)
+
+ options.setHttpAttributes {
+ HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES - ServerAttributes.SERVER_PORT
+ }
+
+ options.setExpectedHttpRoute { endpoint, method ->
+ when (endpoint) {
+ ServerEndpoint.PATH_PARAM -> "/path/{id}/param"
+ else -> expectedHttpRoute(endpoint, method)
+ }
+ }
+
+ // ktor does not have a controller lifecycle so the server span ends immediately when the
+ // response is sent, which is before the controller span finishes.
+ options.setVerifyServerSpanEndTime(false)
+
+ options.setResponseCodeOnNonStandardHttpMethod(405)
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index c9e944fdb48c..8f897c9cade7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -402,6 +402,10 @@ include(":instrumentation:ktor:ktor-1.0:library")
include(":instrumentation:ktor:ktor-2.0:javaagent")
include(":instrumentation:ktor:ktor-2.0:library")
include(":instrumentation:ktor:ktor-2.0:testing")
+include(":instrumentation:ktor:ktor-2-common:library")
+include(":instrumentation:ktor:ktor-3.0:javaagent")
+include(":instrumentation:ktor:ktor-3.0:library")
+include(":instrumentation:ktor:ktor-3.0:testing")
include(":instrumentation:ktor:ktor-common:library")
include(":instrumentation:kubernetes-client-7.0:javaagent")
include(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests")