From 058d32dce49a39287f64789de04c6451cba7f654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alaksiej=20=C5=A0=C4=8Darbaty?= Date: Sat, 4 Mar 2023 20:53:38 +0100 Subject: [PATCH] Ktor client instrumentation --- .../ktor/ktor-2.0/library/README.md | 19 +++- .../ktor/ktor-2.0/library/build.gradle.kts | 8 +- .../ktor/v2_0/InstrumentationProperties.kt | 14 +++ .../ktor/v2_0/client/KtorClientTracing.kt | 107 ++++++++++++++++++ .../v2_0/client/KtorClientTracingBuilder.kt | 76 +++++++++++++ .../client/KtorHttpClientAttributesGetter.kt | 31 +++++ .../ktor/v2_0/client/KtorHttpHeadersSetter.kt | 16 +++ .../client/KtorNetClientAttributesGetter.kt | 20 ++++ .../{ => server}/ApplicationRequestGetter.kt | 2 +- .../KtorHttpServerAttributesGetter.kt | 2 +- .../KtorNetServerAttributesGetter.kt | 2 +- .../v2_0/{ => server}/KtorServerTracing.kt | 4 +- .../client/KtorHttpClientSingleConnection.kt | 44 +++++++ .../ktor/v2_0/client/KtorHttpClientTest.kt | 86 ++++++++++++++ .../v2_0/{ => server}/KtorHttpServerTest.kt | 2 +- .../ktor/v2_0/{ => server}/KtorTestUtil.kt | 2 +- 16 files changed, 424 insertions(+), 11 deletions(-) create mode 100644 instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/InstrumentationProperties.kt create mode 100644 instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt create mode 100644 instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt create mode 100644 instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt create mode 100644 instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpHeadersSetter.kt create mode 100644 instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorNetClientAttributesGetter.kt rename instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/{ => server}/ApplicationRequestGetter.kt (89%) rename instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/{ => server}/KtorHttpServerAttributesGetter.kt (96%) rename instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/{ => server}/KtorNetServerAttributesGetter.kt (94%) rename instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/{ => server}/KtorServerTracing.kt (97%) create mode 100644 instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt create mode 100644 instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt rename instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/{ => server}/KtorHttpServerTest.kt (98%) rename instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/{ => server}/KtorTestUtil.kt (91%) diff --git a/instrumentation/ktor/ktor-2.0/library/README.md b/instrumentation/ktor/ktor-2.0/library/README.md index b1343d9060a8..83f5bb949bf0 100644 --- a/instrumentation/ktor/ktor-2.0/library/README.md +++ b/instrumentation/ktor/ktor-2.0/library/README.md @@ -1,6 +1,6 @@ # Library Instrumentation for Ktor version 2.0 and higher -This package contains libraries to help instrument Ktor. Currently, only server instrumentation is supported. +This package contains libraries to help instrument Ktor. Server and client instrumentations are supported. ## Quickstart @@ -35,7 +35,7 @@ Initialize instrumentation by installing the `KtorServerTracing` feature. You mu the feature. ```kotlin -OpenTelemetry openTelemetry = initializeOpenTelemetryForMe() +val openTelemetry: OpenTelemetry = initializeOpenTelemetryForMe() embeddedServer(Netty, 8080) { install(KtorServerTracing) { @@ -43,3 +43,18 @@ embeddedServer(Netty, 8080) { } } ``` + +## Initializing client instrumentation + +Initialize instrumentation by installing the `KtorClientTracing` feature. You must set the `OpenTelemetry` to use with +the feature. + +```kotlin +val openTelemetry: OpenTelemetry = initializeOpenTelemetryForMe() + +val client = HttpClient { + install(KtorClientTracing) { + setOpenTelemetry(openTelemetry) + } +} +``` diff --git a/instrumentation/ktor/ktor-2.0/library/build.gradle.kts b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts index bd70452c3ab9..0a723cd48685 100644 --- a/instrumentation/ktor/ktor-2.0/library/build.gradle.kts +++ b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts @@ -6,8 +6,11 @@ plugins { id("org.jetbrains.kotlin.jvm") } +val ktorVersion = "2.0.0" + dependencies { - library("io.ktor:ktor-server-core:2.0.0") + library("io.ktor:ktor-client-core:$ktorVersion") + library("io.ktor:ktor-server-core:$ktorVersion") implementation(project(":instrumentation:ktor:ktor-common:library")) implementation("io.opentelemetry:opentelemetry-extension-kotlin") @@ -16,7 +19,8 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - testLibrary("io.ktor:ktor-server-netty:2.0.0") + testLibrary("io.ktor:ktor-server-netty:$ktorVersion") + testLibrary("io.ktor:ktor-client-cio:$ktorVersion") } tasks { diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/InstrumentationProperties.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/InstrumentationProperties.kt new file mode 100644 index 000000000000..510450713e1b --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/InstrumentationProperties.kt @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0 + +/** + * Common properties for both client and server instrumentations + */ +internal object InstrumentationProperties { + + internal const val INSTRUMENTATION_NAME = "io.opentelemetry.ktor-2.0" +} 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 new file mode 100644 index 000000000000..a98d77d60c15 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 kotlinx.coroutines.withContext + +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) + } + + 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 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() + } + } + } + + 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) + + if (openTelemetryContext != null) { + try { + withContext(openTelemetryContext.asContextElement()) { proceed() } + plugin.endSpan(openTelemetryContext, it.call, null) + } catch (e: Throwable) { + plugin.endSpan(openTelemetryContext, it.call, e) + throw e + } + } else { + proceed() + } + } + } + } +} 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 new file mode 100644 index 000000000000..d226b4e114d7 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.client + +import io.ktor.client.request.* +import io.ktor.client.statement.HttpResponse +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor.alwaysClient +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor +import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME + +class KtorClientTracingBuilder { + + private var openTelemetry: OpenTelemetry? = null + private val additionalExtractors = mutableListOf>() + private val httpAttributesExtractorBuilder = HttpClientAttributesExtractor.builder( + KtorHttpClientAttributesGetter, + KtorNetClientAttributesGetter, + ) + + fun setOpenTelemetry(openTelemetry: OpenTelemetry) { + this.openTelemetry = openTelemetry + } + + fun setCapturedRequestHeaders(vararg headers: String) = + setCapturedRequestHeaders(headers.asList()) + + fun setCapturedRequestHeaders(headers: List) { + httpAttributesExtractorBuilder.setCapturedRequestHeaders(headers) + } + + fun setCapturedResponseHeaders(vararg headers: String) = + setCapturedResponseHeaders(headers.asList()) + + fun setCapturedResponseHeaders(headers: List) { + httpAttributesExtractorBuilder.setCapturedResponseHeaders(headers) + } + + fun addAttributesExtractors(vararg extractors: AttributesExtractor) = + addAttributesExtractors(extractors.asList()) + + fun addAttributesExtractors(extractors: Iterable>) { + additionalExtractors += extractors + } + + internal fun build(): KtorClientTracing { + val initializedOpenTelemetry = openTelemetry + ?: throw IllegalArgumentException("OpenTelemetry must be set") + + val instrumenterBuilder = Instrumenter.builder( + initializedOpenTelemetry, + INSTRUMENTATION_NAME, + HttpSpanNameExtractor.create(KtorHttpClientAttributesGetter), + ) + + val instrumenter = instrumenterBuilder + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(KtorHttpClientAttributesGetter)) + .addAttributesExtractor(httpAttributesExtractorBuilder.build()) + .addAttributesExtractors(additionalExtractors) + .addOperationMetrics(HttpClientMetrics.get()) + .buildInstrumenter(alwaysClient()) + + return KtorClientTracing( + instrumenter = instrumenter, + propagators = initializedOpenTelemetry.propagators, + ) + } +} 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.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt new file mode 100644 index 000000000000..ba1cbb5d1b5c --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.client + +import io.ktor.client.request.* +import io.ktor.client.statement.HttpResponse +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter + +internal object KtorHttpClientAttributesGetter : HttpClientAttributesGetter { + + override fun getUrl(request: HttpRequestData) = + request.url.toString() + + override fun getFlavor(request: HttpRequestData, response: HttpResponse?) = + null + + override fun getMethod(request: HttpRequestData) = + request.method.value + + override fun getRequestHeader(request: HttpRequestData, name: String) = + request.headers.getAll(name).orEmpty() + + override fun getStatusCode(request: HttpRequestData, response: HttpResponse, error: Throwable?) = + response.status.value + + override fun getResponseHeader(request: HttpRequestData, response: HttpResponse, name: String) = + response.headers.getAll(name).orEmpty() +} 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.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpHeadersSetter.kt new file mode 100644 index 000000000000..a98567c0f440 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpHeadersSetter.kt @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.client + +import io.ktor.client.request.HttpRequestBuilder +import io.opentelemetry.context.propagation.TextMapSetter + +internal object KtorHttpHeadersSetter : TextMapSetter { + + override fun set(carrier: HttpRequestBuilder?, key: String, value: String) { + carrier?.headers?.set(key, value) + } +} diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorNetClientAttributesGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorNetClientAttributesGetter.kt new file mode 100644 index 000000000000..38a9b9260d47 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorNetClientAttributesGetter.kt @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.client + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +internal object KtorNetClientAttributesGetter : NetClientAttributesGetter { + + override fun getTransport(request: HttpRequestData, response: HttpResponse?) = IP_TCP + + override fun getPeerName(request: HttpRequestData) = request.url.host + + override fun getPeerPort(request: HttpRequestData) = request.url.port +} diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/ApplicationRequestGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/ApplicationRequestGetter.kt similarity index 89% rename from instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/ApplicationRequestGetter.kt rename to instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/ApplicationRequestGetter.kt index 17a30f62782d..267ea53dd7cf 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/ApplicationRequestGetter.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/ApplicationRequestGetter.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.ktor.v2_0 +package io.opentelemetry.instrumentation.ktor.v2_0.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/KtorHttpServerAttributesGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt similarity index 96% rename from instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerAttributesGetter.kt rename to instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt index 09025a366cac..885d9b8d3f42 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerAttributesGetter.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.ktor.v2_0 +package io.opentelemetry.instrumentation.ktor.v2_0.server import io.ktor.server.plugins.* import io.ktor.server.request.* diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorNetServerAttributesGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorNetServerAttributesGetter.kt similarity index 94% rename from instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorNetServerAttributesGetter.kt rename to instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorNetServerAttributesGetter.kt index f8172d304d68..0ea0c9ad26b8 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorNetServerAttributesGetter.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorNetServerAttributesGetter.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.ktor.v2_0 +package io.opentelemetry.instrumentation.ktor.v2_0.server import io.ktor.server.request.* import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorServerTracing.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt similarity index 97% rename from instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorServerTracing.kt rename to instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt index 2ef6b8cd2023..5ea36bf270a3 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorServerTracing.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.ktor.v2_0 +package io.opentelemetry.instrumentation.ktor.v2_0.server import io.ktor.server.application.* import io.ktor.server.request.* @@ -23,6 +23,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttribut import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor +import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME import kotlinx.coroutines.withContext class KtorServerTracing private constructor( @@ -81,7 +82,6 @@ class KtorServerTracing private constructor( } companion object Feature : BaseApplicationPlugin { - private val INSTRUMENTATION_NAME = "io.opentelemetry.ktor-2.0" private val contextKey = AttributeKey("OpenTelemetry") private val errorKey = AttributeKey("OpenTelemetryException") diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt new file mode 100644 index 000000000000..1a1f00a8fc58 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_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 host: String, + private val port: Int, + private val installTracing: HttpClientConfig<*>.() -> Unit, +) : 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", + 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-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt new file mode 100644 index 000000000000..5dded76b4dd6 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_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.HttpClientInstrumentationExtension +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.trace.attributes.SemanticAttributes.HTTP_FLAVOR +import kotlinx.coroutines.* +import org.junit.jupiter.api.extension.RegisterExtension +import java.net.URI + +class KtorHttpClientTest : AbstractHttpClientTest() { + + 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) { + // this instrumentation creates a span per each physical request + // related issue https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/5722 + disableTestRedirects() + + setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - HTTP_FLAVOR } + + setSingleConnectionFactory { host, port -> + KtorHttpClientSingleConnection(host, port) { installTracing() } + } + } + } + + companion object { + @JvmStatic + @RegisterExtension + private val TESTING = HttpClientInstrumentationExtension.forLibrary() + + private val CLIENT = HttpClient(CIO) { + install(HttpRedirect) + + installTracing() + } + + private fun HttpClientConfig<*>.installTracing() { + install(KtorClientTracing) { + setOpenTelemetry(TESTING.openTelemetry) + } + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt similarity index 98% rename from instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerTest.kt rename to instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt index ccefbce8b663..91b11caa8569 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerTest.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.ktor.v2_0 +package io.opentelemetry.instrumentation.ktor.v2_0.server import io.ktor.http.* import io.ktor.server.application.* diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorTestUtil.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorTestUtil.kt similarity index 91% rename from instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorTestUtil.kt rename to instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorTestUtil.kt index 308b33ef36a1..e6decdaf77d6 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorTestUtil.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorTestUtil.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.ktor.v2_0 +package io.opentelemetry.instrumentation.ktor.v2_0.server import io.ktor.server.application.* import io.opentelemetry.api.OpenTelemetry