Skip to content

Commit

Permalink
Add library instrumentation for ktor. (#4983)
Browse files Browse the repository at this point in the history
  • Loading branch information
anuraaga committed Jan 5, 2022
1 parent 7c58220 commit e08ed9d
Show file tree
Hide file tree
Showing 10 changed files with 529 additions and 0 deletions.
18 changes: 18 additions & 0 deletions instrumentation/ktor-1.0/library/README.md
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)
}
}
```
37 changes: 37 additions & 0 deletions instrumentation/ktor-1.0/library/build.gradle.kts
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))
}
}
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)
}
}
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
}
}
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
}
}
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
}
}
}
Loading

0 comments on commit e08ed9d

Please sign in to comment.