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

Demonstrate bug: span hierarchy is incorrect when client call is nested within an internal span #510

Draft
wants to merge 5 commits into
base: 6.5.x
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
micronaut = "4.3.3"
micronaut = "4.3.5"
micronaut-test = "4.2.0"
micronaut-logging = "1.2.2"
micronaut-platform = "4.2.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import io.opentracing.Tracer
import spock.lang.AutoCleanup
import spock.lang.PendingFeature
import spock.lang.Specification
import spock.util.concurrent.PollingConditions

Expand Down Expand Up @@ -513,7 +512,6 @@ class HttpTracingExclusionSpec extends Specification {
}
}

@PendingFeature
void 'test continue nested HTTP tracing - reactive'() {
when:
HttpResponse<String> response = client.toBlocking().exchange('/traced/nestedReactive/John', String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter
import io.opentelemetry.semconv.SemanticAttributes
import io.reactivex.Single
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
Expand All @@ -50,6 +51,7 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage

import static io.micronaut.scheduling.TaskExecutors.IO
import static java.nio.charset.StandardCharsets.UTF_8

@Slf4j("LOG")
class OpenTelemetryHttpSpec extends Specification {
Expand Down Expand Up @@ -407,10 +409,116 @@ class OpenTelemetryHttpSpec extends Specification {
exporter.reset()
}

void 'test consecutive sibling client calls'() {
def internalSpanCount = 0
def serverSpanCount = 3
def clientSpanCount = 2

when:
HttpResponse<String> response = httpClient.toBlocking().exchange('/words/quad?input=foo', String)

then:
response.body() == 'foo foo foo foo'

and: 'all spans are finished'
conditions.eventually {
hasSpans(internalSpanCount, serverSpanCount, clientSpanCount)
def mainServerSpan = exporter.finishedSpanItems.find { it.kind == SpanKind.SERVER && it.name == 'GET /words/quad' }
def clientSpans = exporter.finishedSpanItems.stream().filter { it.kind == SpanKind.CLIENT }.toList()
clientSpans.stream().allMatch { cs -> cs.parentSpanId == mainServerSpan.spanId && cs.traceId == mainServerSpan.traceId }
hasHttpSemanticAttributes(HttpStatus.OK)
}

cleanup:
exporter.reset()
}

void 'test tracing with Mono->CompletableFuture conversion'() {
def internalSpanCount = 1
def serverSpanCount = 2
def clientSpanCount = 1

when:
HttpResponse<String> response = httpClient.toBlocking().exchange('/future/test?input=' + URLEncoder.encode(' foo ', UTF_8), String)

then:
response.body() == '(foo foo)'

and: 'all spans are finished'
conditions.eventually {
hasSpans(internalSpanCount, serverSpanCount, clientSpanCount)
def mainServerSpan = exporter.finishedSpanItems.find { it.kind == SpanKind.SERVER && it.name == 'GET /future/test' }
def internalSpan = exporter.finishedSpanItems.find { it.kind == SpanKind.INTERNAL }
def clientSpan = exporter.finishedSpanItems.find { it.kind == SpanKind.CLIENT }
internalSpan.name == 'InternalWorker.transform'
internalSpan.traceId == mainServerSpan.traceId
internalSpan.parentSpanId == mainServerSpan.spanId
clientSpan.traceId == mainServerSpan.traceId
clientSpan.parentSpanId == internalSpan.spanId // it is actually equal to mainServerSpan.spanId instead
hasHttpSemanticAttributes(HttpStatus.OK)
}

cleanup:
exporter.reset()
}

@Introspected
static class SomeBody {
}

@Client("/words")
static interface WordsClient {

@Get(uri = '/double')
Mono<String> doubleWords(@QueryValue String input)
}

@Controller('/words')
static class WordsController {

@Inject
@Client
WordsClient downstreamClient

@Get("/double")
Mono<String> doubleWords(@QueryValue String input) {
Mono.just(input.isEmpty() ? '' : "$input $input")
}

@Get("/quad")
Mono<String> quadrupleWords(@QueryValue String input) {
downstreamClient.doubleWords(input)
.flatMap { resp -> downstreamClient.doubleWords(resp) }
}
}

@Singleton
static class InternalWorker {

@Inject
@Client
WordsClient downstreamClient

@NewSpan
Mono<String> transform(@SpanTag String input) {
return Mono.just(input.trim())
.flatMap { s -> downstreamClient.doubleWords(s.trim()) }
.map {"($it)".toString() }
}
}

@Controller("/future")
static class FutureController {

@Inject
InternalWorker internalWorker

@Get("/test")
CompletableFuture<String> test(@QueryValue String input) {
return internalWorker.transform(input).toFuture()
}
}

@Controller("/annotations")
static class TestController {

Expand Down