Skip to content

Commit

Permalink
Add ktor 3 instrumentation (#12562)
Browse files Browse the repository at this point in the history
  • Loading branch information
laurit authored Nov 11, 2024
1 parent fdfb764 commit 76c2d2d
Show file tree
Hide file tree
Showing 46 changed files with 1,577 additions and 585 deletions.
2 changes: 1 addition & 1 deletion docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ These are the supported libraries and frameworks:
| [Jodd Http](https://http.jodd.org/) | 4.2+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | N/A | Controller Spans [3] |
| [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation |
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library),<br>[opentelemetry-ktor-3.0](../instrumentation/ktor/ktor-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [Kubernetes Client](https://github.com/kubernetes-client/java) | 7.0+ | N/A | [HTTP Client Spans] |
| [Lettuce](https://github.com/lettuce-io/lettuce-core) | 4.0+ | [opentelemetry-lettuce-5.1](../instrumentation/lettuce/lettuce-5.1/library) | [Database Client Spans] |
| [Log4j 1](https://logging.apache.org/log4j/1.2/) | 1.2+ | N/A | none |
Expand Down
2 changes: 1 addition & 1 deletion instrumentation/ktor/ktor-1.0/library/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Library Instrumentation for Ktor versions 1.x
# Library Instrumentation for Ktor version 1.x

This package contains libraries to help instrument Ktor. Currently, only server instrumentation is supported.

Expand Down
22 changes: 22 additions & 0 deletions instrumentation/ktor/ktor-2-common/library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion

plugins {
id("otel.library-instrumentation")
id("org.jetbrains.kotlin.jvm")
}
dependencies {
implementation(project(":instrumentation:ktor:ktor-common:library"))
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compileOnly("io.ktor:ktor-client-core:2.0.0")
compileOnly("io.ktor:ktor-server-core:2.0.0")
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
@Suppress("deprecation")
languageVersion.set(KotlinVersion.KOTLIN_1_4)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.client

import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.opentelemetry.context.Context
import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter

abstract class AbstractKtorClientTracing(
private val instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
private val propagators: ContextPropagators,
) {

internal fun createSpan(requestBuilder: HttpRequestBuilder): Context? {
val parentContext = Context.current()
val requestData = requestBuilder.build()

return if (instrumenter.shouldStart(parentContext, requestData)) {
instrumenter.start(parentContext, requestData)
} else {
null
}
}

internal fun populateRequestHeaders(requestBuilder: HttpRequestBuilder, context: Context) {
propagators.textMapPropagator.inject(context, requestBuilder, KtorHttpHeadersSetter)
}

internal fun endSpan(context: Context, call: HttpClientCall, error: Throwable?) {
endSpan(context, HttpRequestBuilder().takeFrom(call.request), call.response, error)
}

internal fun endSpan(context: Context, requestBuilder: HttpRequestBuilder, response: HttpResponse?, error: Throwable?) {
instrumenter.end(context, requestBuilder.build(), response, error)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.client

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.common.AttributesBuilder
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil

abstract class AbstractKtorClientTracingBuilder(
private val instrumentationName: String
) {
companion object {
init {
KtorBuilderUtil.clientBuilderExtractor = { it.clientBuilder }
}
}

internal lateinit var openTelemetry: OpenTelemetry
protected lateinit var clientBuilder: DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>

fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
this.openTelemetry = openTelemetry
this.clientBuilder = DefaultHttpClientInstrumenterBuilder.create(
instrumentationName,
openTelemetry,
KtorHttpClientAttributesGetter
)
}

protected fun getOpenTelemetry(): OpenTelemetry {
return openTelemetry
}

@Deprecated(
"Please use method `capturedRequestHeaders`",
ReplaceWith("capturedRequestHeaders(headers.asIterable())")
)
fun setCapturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())

@Deprecated(
"Please use method `capturedRequestHeaders`",
ReplaceWith("capturedRequestHeaders(headers)")
)
fun setCapturedRequestHeaders(headers: List<String>) = capturedRequestHeaders(headers)

fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())

fun capturedRequestHeaders(headers: Iterable<String>) {
clientBuilder.setCapturedRequestHeaders(headers.toList())
}

@Deprecated(
"Please use method `capturedResponseHeaders`",
ReplaceWith("capturedResponseHeaders(headers.asIterable())")
)
fun setCapturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())

@Deprecated(
"Please use method `capturedResponseHeaders`",
ReplaceWith("capturedResponseHeaders(headers)")
)
fun setCapturedResponseHeaders(headers: List<String>) = capturedResponseHeaders(headers)

fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())

fun capturedResponseHeaders(headers: Iterable<String>) {
clientBuilder.setCapturedResponseHeaders(headers.toList())
}

@Deprecated(
"Please use method `knownMethods`",
ReplaceWith("knownMethods(knownMethods)")
)
fun setKnownMethods(knownMethods: Set<String>) = knownMethods(knownMethods)

fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())

fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())

@JvmName("knownMethodsJvm")
fun knownMethods(methods: Iterable<HttpMethod>) = knownMethods(methods.map { it.value })

fun knownMethods(methods: Iterable<String>) {
clientBuilder.setKnownMethods(methods.toSet())
}

@Deprecated("Please use method `attributeExtractor`")
fun addAttributesExtractors(vararg extractors: AttributesExtractor<in HttpRequestData, in HttpResponse>) = addAttributesExtractors(extractors.asList())

@Deprecated("Please use method `attributeExtractor`")
fun addAttributesExtractors(extractors: Iterable<AttributesExtractor<in HttpRequestData, in HttpResponse>>) {
extractors.forEach {
attributeExtractor {
onStart { it.onStart(attributes, parentContext, request) }
onEnd { it.onEnd(attributes, parentContext, request, response, error) }
}
}
}

fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
val builder = ExtractorBuilder().apply(extractorBuilder).build()
this.clientBuilder.addAttributeExtractor(
object : AttributesExtractor<HttpRequestData, HttpResponse> {
override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: HttpRequestData) {
builder.onStart(OnStartData(attributes, parentContext, request))
}

override fun onEnd(attributes: AttributesBuilder, context: Context, request: HttpRequestData, response: HttpResponse?, error: Throwable?) {
builder.onEnd(OnEndData(attributes, context, request, response, error))
}
}
)
}

class ExtractorBuilder {
private var onStart: OnStartData.() -> Unit = {}
private var onEnd: OnEndData.() -> Unit = {}

fun onStart(block: OnStartData.() -> Unit) {
onStart = block
}

fun onEnd(block: OnEndData.() -> Unit) {
onEnd = block
}

internal fun build(): Extractor {
return Extractor(onStart, onEnd)
}
}

internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)

data class OnStartData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: HttpRequestData
)

data class OnEndData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: HttpRequestData,
val response: HttpResponse?,
val error: Throwable?
)

/**
* Configures the instrumentation to emit experimental HTTP client metrics.
*
* @param emitExperimentalHttpClientMetrics `true` if the experimental HTTP client metrics are to be emitted.
*/
@Deprecated("Please use method `emitExperimentalHttpClientMetrics`")
fun setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics: Boolean) {
if (emitExperimentalHttpClientMetrics) {
emitExperimentalHttpClientMetrics()
}
}

fun emitExperimentalHttpClientMetrics() {
clientBuilder.setEmitExperimentalHttpClientMetrics(true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v2_0.client
package io.opentelemetry.instrumentation.ktor.client

import io.ktor.client.request.*
import io.ktor.client.statement.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v2_0.client
package io.opentelemetry.instrumentation.ktor.client

import io.ktor.client.request.HttpRequestBuilder
import io.opentelemetry.context.propagation.TextMapSetter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v2_0.internal
package io.opentelemetry.instrumentation.ktor.internal

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder
import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
object KtorBuilderUtil {
lateinit var clientBuilderExtractor: (KtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
lateinit var serverBuilderExtractor: (KtorServerTracing.Configuration) -> DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
lateinit var clientBuilderExtractor: (AbstractKtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
lateinit var serverBuilderExtractor: (
AbstractKtorServerTracingBuilder
) -> DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.internal

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.opentelemetry.context.Context
import io.opentelemetry.extension.kotlin.asContextElement
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
object KtorClientTracingUtil {
private val openTelemetryContextKey = AttributeKey<Context>("OpenTelemetry")

fun install(plugin: AbstractKtorClientTracing, scope: HttpClient) {
installSpanCreation(plugin, scope)
installSpanEnd(plugin, scope)
}

private fun installSpanCreation(plugin: AbstractKtorClientTracing, scope: HttpClient) {
val initializeRequestPhase = PipelinePhase("OpenTelemetryInitializeRequest")
scope.requestPipeline.insertPhaseAfter(HttpRequestPipeline.State, initializeRequestPhase)

scope.requestPipeline.intercept(initializeRequestPhase) {
val openTelemetryContext = HttpClientRequestResendCount.initialize(Context.current())
withContext(openTelemetryContext.asContextElement()) { proceed() }
}

val createSpanPhase = PipelinePhase("OpenTelemetryCreateSpan")
scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, createSpanPhase)

scope.sendPipeline.intercept(createSpanPhase) {
val requestBuilder = context
val openTelemetryContext = plugin.createSpan(requestBuilder)

if (openTelemetryContext != null) {
try {
requestBuilder.attributes.put(openTelemetryContextKey, openTelemetryContext)
plugin.populateRequestHeaders(requestBuilder, openTelemetryContext)

withContext(openTelemetryContext.asContextElement()) { proceed() }
} catch (e: Throwable) {
plugin.endSpan(openTelemetryContext, requestBuilder, null, e)
throw e
}
} else {
proceed()
}
}
}

@OptIn(InternalCoroutinesApi::class)
private fun installSpanEnd(plugin: AbstractKtorClientTracing, scope: HttpClient) {
val endSpanPhase = PipelinePhase("OpenTelemetryEndSpan")
scope.receivePipeline.insertPhaseBefore(HttpReceivePipeline.State, endSpanPhase)

scope.receivePipeline.intercept(endSpanPhase) {
val openTelemetryContext = it.call.attributes.getOrNull(openTelemetryContextKey)
openTelemetryContext ?: return@intercept

scope.launch {
val job = it.call.coroutineContext.job
job.join()
val cause = if (!job.isCancelled) {
null
} else {
kotlin.runCatching { job.getCancellationException() }.getOrNull()
}

plugin.endSpan(openTelemetryContext, it.call, cause)
}
}
}
}
Loading

0 comments on commit 76c2d2d

Please sign in to comment.