-
Notifications
You must be signed in to change notification settings - Fork 892
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 2
- Loading branch information
Showing
21 changed files
with
524 additions
and
5 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
File renamed without changes.
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
File renamed without changes.
File renamed without changes.
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
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,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 changes: 19 additions & 0 deletions
19
...ry/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_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,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 changes: 73 additions & 0 deletions
73
.../main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorHttpServerAttributesGetter.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,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 changes: 29 additions & 0 deletions
29
...c/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/KtorNetServerAttributesGetter.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,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 changes: 166 additions & 0 deletions
166
...0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_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,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 | ||
} | ||
} | ||
} |
Oops, something went wrong.