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")