Skip to content

Commit

Permalink
Add library instrumentation for ktor 2 (#5797)
Browse files Browse the repository at this point in the history
  • Loading branch information
laurit authored Apr 11, 2022
1 parent 6edab71 commit 91066a8
Show file tree
Hide file tree
Showing 21 changed files with 521 additions and 5 deletions.
2 changes: 2 additions & 0 deletions docs/standalone-library-instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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.+")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
18 changes: 18 additions & 0 deletions instrumentation/ktor/ktor-2.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)
}
}
```
28 changes: 28 additions & 0 deletions instrumentation/ktor/ktor-2.0/library/build.gradle.kts
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"
}
}
}
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)
}
}
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
}
}
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
}
}
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
}
}
}
Loading

0 comments on commit 91066a8

Please sign in to comment.