-
Notifications
You must be signed in to change notification settings - Fork 860
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
46 changed files
with
1,577 additions
and
585 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
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
22 changes: 22 additions & 0 deletions
22
instrumentation/ktor/ktor-2-common/library/build.gradle.kts
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,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) | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
...src/main/kotlin/io/opentelemetry/instrumentation/ktor/client/AbstractKtorClientTracing.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,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) | ||
} | ||
} |
172 changes: 172 additions & 0 deletions
172
...n/kotlin/io/opentelemetry/instrumentation/ktor/client/AbstractKtorClientTracingBuilder.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,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) | ||
} | ||
} |
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
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
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
88 changes: 88 additions & 0 deletions
88
...y/src/main/kotlin/io/opentelemetry/instrumentation/ktor/internal/KtorClientTracingUtil.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,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) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.