Browse Source

Add library instrumentation for ktor 2 (#5797)

Lauri Tulmin 2 years ago
parent
commit
91066a8073
21 changed files with 521 additions and 5 deletions
  1. 2 0
      docs/standalone-library-instrumentation.md
  2. 0 0
      instrumentation/ktor/ktor-1.0/library/README.md
  3. 1 2
      instrumentation/ktor/ktor-1.0/library/build.gradle.kts
  4. 0 0
      instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/ApplicationRequestGetter.kt
  5. 0 0
      instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt
  6. 1 0
      instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt
  7. 0 0
      instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt
  8. 0 0
      instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt
  9. 0 0
      instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorTestUtil.kt
  10. 18 0
      instrumentation/ktor/ktor-2.0/library/README.md
  11. 28 0
      instrumentation/ktor/ktor-2.0/library/build.gradle.kts
  12. 19 0
      instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/ApplicationRequestGetter.kt
  13. 73 0
      instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerAttributesGetter.kt
  14. 29 0
      instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorNetServerAttributesGetter.kt
  15. 166 0
      instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorServerTracing.kt
  16. 138 0
      instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerTest.kt
  17. 22 0
      instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorTestUtil.kt
  18. 19 0
      instrumentation/ktor/ktor-common/library/build.gradle.kts
  19. 1 1
      instrumentation/ktor/ktor-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/IpAddressUtil.kt
  20. 1 1
      instrumentation/ktor/ktor-common/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/IpAddressUtilTest.kt
  21. 3 1
      settings.gradle.kts

+ 2 - 0
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)

+ 0 - 0
instrumentation/ktor-1.0/library/README.md → instrumentation/ktor/ktor-1.0/library/README.md


+ 1 - 2
instrumentation/ktor-1.0/library/build.gradle.kts → instrumentation/ktor/ktor-1.0/library/build.gradle.kts

@@ -9,14 +9,13 @@ 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.+")

+ 0 - 0
instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/ApplicationRequestGetter.kt → instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/ApplicationRequestGetter.kt


+ 0 - 0
instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt → instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt


+ 1 - 0
instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt → 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<ApplicationRequest> {

+ 0 - 0
instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt → instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt


+ 0 - 0
instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt → instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt


+ 0 - 0
instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorTestUtil.kt → instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorTestUtil.kt


+ 18 - 0
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)
+  }
+}
+```

+ 28 - 0
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"
+    }
+  }
+}

+ 19 - 0
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<ApplicationRequest> {
+  override fun keys(carrier: ApplicationRequest): Iterable<String> {
+    return carrier.headers.names()
+  }
+
+  override fun get(carrier: ApplicationRequest?, name: String): String? {
+    return carrier?.headers?.get(name)
+  }
+}

+ 73 - 0
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<ApplicationRequest, ApplicationResponse> {
+  INSTANCE;
+
+  override fun method(request: ApplicationRequest): String {
+    return request.httpMethod.value
+  }
+
+  override fun requestHeader(request: ApplicationRequest, name: String): List<String> {
+    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<String> {
+    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
+  }
+}

+ 29 - 0
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<ApplicationRequest> {
+  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
+  }
+}

+ 166 - 0
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<ApplicationRequest, ApplicationResponse>
+) {
+
+  class Configuration {
+    internal lateinit var openTelemetry: OpenTelemetry
+
+    internal val additionalExtractors = mutableListOf<AttributesExtractor<in ApplicationRequest, in ApplicationResponse>>()
+
+    internal val httpAttributesExtractorBuilder = HttpServerAttributesExtractor.builder(KtorHttpServerAttributesGetter.INSTANCE)
+
+    internal var statusExtractor:
+      (SpanStatusExtractor<ApplicationRequest, ApplicationResponse>) -> SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse> = { a -> a }
+
+    fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
+      this.openTelemetry = openTelemetry
+    }
+
+    fun setStatusExtractor(extractor: (SpanStatusExtractor<ApplicationRequest, ApplicationResponse>) -> SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse>) {
+      this.statusExtractor = extractor
+    }
+
+    fun addAttributeExtractor(extractor: AttributesExtractor<in ApplicationRequest, in ApplicationResponse>) {
+      additionalExtractors.add(extractor)
+    }
+
+    fun setCapturedRequestHeaders(requestHeaders: List<String>) {
+      httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders)
+    }
+
+    fun setCapturedResponseHeaders(responseHeaders: List<String>) {
+      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<Application, Configuration, KtorServerTracing> {
+    private val INSTRUMENTATION_NAME = "io.opentelemetry.ktor-1.0"
+
+    private val contextKey = AttributeKey<Context>("OpenTelemetry")
+    private val errorKey = AttributeKey<Throwable>("OpenTelemetryException")
+
+    override val key: AttributeKey<KtorServerTracing> = 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<ApplicationRequest, ApplicationResponse>(
+        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
+    }
+  }
+}

+ 138 - 0
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<ApplicationEngine>() {
+
+  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)
+      }
+    }
+  }
+}

+ 22 - 0
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))
+      }
+    }
+  }
+}

+ 19 - 0
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"
+    }
+  }
+}

+ 1 - 1
instrumentation/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/IpAddressUtil.kt → 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
 

+ 1 - 1
instrumentation/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/IpAddressUtilTest.kt → 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

+ 3 - 1
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")