Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make spanKindExtractor configurable in Ktor instrumentations #8255

Merged
merged 3 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ 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.SpanKindExtractor
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.internal.InstrumenterUtil
import kotlinx.coroutines.withContext

class KtorServerTracing private constructor(
Expand All @@ -42,6 +44,9 @@ class KtorServerTracing private constructor(
internal var statusExtractor:
(SpanStatusExtractor<ApplicationRequest, ApplicationResponse>) -> SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse> = { a -> a }

internal var spanKindExtractor:
(SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest> = { a -> a }

fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
this.openTelemetry = openTelemetry
}
Expand All @@ -52,6 +57,12 @@ class KtorServerTracing private constructor(
this.statusExtractor = extractor
}

fun setSpanKindExtractor(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not written any tests for this method, as there were no unit tests for the existing setStatusExtractor or addAttributeExtractor, is that ok?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some integration tests that exercise the entire instrumentation module.
You could perhaps use the AbstractHttpServerUsingTest and HttpServerInstrumentationExtension directly and set up a simple HTTP server with one endpoint, just to verify that the span it emits has the span kind you want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your detailed explanation, I added test cases in 959ee02.

extractor: (SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest>
) {
this.spanKindExtractor = extractor
}

fun addAttributeExtractor(extractor: AttributesExtractor<in ApplicationRequest, in ApplicationResponse>) {
additionalExtractors.add(extractor)
}
Expand Down Expand Up @@ -112,7 +123,11 @@ class KtorServerTracing private constructor(
addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter))
}

val instrumenter = instrumenterBuilder.buildServerInstrumenter(ApplicationRequestGetter)
val instrumenter = InstrumenterUtil.buildUpstreamInstrumenter(
instrumenterBuilder,
ApplicationRequestGetter,
configuration.spanKindExtractor(SpanKindExtractor.alwaysServer())
)

val feature = KtorServerTracing(instrumenter)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v1_0

import io.ktor.application.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension
import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse
import io.opentelemetry.testing.internal.armeria.common.HttpMethod
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.extension.RegisterExtension
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.Arguments.arguments
import org.junit.jupiter.params.provider.MethodSource
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import java.util.stream.Stream

class KtorServerSpanKindExtractorTest : AbstractHttpServerUsingTest<ApplicationEngine>() {

private val consumerKindEndpoint = ServerEndpoint("consumerKindEndpoint", "from-pubsub/run", 200, "")
private val serverKindEndpoint = ServerEndpoint("serverKindEndpoint", "from-client/run", 200, "")

companion object {
@JvmStatic
@RegisterExtension
val testing: InstrumentationExtension = HttpServerInstrumentationExtension.forLibrary()
}

@BeforeAll
fun setupOptions() {
startServer()
}

@AfterAll
fun cleanup() {
cleanupServer()
}

override fun getContextPath() = ""

override fun setupServer(): ApplicationEngine {
return embeddedServer(Netty, port = port) {
install(KtorServerTracing) {
setOpenTelemetry(testing.openTelemetry)
setSpanKindExtractor {
SpanKindExtractor { req ->
if (req.uri.startsWith("/from-pubsub/")) {
SpanKind.CONSUMER
} else {
SpanKind.SERVER
}
}
}
}

routing {
post(consumerKindEndpoint.path) {
call.respondText(consumerKindEndpoint.body, status = HttpStatusCode.fromValue(consumerKindEndpoint.status))
}

post(serverKindEndpoint.path) {
call.respondText(serverKindEndpoint.body, status = HttpStatusCode.fromValue(serverKindEndpoint.status))
}
}
}.start()
}

override fun stopServer(server: ApplicationEngine) {
server.stop(0, 10, TimeUnit.SECONDS)
}

@ParameterizedTest
@MethodSource("provideArguments")
fun testSpanKindExtractor(endpoint: ServerEndpoint, expectedKind: SpanKind) {
val request = AggregatedHttpRequest.of(HttpMethod.valueOf("POST"), resolveAddress(endpoint))
val response: AggregatedHttpResponse = client.execute(request).aggregate().join()
assertThat(response.status().code()).isEqualTo(endpoint.status)

testing.waitAndAssertTraces(
Consumer { trace ->
trace.hasSpansSatisfyingExactly(
Consumer { span ->
span.hasKind(expectedKind)
}
)
}
)
}

private fun provideArguments(): Stream<Arguments> {
return Stream.of(
arguments(consumerKindEndpoint, SpanKind.CONSUMER),
arguments(serverKindEndpoint, SpanKind.SERVER),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ 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.SpanKindExtractor
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.internal.InstrumenterUtil
import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
import kotlinx.coroutines.withContext

Expand All @@ -43,6 +45,9 @@ class KtorServerTracing private constructor(
internal var statusExtractor:
(SpanStatusExtractor<ApplicationRequest, ApplicationResponse>) -> SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse> = { a -> a }

internal var spanKindExtractor:
(SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest> = { a -> a }

fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
this.openTelemetry = openTelemetry
}
Expand All @@ -53,6 +58,12 @@ class KtorServerTracing private constructor(
this.statusExtractor = extractor
}

fun setSpanKindExtractor(
extractor: (SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest>
) {
this.spanKindExtractor = extractor
}

fun addAttributeExtractor(extractor: AttributesExtractor<in ApplicationRequest, in ApplicationResponse>) {
additionalExtractors.add(extractor)
}
Expand Down Expand Up @@ -112,7 +123,11 @@ class KtorServerTracing private constructor(
addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter))
}

val instrumenter = instrumenterBuilder.buildServerInstrumenter(ApplicationRequestGetter)
val instrumenter = InstrumenterUtil.buildUpstreamInstrumenter(
instrumenterBuilder,
ApplicationRequestGetter,
configuration.spanKindExtractor(SpanKindExtractor.alwaysServer())
)

val feature = KtorServerTracing(instrumenter)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v2_0.server

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension
import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse
import io.opentelemetry.testing.internal.armeria.common.HttpMethod
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.extension.RegisterExtension
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.Arguments.arguments
import org.junit.jupiter.params.provider.MethodSource
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import java.util.stream.Stream

class KtorServerSpanKindExtractorTest : AbstractHttpServerUsingTest<ApplicationEngine>() {

private val consumerKindEndpoint = ServerEndpoint("consumerKindEndpoint", "from-pubsub/run", 200, "")
private val serverKindEndpoint = ServerEndpoint("serverKindEndpoint", "from-client/run", 200, "")

companion object {
@JvmStatic
@RegisterExtension
val testing: InstrumentationExtension = HttpServerInstrumentationExtension.forLibrary()
}

@BeforeAll
fun setupOptions() {
startServer()
}

@AfterAll
fun cleanup() {
cleanupServer()
}

override fun getContextPath() = ""

override fun setupServer(): ApplicationEngine {
return embeddedServer(Netty, port = port) {
install(KtorServerTracing) {
setOpenTelemetry(testing.openTelemetry)
setSpanKindExtractor {
SpanKindExtractor { req ->
if (req.uri.startsWith("/from-pubsub/")) {
SpanKind.CONSUMER
} else {
SpanKind.SERVER
}
}
}
}

routing {
post(consumerKindEndpoint.path) {
call.respondText(consumerKindEndpoint.body, status = HttpStatusCode.fromValue(consumerKindEndpoint.status))
}

post(serverKindEndpoint.path) {
call.respondText(serverKindEndpoint.body, status = HttpStatusCode.fromValue(serverKindEndpoint.status))
}
}
}.start()
}

override fun stopServer(server: ApplicationEngine) {
server.stop(0, 10, TimeUnit.SECONDS)
}

@ParameterizedTest
@MethodSource("provideArguments")
fun testSpanKindExtractor(endpoint: ServerEndpoint, expectedKind: SpanKind) {
val request = AggregatedHttpRequest.of(HttpMethod.valueOf("POST"), resolveAddress(endpoint))
val response: AggregatedHttpResponse = client.execute(request).aggregate().join()
assertThat(response.status().code()).isEqualTo(endpoint.status)

testing.waitAndAssertTraces(
Consumer { trace ->
trace.hasSpansSatisfyingExactly(
Consumer { span ->
span.hasKind(expectedKind)
}
)
}
)
}

private fun provideArguments(): Stream<Arguments> {
return Stream.of(
arguments(consumerKindEndpoint, SpanKind.CONSUMER),
arguments(serverKindEndpoint, SpanKind.SERVER),
)
}
}