diff --git a/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts b/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..ed95838040b7 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts @@ -0,0 +1,42 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.jetbrains.kotlin.jvm") + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.jetbrains.kotlinx") + module.set("ktor-server-core") + versions.set("[2.0.0,)") + } +} + +val ktorVersion = "2.0.0" + +dependencies { + library("io.ktor:ktor-client-core:$ktorVersion") + library("io.ktor:ktor-server-core:$ktorVersion") + + implementation(project(":instrumentation:ktor:ktor-2.0:library")) + + compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + + testImplementation(project(":instrumentation:ktor:ktor-2.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") +} + +tasks { + withType(KotlinCompile::class).configureEach { + kotlinOptions { + jvmTarget = "1.8" + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java new file mode 100644 index 000000000000..194ac50ff5ac --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ktor.v2_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.api.OpenTelemetry; +import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracing; +import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder; +import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +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) { + OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + builder.setOpenTelemetry(openTelemetry); + builder.setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()); + builder.setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()); + builder.setKnownMethods(CommonConfig.get().getKnownHttpRequestMethods()); + + return kotlin.Unit.INSTANCE; + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorInstrumentationModule.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorInstrumentationModule.java new file mode 100644 index 000000000000..e13de2782242 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorInstrumentationModule.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ktor.v2_0; + +import static java.util.Arrays.asList; + +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 KtorInstrumentationModule extends InstrumentationModule { + + public KtorInstrumentationModule() { + super("ktor", "ktor-2.0"); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.opentelemetry.extension.kotlin."); + } + + @Override + public List typeInstrumentations() { + return asList(new ServerInstrumentation(), new HttpClientInstrumentation()); + } +} diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java new file mode 100644 index 000000000000..c11a07fc4695 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ktor.v2_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.api.OpenTelemetry; +import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing; +import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +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.ApplicationEngineEnvironmentReloading"); + } + + @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) { + OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + configuration.setOpenTelemetry(openTelemetry); + configuration.setCapturedRequestHeaders(CommonConfig.get().getServerRequestHeaders()); + configuration.setCapturedResponseHeaders(CommonConfig.get().getServerResponseHeaders()); + configuration.setKnownMethods(CommonConfig.get().getKnownHttpRequestMethods()); + + return kotlin.Unit.INSTANCE; + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt b/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt new file mode 100644 index 000000000000..12589df5fdb4 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt @@ -0,0 +1,27 @@ +/* + * 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.instrumentation.testing.junit.http.HttpClientInstrumentationExtension +import kotlinx.coroutines.* +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-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt new file mode 100644 index 000000000000..f9776431becf --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.server + +import io.ktor.server.application.* +import io.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-2.0/library/build.gradle.kts b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts index 0a723cd48685..3b007244cc36 100644 --- a/instrumentation/ktor/ktor-2.0/library/build.gradle.kts +++ b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation(project(":instrumentation:ktor:ktor-2.0:testing")) testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testLibrary("io.ktor:ktor-server-netty:$ktorVersion") diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt index c600c60bc776..f6d10b6b21a1 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt @@ -173,10 +173,7 @@ class KtorServerTracing private constructor( } pipeline.environment.monitor.subscribe(Routing.RoutingCallStarted) { call -> - val context = call.attributes.getOrNull(contextKey) - if (context != null) { - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, { _, arg -> arg.route.parent.toString() }, call) - } + HttpRouteHolder.updateHttpRoute(Context.current(), HttpRouteSource.SERVLET, { _, arg -> arg.route.parent.toString() }, call) } return feature 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 index 39a68f6845bd..0d9fa7971774 100644 --- 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 @@ -6,84 +6,22 @@ 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 -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) { - disableTestReadTimeout() - // 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 - setOf(SemanticAttributes.NET_PROTOCOL_NAME, SemanticAttributes.NET_PROTOCOL_VERSION) } - - setSingleConnectionFactory { host, port -> - KtorHttpClientSingleConnection(host, port) { installTracing() } - } - } - } +class KtorHttpClientTest : AbstractKtorHttpClientTest() { 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) - setCapturedRequestHeaders(listOf(AbstractHttpClientTest.TEST_REQUEST_HEADER)) - setCapturedResponseHeaders(listOf(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) - } + override fun HttpClientConfig<*>.installTracing() { + install(KtorClientTracing) { + setOpenTelemetry(TESTING.openTelemetry) + setCapturedRequestHeaders(listOf(TEST_REQUEST_HEADER)) + setCapturedResponseHeaders(listOf(TEST_RESPONSE_HEADER)) } } } diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt index 177261fdeabb..97f4022c3102 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt @@ -5,138 +5,24 @@ package io.opentelemetry.instrumentation.ktor.v2_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.http.AbstractHttpServerTest +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension -import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import kotlinx.coroutines.withContext import org.junit.jupiter.api.extension.RegisterExtension -import java.util.concurrent.ExecutionException -import java.util.concurrent.TimeUnit -class KtorHttpServerTest : AbstractHttpServerTest() { +class KtorHttpServerTest : AbstractKtorHttpServerTest() { companion object { @JvmStatic @RegisterExtension - val testing = HttpServerInstrumentationExtension.forLibrary() + val TESTING: InstrumentationExtension = HttpServerInstrumentationExtension.forLibrary() } - override fun setupServer(): ApplicationEngine { - return embeddedServer(Netty, port = port) { - KtorTestUtil.installOpenTelemetry(this, testing.openTelemetry) - - 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 getTesting(): InstrumentationExtension { + return TESTING } - override fun stopServer(server: ApplicationEngine) { - 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 = testing.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 - SemanticAttributes.NET_PEER_PORT - } - - options.setExpectedHttpRoute { - when (it) { - ServerEndpoint.PATH_PARAM -> "/path/{id}/param" - else -> expectedHttpRoute(it) - } - } - - // 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) + override fun installOpenTelemetry(application: Application) { + KtorTestUtil.installOpenTelemetry(application, TESTING.openTelemetry) } } diff --git a/instrumentation/ktor/ktor-2.0/testing/build.gradle.kts b/instrumentation/ktor/ktor-2.0/testing/build.gradle.kts new file mode 100644 index 000000000000..5e46694d473e --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/testing/build.gradle.kts @@ -0,0 +1,30 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("otel.java-conventions") + + id("org.jetbrains.kotlin.jvm") +} + +val ktorVersion = "2.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") +} + +tasks { + withType(KotlinCompile::class).configureEach { + kotlinOptions { + jvmTarget = "1.8" + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt new file mode 100644 index 000000000000..58bba1f83baf --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt @@ -0,0 +1,75 @@ +/* + * 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.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 +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() + + setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(SemanticAttributes.NET_PROTOCOL_NAME, SemanticAttributes.NET_PROTOCOL_VERSION) } + + setSingleConnectionFactory { host, port -> + KtorHttpClientSingleConnection(host, port) { installTracing() } + } + } + } +} 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/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt similarity index 100% rename from instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt rename to instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt diff --git a/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/AbstractKtorHttpServerTest.kt b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/AbstractKtorHttpServerTest.kt new file mode 100644 index 000000000000..62a7f8280426 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/AbstractKtorHttpServerTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.server + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.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.trace.attributes.SemanticAttributes +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(): ApplicationEngine { + 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: ApplicationEngine) { + 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 - SemanticAttributes.NET_PEER_PORT + } + + options.setExpectedHttpRoute { + when (it) { + ServerEndpoint.PATH_PARAM -> "/path/{id}/param" + else -> expectedHttpRoute(it) + } + } + + // 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) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 79d534e337cc..2a155b04f5b5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -338,7 +338,9 @@ include(":instrumentation:kafka:kafka-clients:kafka-clients-common:library") include(":instrumentation:kafka:kafka-streams-0.11:javaagent") include(":instrumentation:kotlinx-coroutines:javaagent") 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-common:library") include(":instrumentation:kubernetes-client-7.0:javaagent") include(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests")