From 85513ba656760621c39a5287d774b5cf86a8aca4 Mon Sep 17 00:00:00 2001 From: "leonid.stashevsky" Date: Thu, 17 Oct 2024 09:24:42 +0200 Subject: [PATCH] Add Ktor 3.0.0 support --- docs/supported-libraries.md | 2 +- .../ktor/ktor-3.0/javaagent/build.gradle.kts | 48 ++++ .../ktor/v3_0/HttpClientInstrumentation.java | 62 +++++ .../v3_0/KtorClientInstrumentationModule.java | 31 +++ .../v3_0/KtorServerInstrumentationModule.java | 31 +++ .../ktor/v3_0/ServerInstrumentation.java | 58 ++++ .../ktor/v3_0/client/KtorHttpClientTest.kt | 22 ++ .../ktor/v3_0/server/KtorHttpServerTest.kt | 33 +++ .../ktor/ktor-3.0/library/README.md | 60 +++++ .../ktor/ktor-3.0/library/build.gradle.kts | 39 +++ .../ktor/v3_0/InstrumentationProperties.kt | 14 + .../ktor/v3_0/client/KtorClientTracing.kt | 112 ++++++++ .../v3_0/client/KtorClientTracingBuilder.kt | 141 ++++++++++ .../client/KtorHttpClientAttributesGetter.kt | 37 +++ .../ktor/v3_0/client/KtorHttpHeadersSetter.kt | 16 ++ .../ktor/v3_0/internal/KtorBuilderUtil.kt | 24 ++ .../v3_0/server/ApplicationRequestGetter.kt | 19 ++ .../server/KtorHttpServerAttributesGetter.kt | 56 ++++ .../ktor/v3_0/server/KtorServerTracing.kt | 253 ++++++++++++++++++ .../ktor/v3_0/client/KtorHttpClientTest.kt | 27 ++ .../ktor/v3_0/server/KtorHttpServerTest.kt | 28 ++ .../server/KtorServerSpanKindExtractorTest.kt | 111 ++++++++ .../ktor/v3_0/server/KtorTestUtil.kt | 22 ++ .../ktor/ktor-3.0/testing/build.gradle.kts | 28 ++ .../v3_0/client/AbstractKtorHttpClientTest.kt | 75 ++++++ .../client/KtorHttpClientSingleConnection.kt | 44 +++ .../v3_0/server/AbstractKtorHttpServerTest.kt | 141 ++++++++++ settings.gradle.kts | 3 + 28 files changed, 1536 insertions(+), 1 deletion(-) create mode 100644 instrumentation/ktor/ktor-3.0/javaagent/build.gradle.kts create mode 100644 instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/HttpClientInstrumentation.java create mode 100644 instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/KtorClientInstrumentationModule.java create mode 100644 instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/KtorServerInstrumentationModule.java create mode 100644 instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/ServerInstrumentation.java create mode 100644 instrumentation/ktor/ktor-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientTest.kt create mode 100644 instrumentation/ktor/ktor-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerTest.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/README.md create mode 100644 instrumentation/ktor/ktor-3.0/library/build.gradle.kts create mode 100644 instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/InstrumentationProperties.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracing.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracingBuilder.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientAttributesGetter.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpHeadersSetter.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/internal/KtorBuilderUtil.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/ApplicationRequestGetter.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerAttributesGetter.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerTracing.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientTest.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerTest.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerSpanKindExtractorTest.kt create mode 100644 instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorTestUtil.kt create mode 100644 instrumentation/ktor/ktor-3.0/testing/build.gradle.kts create mode 100644 instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/AbstractKtorHttpClientTest.kt create mode 100644 instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientSingleConnection.kt create mode 100644 instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/AbstractKtorHttpServerTest.kt diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 91a0c211446c..26ff9517b4c9 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-3.0/javaagent/build.gradle.kts b/instrumentation/ktor/ktor-3.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..5804d4484eac --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/javaagent/build.gradle.kts @@ -0,0 +1,48 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("org.jetbrains.kotlin.jvm") + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.jetbrains.kotlinx") + module.set("ktor-server-core") + versions.set("[3.0.0,)") + assertInverse.set(true) + } +} + +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")) + + 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") + + latestDepTestLibrary("io.ktor:ktor-client-core:3.+") + latestDepTestLibrary("io.ktor:ktor-server-core:3.+") + latestDepTestLibrary("io.ktor:ktor-server-netty:3.+") + latestDepTestLibrary("io.ktor:ktor-client-cio:3.+") +} + +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..4e804972c7b9 --- /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.v3_0.client.KtorClientTracing; +import io.opentelemetry.instrumentation.ktor.v3_0.client.KtorClientTracingBuilder; +import io.opentelemetry.instrumentation.ktor.v3_0.internal.KtorBuilderUtil; +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 kotlin.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..b23e861fc5d4 --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/KtorClientInstrumentationModule.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 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 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..23f20e5b6427 --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v3_0/ServerInstrumentation.java @@ -0,0 +1,58 @@ +/* + * 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.v3_0.internal.KtorBuilderUtil; +import io.opentelemetry.instrumentation.ktor.v3_0.server.KtorServerTracing; +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, KtorServerTracing.Feature, new SetupFunction()); + } + } + + public static class SetupFunction + implements Function1 { + + @Override + public Unit invoke(KtorServerTracing.Configuration configuration) { + configuration.setOpenTelemetry(GlobalOpenTelemetry.get()); + KtorBuilderUtil.serverBuilderExtractor + .invoke(configuration) + .configure(AgentCommonConfig.get()); + return kotlin.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..6a7cc005012b --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/build.gradle.kts @@ -0,0 +1,39 @@ +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") + + implementation(project(":instrumentation:ktor:ktor-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") + + latestDepTestLibrary("io.ktor:ktor-client-core:3.+") + latestDepTestLibrary("io.ktor:ktor-server-core:3.+") + latestDepTestLibrary("io.ktor:ktor-server-netty:3.+") + latestDepTestLibrary("io.ktor:ktor-client-cio:3.+") +} + +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..08c3484f86ea --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracing.kt @@ -0,0 +1,112 @@ +/* + * 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.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.InternalCoroutinesApi +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +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() + } + } + } + + @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) + } + } + } + } +} 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..2f2b97786e87 --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorClientTracingBuilder.kt @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v3_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.v3_0.InstrumentationProperties.INSTRUMENTATION_NAME +import io.opentelemetry.instrumentation.ktor.v3_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) + } + + internal fun build(): KtorClientTracing = KtorClientTracing( + instrumenter = clientBuilder.build(), + propagators = openTelemetry.propagators, + ) +} diff --git a/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientAttributesGetter.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientAttributesGetter.kt new file mode 100644 index 000000000000..d15c9b45087c --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientAttributesGetter.kt @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v3_0.client + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter + +internal object KtorHttpClientAttributesGetter : HttpClientAttributesGetter { + + override fun getUrlFull(request: HttpRequestData) = request.url.toString() + + override fun getHttpRequestMethod(request: HttpRequestData) = request.method.value + + override fun getHttpRequestHeader(request: HttpRequestData, name: String) = request.headers.getAll(name).orEmpty() + + override fun getHttpResponseStatusCode(request: HttpRequestData, response: HttpResponse, error: Throwable?) = response.status.value + + override fun getHttpResponseHeader(request: HttpRequestData, response: HttpResponse, name: String) = response.headers.getAll(name).orEmpty() + + override fun getNetworkProtocolName(request: HttpRequestData, response: HttpResponse?): String? = response?.version?.name + + override fun getNetworkProtocolVersion(request: HttpRequestData, response: HttpResponse?): String? { + val version = response?.version ?: return null + if (version.minor == 0) { + return "${version.major}" + } + return "${version.major}.${version.minor}" + } + + override fun getServerAddress(request: HttpRequestData) = request.url.host + + override fun getServerPort(request: HttpRequestData) = request.url.port +} diff --git a/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpHeadersSetter.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpHeadersSetter.kt new file mode 100644 index 000000000000..762f724df18d --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpHeadersSetter.kt @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v3_0.client + +import io.ktor.client.request.* +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-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/internal/KtorBuilderUtil.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/internal/KtorBuilderUtil.kt new file mode 100644 index 000000000000..7de513a2e23f --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/internal/KtorBuilderUtil.kt @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v3_0.internal + +import io.ktor.client.request.* +import io.ktor.client.statement.* +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.v3_0.client.KtorClientTracingBuilder +import io.opentelemetry.instrumentation.ktor.v3_0.server.KtorServerTracing + +/** + * 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 +} diff --git a/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/ApplicationRequestGetter.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/ApplicationRequestGetter.kt new file mode 100644 index 000000000000..489a189fa03d --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/ApplicationRequestGetter.kt @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v3_0.server + +import io.ktor.server.request.* +import io.opentelemetry.context.propagation.TextMapGetter + +internal object ApplicationRequestGetter : TextMapGetter { + override fun keys(carrier: ApplicationRequest): Iterable { + return carrier.headers.names() + } + + override fun get(carrier: ApplicationRequest?, name: String): String? { + return carrier?.headers?.get(name) + } +} diff --git a/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerAttributesGetter.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerAttributesGetter.kt new file mode 100644 index 000000000000..e2fd8153114a --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerAttributesGetter.kt @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v3_0.server + +import io.ktor.server.plugins.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter +import io.opentelemetry.instrumentation.ktor.isIpAddress + +internal enum class KtorHttpServerAttributesGetter : HttpServerAttributesGetter { + INSTANCE, ; + + override fun getHttpRequestMethod(request: ApplicationRequest): String { + return request.httpMethod.value + } + + override fun getHttpRequestHeader(request: ApplicationRequest, name: String): List { + return request.headers.getAll(name) ?: emptyList() + } + + override fun getHttpResponseStatusCode(request: ApplicationRequest, response: ApplicationResponse, error: Throwable?): Int? { + return response.status()?.value + } + + override fun getHttpResponseHeader(request: ApplicationRequest, response: ApplicationResponse, name: String): List { + return response.headers.allValues().getAll(name) ?: emptyList() + } + + override fun getUrlScheme(request: ApplicationRequest): String { + return request.origin.scheme + } + + override fun getUrlPath(request: ApplicationRequest): String { + return request.path() + } + + override fun getUrlQuery(request: ApplicationRequest): String { + return request.queryString() + } + + override fun getNetworkProtocolName(request: ApplicationRequest, response: ApplicationResponse?): String? = if (request.httpVersion.startsWith("HTTP/")) "http" else null + + override fun getNetworkProtocolVersion(request: ApplicationRequest, response: ApplicationResponse?): String? = if (request.httpVersion.startsWith("HTTP/")) request.httpVersion.substring("HTTP/".length) else null + + override fun getNetworkPeerAddress(request: ApplicationRequest, response: ApplicationResponse?): String? { + val remote = request.local.remoteHost + if ("unknown" != remote && isIpAddress(remote)) { + return remote + } + return null + } +} diff --git a/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerTracing.kt b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerTracing.kt new file mode 100644 index 000000000000..24ad8951254b --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerTracing.kt @@ -0,0 +1,253 @@ +/* + * 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.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.RoutingRoot.Plugin.RoutingCallStarted +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.v3_0.InstrumentationProperties.INSTRUMENTATION_NAME +import io.opentelemetry.instrumentation.ktor.v3_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 (cause: Throwable) { + error = cause + throw cause + } finally { + feature.end(context, call, error) + } + } else { + proceed() + } + } + + pipeline.monitor.subscribe(RoutingCallStarted) { call -> + HttpServerRoute.update( + Context.current(), + HttpServerRouteSource.SERVER, + { _, arg -> arg.route.parent.toString() }, + call + ) + } + + return feature + } + } +} 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..47e93b329c33 --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorHttpServerTest.kt @@ -0,0 +1,28 @@ +/* + * 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) { + KtorTestUtil.installOpenTelemetry(application, TESTING.openTelemetry) + } +} diff --git a/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerSpanKindExtractorTest.kt b/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerSpanKindExtractorTest.kt new file mode 100644 index 000000000000..60191c353104 --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorServerSpanKindExtractorTest.kt @@ -0,0 +1,111 @@ +/* + * 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.SpanKind +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpMethod +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.concurrent.TimeUnit +import java.util.function.Consumer +import java.util.stream.Stream + +class KtorServerSpanKindExtractorTest : AbstractHttpServerUsingTest>() { + + private val consumerKindEndpoint = ServerEndpoint("consumerKindEndpoint", "from-pubsub/run", 200, "") + private val serverKindEndpoint = ServerEndpoint("serverKindEndpoint", "from-client/run", 200, "") + + companion object { + @JvmStatic + @RegisterExtension + val testing: InstrumentationExtension = HttpServerInstrumentationExtension.forLibrary() + } + + @BeforeAll + fun setupOptions() { + startServer() + } + + @AfterAll + fun cleanup() { + cleanupServer() + } + + override fun getContextPath() = "" + + override fun setupServer(): EmbeddedServer<*, *> { + return embeddedServer(Netty, port = port) { + install(KtorServerTracing) { + setOpenTelemetry(testing.openTelemetry) + spanKindExtractor { + if (uri.startsWith("/from-pubsub/")) { + SpanKind.CONSUMER + } else { + SpanKind.SERVER + } + } + } + + routing { + post(consumerKindEndpoint.path) { + call.respondText(consumerKindEndpoint.body, status = HttpStatusCode.fromValue(consumerKindEndpoint.status)) + } + + post(serverKindEndpoint.path) { + call.respondText(serverKindEndpoint.body, status = HttpStatusCode.fromValue(serverKindEndpoint.status)) + } + } + }.start() + } + + override fun stopServer(server: EmbeddedServer<*, *>) { + server.stop(0, 10, TimeUnit.SECONDS) + } + + @ParameterizedTest + @MethodSource("provideArguments") + fun testSpanKindExtractor(endpoint: ServerEndpoint, expectedKind: SpanKind) { + val request = AggregatedHttpRequest.of(HttpMethod.valueOf("POST"), resolveAddress(endpoint)) + val response: AggregatedHttpResponse = client.execute(request).aggregate().join() + assertThat(response.status().code()).isEqualTo(endpoint.status) + + testing.waitAndAssertTraces( + Consumer { trace -> + trace.hasSpansSatisfyingExactly( + Consumer { span -> + span.hasKind(expectedKind) + } + ) + } + ) + } + + private fun provideArguments(): Stream { + return Stream.of( + arguments(consumerKindEndpoint, SpanKind.CONSUMER), + arguments(serverKindEndpoint, SpanKind.SERVER), + ) + } +} diff --git a/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorTestUtil.kt b/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorTestUtil.kt new file mode 100644 index 000000000000..3c0537c34e02 --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/server/KtorTestUtil.kt @@ -0,0 +1,22 @@ +/* + * 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.api.OpenTelemetry +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest + +class KtorTestUtil { + companion object { + fun installOpenTelemetry(application: Application, openTelemetry: OpenTelemetry) { + application.install(KtorServerTracing) { + setOpenTelemetry(openTelemetry) + capturedRequestHeaders(AbstractHttpServerTest.TEST_REQUEST_HEADER) + capturedResponseHeaders(AbstractHttpServerTest.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..9f19bb9ed5c5 --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/AbstractKtorHttpClientTest.kt @@ -0,0 +1,75 @@ +/* + * 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 java.net.URI + +abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest() { + + private val client = HttpClient(CIO) { + install(HttpRedirect) + + installTracing() + } + + 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() + // this instrumentation creates a span per each physical request + // related issue https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/5722 + disableTestRedirects() + spanEndsAfterBody() + + setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) } + + setSingleConnectionFactory { host, port -> + KtorHttpClientSingleConnection(host, port) { installTracing() } + } + } + } +} 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..f76f6975a9f9 --- /dev/null +++ b/instrumentation/ktor/ktor-3.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/client/KtorHttpClientSingleConnection.kt @@ -0,0 +1,44 @@ +/* + * 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 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-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..607e4427d5ef --- /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 Exception(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 82580654f25a..09a2f3ffc1af 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -402,6 +402,9 @@ 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-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")