diff --git a/docs/standalone-library-instrumentation.md b/docs/standalone-library-instrumentation.md index e99d660a2f57..dd0c7f4b1657 100644 --- a/docs/standalone-library-instrumentation.md +++ b/docs/standalone-library-instrumentation.md @@ -12,6 +12,8 @@ that can be used if you prefer that over using the Java agent: * [Guava](../instrumentation/guava-10.0/library) * [GraphQL Java](../instrumentation/graphql-java-12.0/library) * [JDBC](../instrumentation/jdbc/library) +* [Ktor 1.0](../instrumentation/ktor/ktor-1.0/library) +* [Ktor 2.0](../instrumentation/ktor/ktor-2.0/library) * [Lettuce](../instrumentation/lettuce/lettuce-5.1/library) * [Log4j appender](../instrumentation/log4j/log4j-appender-2.16/library) * [Log4j thread context](../instrumentation/log4j/log4j-context-data/log4j-context-data-2.16/library-autoconfigure) diff --git a/instrumentation/ktor-1.0/library/README.md b/instrumentation/ktor/ktor-1.0/library/README.md similarity index 100% rename from instrumentation/ktor-1.0/library/README.md rename to instrumentation/ktor/ktor-1.0/library/README.md diff --git a/instrumentation/ktor-1.0/library/build.gradle.kts b/instrumentation/ktor/ktor-1.0/library/build.gradle.kts similarity index 71% rename from instrumentation/ktor-1.0/library/build.gradle.kts rename to instrumentation/ktor/ktor-1.0/library/build.gradle.kts index e4d7e01cc64b..f48168837728 100644 --- a/instrumentation/ktor-1.0/library/build.gradle.kts +++ b/instrumentation/ktor/ktor-1.0/library/build.gradle.kts @@ -9,15 +9,17 @@ plugins { dependencies { library("io.ktor:ktor-server-core:1.0.0") + implementation(project(":instrumentation:ktor:ktor-common:library")) implementation("io.opentelemetry:opentelemetry-extension-kotlin") compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - // Note, we do not have a :testing library yet because there doesn't seem to be a way to have the Kotlin classes - // available for use from Spock. We will first need to migrate HttpServerTest to be usable outside of Spock. testLibrary("io.ktor:ktor-server-netty:1.0.0") + + latestDepTestLibrary("io.ktor:ktor-server-core:1.+") + latestDepTestLibrary("io.ktor:ktor-server-netty:1.+") } tasks { diff --git a/instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/ApplicationRequestGetter.kt b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/ApplicationRequestGetter.kt similarity index 100% rename from instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/ApplicationRequestGetter.kt rename to instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/ApplicationRequestGetter.kt diff --git a/instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt similarity index 100% rename from instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt rename to instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt diff --git a/instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt similarity index 93% rename from instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt rename to instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt index 74e80ac2eceb..bc6bd76c10d7 100644 --- a/instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt +++ b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt @@ -7,6 +7,7 @@ package io.opentelemetry.instrumentation.ktor.v1_0 import io.ktor.request.* import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter +import io.opentelemetry.instrumentation.ktor.isIpAddress import io.opentelemetry.semconv.trace.attributes.SemanticAttributes internal class KtorNetServerAttributesGetter : NetServerAttributesGetter { diff --git a/instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt similarity index 100% rename from instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt rename to instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt diff --git a/instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt similarity index 100% rename from instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt rename to instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt diff --git a/instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorTestUtil.kt b/instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorTestUtil.kt similarity index 100% rename from instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorTestUtil.kt rename to instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorTestUtil.kt diff --git a/instrumentation/ktor/ktor-2.0/library/README.md b/instrumentation/ktor/ktor-2.0/library/README.md new file mode 100644 index 000000000000..36a0b69dc7ab --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/README.md @@ -0,0 +1,18 @@ +# Ktor Instrumentation + +This package contains libraries to help instrument Ktor. Currently, only server instrumentation is supported. + +## Initializing server instrumentation + +Initialize instrumentation by installing the `KtorServerTracing` feature. You must set the `OpenTelemetry` to use with +the feature. + +```kotlin +OpenTelemetry openTelemetry = initializeOpenTelemetryForMe() + +embeddedServer(Netty, 8080) { + install(KtorServerTracing) { + setOpenTelemetry(openTelemetry) + } +} +``` diff --git a/instrumentation/ktor/ktor-2.0/library/build.gradle.kts b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts new file mode 100644 index 000000000000..bd70452c3ab9 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts @@ -0,0 +1,28 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("otel.library-instrumentation") + + id("org.jetbrains.kotlin.jvm") +} + +dependencies { + library("io.ktor:ktor-server-core:2.0.0") + + implementation(project(":instrumentation:ktor:ktor-common:library")) + implementation("io.opentelemetry:opentelemetry-extension-kotlin") + + compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + testLibrary("io.ktor:ktor-server-netty:2.0.0") +} + +tasks { + withType(KotlinCompile::class).configureEach { + kotlinOptions { + jvmTarget = "1.8" + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/ApplicationRequestGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/ApplicationRequestGetter.kt new file mode 100644 index 000000000000..17a30f62782d --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/ApplicationRequestGetter.kt @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0 + +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-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerAttributesGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerAttributesGetter.kt new file mode 100644 index 000000000000..ee5ac98322fa --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerAttributesGetter.kt @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0 + +import io.ktor.server.plugins.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +internal enum class KtorHttpServerAttributesGetter : + HttpServerAttributesGetter { + INSTANCE; + + override fun method(request: ApplicationRequest): String { + return request.httpMethod.value + } + + override fun requestHeader(request: ApplicationRequest, name: String): List { + return request.headers.getAll(name) ?: emptyList() + } + + override fun requestContentLength(request: ApplicationRequest, response: ApplicationResponse?): Long? { + return null + } + + override fun requestContentLengthUncompressed(request: ApplicationRequest, response: ApplicationResponse?): Long? { + return null + } + + override fun statusCode(request: ApplicationRequest, response: ApplicationResponse): Int? { + return response.status()?.value + } + + override fun responseContentLength(request: ApplicationRequest, response: ApplicationResponse): Long? { + return null + } + + override fun responseContentLengthUncompressed(request: ApplicationRequest, response: ApplicationResponse): Long? { + return null + } + + override fun responseHeader(request: ApplicationRequest, response: ApplicationResponse, name: String): List { + return response.headers.allValues().getAll(name) ?: emptyList() + } + + override fun flavor(request: ApplicationRequest): String? { + return when (request.httpVersion) { + "HTTP/1.1" -> SemanticAttributes.HttpFlavorValues.HTTP_1_1 + "HTTP/2.0" -> SemanticAttributes.HttpFlavorValues.HTTP_2_0 + else -> null + } + } + + override fun target(request: ApplicationRequest): String { + return request.uri + } + + override fun route(request: ApplicationRequest): String? { + return null + } + + override fun scheme(request: ApplicationRequest): String { + return request.origin.scheme + } + + override fun serverName(request: ApplicationRequest): String? { + return null + } +} diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorNetServerAttributesGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorNetServerAttributesGetter.kt new file mode 100644 index 000000000000..34c78570e8c5 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorNetServerAttributesGetter.kt @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0 + +import io.ktor.server.request.* +import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter +import io.opentelemetry.instrumentation.ktor.isIpAddress +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +internal class KtorNetServerAttributesGetter : NetServerAttributesGetter { + override fun transport(request: ApplicationRequest): String { + return SemanticAttributes.NetTransportValues.IP_TCP + } + + override fun peerPort(request: ApplicationRequest): Int? { + return null + } + + override fun peerIp(request: ApplicationRequest): String? { + var remote = request.local.remoteHost + if (remote != null && "unknown" != remote && isIpAddress(remote)) { + return remote + } + return null + } +} diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorServerTracing.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorServerTracing.kt new file mode 100644 index 000000000000..860e49a29315 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorServerTracing.kt @@ -0,0 +1,166 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0 + +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.util.* +import io.ktor.util.pipeline.* +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.context.Context +import io.opentelemetry.extension.kotlin.asContextElement +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor +import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesExtractor +import kotlinx.coroutines.withContext + +class KtorServerTracing private constructor( + private val instrumenter: Instrumenter +) { + + class Configuration { + internal lateinit var openTelemetry: OpenTelemetry + + internal val additionalExtractors = mutableListOf>() + + internal val httpAttributesExtractorBuilder = HttpServerAttributesExtractor.builder(KtorHttpServerAttributesGetter.INSTANCE) + + internal var statusExtractor: + (SpanStatusExtractor) -> SpanStatusExtractor = { a -> a } + + fun setOpenTelemetry(openTelemetry: OpenTelemetry) { + this.openTelemetry = openTelemetry + } + + fun setStatusExtractor(extractor: (SpanStatusExtractor) -> SpanStatusExtractor) { + this.statusExtractor = extractor + } + + fun addAttributeExtractor(extractor: AttributesExtractor) { + additionalExtractors.add(extractor) + } + + fun setCapturedRequestHeaders(requestHeaders: List) { + httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders) + } + + fun setCapturedResponseHeaders(responseHeaders: List) { + httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders) + } + + internal fun isOpenTelemetryInitialized(): Boolean = this::openTelemetry.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 INSTRUMENTATION_NAME = "io.opentelemetry.ktor-1.0" + + 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) + + if (!configuration.isOpenTelemetryInitialized()) { + throw IllegalArgumentException("OpenTelemetry must be set") + } + + val httpAttributesGetter = KtorHttpServerAttributesGetter.INSTANCE + + val instrumenterBuilder = Instrumenter.builder( + configuration.openTelemetry, + INSTRUMENTATION_NAME, + HttpSpanNameExtractor.create(httpAttributesGetter) + ) + + configuration.additionalExtractors.forEach { instrumenterBuilder.addAttributesExtractor(it) } + + with(instrumenterBuilder) { + setSpanStatusExtractor(configuration.statusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter))) + addAttributesExtractor(NetServerAttributesExtractor.create(KtorNetServerAttributesGetter())) + addAttributesExtractor(configuration.httpAttributesExtractorBuilder.build()) + addRequestMetrics(HttpServerMetrics.get()) + addContextCustomizer(HttpRouteHolder.get()) + } + + val instrumenter = instrumenterBuilder.newServerInstrumenter(ApplicationRequestGetter) + + val feature = KtorServerTracing(instrumenter) + + val startPhase = PipelinePhase("OpenTelemetry") + pipeline.insertPhaseBefore(ApplicationCallPipeline.Monitoring, startPhase) + pipeline.intercept(startPhase) { + val context = feature.start(call) + + if (context != null) { + call.attributes.put(contextKey, context) + withContext(context.asContextElement()) { + try { + proceed() + } catch (err: Throwable) { + // Stash error for reporting later since need ktor to finish setting up the response + call.attributes.put(errorKey, err) + throw err + } + } + } else { + proceed() + } + } + + val postSendPhase = PipelinePhase("OpenTelemetryPostSend") + pipeline.sendPipeline.insertPhaseAfter(ApplicationSendPipeline.After, postSendPhase) + pipeline.sendPipeline.intercept(postSendPhase) { + val context = call.attributes.getOrNull(contextKey) + if (context != null) { + var error: Throwable? = call.attributes.getOrNull(errorKey) + try { + proceed() + } catch (t: Throwable) { + error = t + throw t + } finally { + feature.end(context, call, error) + } + } else { + proceed() + } + } + + pipeline.environment.monitor.subscribe(Routing.RoutingCallStarted) { call -> + val context = call.attributes.getOrNull(contextKey) + if (context != null) { + HttpRouteHolder.updateHttpRoute(context, 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/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerTest.kt new file mode 100644 index 000000000000..cc8140eef88f --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0 + +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.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() { + + companion object { + @JvmStatic + @RegisterExtension + val testing = 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 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) + } + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorTestUtil.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorTestUtil.kt new file mode 100644 index 000000000000..308b33ef36a1 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorTestUtil.kt @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0 + +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) + setCapturedRequestHeaders(listOf(AbstractHttpServerTest.TEST_REQUEST_HEADER)) + setCapturedResponseHeaders(listOf(AbstractHttpServerTest.TEST_RESPONSE_HEADER)) + } + } + } +} diff --git a/instrumentation/ktor/ktor-common/library/build.gradle.kts b/instrumentation/ktor/ktor-common/library/build.gradle.kts new file mode 100644 index 000000000000..0362618225fc --- /dev/null +++ b/instrumentation/ktor/ktor-common/library/build.gradle.kts @@ -0,0 +1,19 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("otel.library-instrumentation") + id("org.jetbrains.kotlin.jvm") +} + +dependencies { + compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") +} + +tasks { + withType(KotlinCompile::class).configureEach { + kotlinOptions { + jvmTarget = "1.8" + } + } +} diff --git a/instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/IpAddressUtil.kt b/instrumentation/ktor/ktor-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/IpAddressUtil.kt similarity index 96% rename from instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/IpAddressUtil.kt rename to instrumentation/ktor/ktor-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/IpAddressUtil.kt index 4bfda7b562dc..f55a200b968f 100644 --- a/instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/IpAddressUtil.kt +++ b/instrumentation/ktor/ktor-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/IpAddressUtil.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.ktor.v1_0 +package io.opentelemetry.instrumentation.ktor import java.util.regex.Pattern diff --git a/instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/IpAddressUtilTest.kt b/instrumentation/ktor/ktor-common/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/IpAddressUtilTest.kt similarity index 92% rename from instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/IpAddressUtilTest.kt rename to instrumentation/ktor/ktor-common/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/IpAddressUtilTest.kt index dbd3733a20dd..a6b6e193dec3 100644 --- a/instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/IpAddressUtilTest.kt +++ b/instrumentation/ktor/ktor-common/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/IpAddressUtilTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.ktor.v1_0 +package io.opentelemetry.instrumentation.ktor import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat import org.junit.jupiter.api.Test diff --git a/settings.gradle.kts b/settings.gradle.kts index 8af69b44daa7..40bea75dbc80 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -269,7 +269,9 @@ include(":instrumentation:kafka:kafka-clients:kafka-clients-2.6:library") 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-1.0:library") +include(":instrumentation:ktor:ktor-1.0:library") +include(":instrumentation:ktor:ktor-2.0:library") +include(":instrumentation:ktor:ktor-common:library") include(":instrumentation:kubernetes-client-7.0:javaagent") include(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests") include(":instrumentation:lettuce:lettuce-common:library")