-
Notifications
You must be signed in to change notification settings - Fork 869
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add library instrumentation for ktor.
- Loading branch information
Showing
10 changed files
with
529 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile | ||
|
||
plugins { | ||
id("otel.library-instrumentation") | ||
|
||
id("org.jetbrains.kotlin.jvm") | ||
} | ||
|
||
dependencies { | ||
library("io.ktor:ktor-server-core:1.0.0") | ||
|
||
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") | ||
} | ||
|
||
tasks { | ||
withType(KotlinCompile::class).configureEach { | ||
kotlinOptions { | ||
jvmTarget = "1.8" | ||
} | ||
} | ||
|
||
val compileTestKotlin by existing(AbstractCompile::class) | ||
|
||
named<GroovyCompile>("compileTestGroovy") { | ||
// Note: look like it should be `classpath += files(sourceSets.test.kotlin.classesDirectory)` | ||
// instead, but kotlin plugin doesn't support it (yet?) | ||
classpath = classpath.plus(files(compileTestKotlin.get().destinationDir)) | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
...ry/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/ApplicationRequestGetter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.ktor.v1_0 | ||
|
||
import io.ktor.application.* | ||
import io.ktor.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) | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
...in/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesExtractor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.ktor.v1_0 | ||
|
||
import io.ktor.application.* | ||
import io.ktor.features.* | ||
import io.ktor.request.* | ||
import io.ktor.response.* | ||
import io.ktor.routing.* | ||
import io.opentelemetry.instrumentation.api.instrumenter.http.CapturedHttpHeaders | ||
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor | ||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes | ||
|
||
internal class KtorHttpServerAttributesExtractor(capturedHttpHeaders: CapturedHttpHeaders) : | ||
HttpServerAttributesExtractor<ApplicationRequest, ApplicationResponse>(capturedHttpHeaders) { | ||
|
||
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, response: ApplicationResponse?): String? { | ||
return null | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
...ain/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesExtractor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.ktor.v1_0 | ||
|
||
import io.ktor.features.* | ||
import io.ktor.request.* | ||
import io.ktor.response.* | ||
import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesExtractor | ||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes | ||
|
||
internal class KtorNetServerAttributesExtractor : NetServerAttributesExtractor<ApplicationRequest, ApplicationResponse>() { | ||
override fun transport(request: ApplicationRequest): String { | ||
return SemanticAttributes.NetTransportValues.IP_TCP | ||
} | ||
|
||
override fun peerName(request: ApplicationRequest): String { | ||
return request.origin.host | ||
} | ||
|
||
override fun peerPort(request: ApplicationRequest): Int { | ||
return request.origin.port | ||
} | ||
|
||
override fun peerIp(request: ApplicationRequest): String { | ||
return request.origin.remoteHost | ||
} | ||
} |
161 changes: 161 additions & 0 deletions
161
...0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.ktor.v1_0 | ||
|
||
import io.ktor.application.* | ||
import io.ktor.request.* | ||
import io.ktor.response.* | ||
import io.ktor.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.config.Config | ||
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.CapturedHttpHeaders | ||
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.servlet.ServerSpanNaming | ||
import kotlinx.coroutines.withContext | ||
|
||
class KtorServerTracing private constructor( | ||
private val instrumenter: Instrumenter<ApplicationRequest, ApplicationResponse> | ||
) { | ||
|
||
class Configuration { | ||
internal lateinit var openTelemetry: OpenTelemetry | ||
|
||
internal var capturedHttpHeaders = CapturedHttpHeaders.server(Config.get()) | ||
|
||
internal val additionalExtractors = mutableListOf<AttributesExtractor<in ApplicationRequest, in ApplicationResponse>>() | ||
|
||
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 captureHttpHeaders(capturedHttpHeaders: CapturedHttpHeaders) { | ||
this.capturedHttpHeaders = capturedHttpHeaders | ||
} | ||
|
||
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 : ApplicationFeature<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 httpAttributesExtractor = KtorHttpServerAttributesExtractor(configuration.capturedHttpHeaders) | ||
|
||
val instrumenterBuilder = Instrumenter.builder<ApplicationRequest, ApplicationResponse>( | ||
configuration.openTelemetry, | ||
INSTRUMENTATION_NAME, | ||
HttpSpanNameExtractor.create(httpAttributesExtractor) | ||
) | ||
|
||
configuration.additionalExtractors.forEach { instrumenterBuilder.addAttributesExtractor(it) } | ||
|
||
with(instrumenterBuilder) { | ||
setSpanStatusExtractor(configuration.statusExtractor(HttpSpanStatusExtractor.create(httpAttributesExtractor))) | ||
addAttributesExtractor(KtorNetServerAttributesExtractor()) | ||
addAttributesExtractor(httpAttributesExtractor) | ||
addRequestMetrics(HttpServerMetrics.get()) | ||
addContextCustomizer(ServerSpanNaming.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) { | ||
ServerSpanNaming.updateServerSpanName(context, ServerSpanNaming.Source.SERVLET, { _, arg -> arg.route.parent.toString() }, call) | ||
} | ||
} | ||
|
||
return feature | ||
} | ||
} | ||
} |
Oops, something went wrong.