Refactor Reactor-Netty 1.0 tests to Java (#6497)
This commit is contained in:
parent
33d2e40a9e
commit
892fb8a38e
|
@ -15,9 +15,10 @@ import reactor.util.context.ContextView;
|
||||||
|
|
||||||
public final class DecoratorFunctions {
|
public final class DecoratorFunctions {
|
||||||
|
|
||||||
// ignore our own callbacks - or already decorated functions
|
// ignore already decorated functions
|
||||||
public static boolean shouldDecorate(Class<?> callbackClass) {
|
public static boolean shouldDecorate(Class<?> callbackClass) {
|
||||||
return !callbackClass.getName().startsWith("io.opentelemetry.javaagent");
|
return callbackClass != OnMessageDecorator.class
|
||||||
|
&& callbackClass != OnMessageErrorDecorator.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class OnMessageDecorator<M extends HttpClientInfos>
|
public static final class OnMessageDecorator<M extends HttpClientInfos>
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright The OpenTelemetry Authors
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import io.opentelemetry.instrumentation.test.AgentTestTrait
|
|
||||||
import io.opentelemetry.instrumentation.test.InstrumentationSpecification
|
|
||||||
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer
|
|
||||||
import io.opentelemetry.test.reactor.netty.TracedWithSpan
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import reactor.netty.http.client.HttpClient
|
|
||||||
import reactor.test.StepVerifier
|
|
||||||
import spock.lang.Shared
|
|
||||||
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
import static io.opentelemetry.api.trace.SpanKind.CLIENT
|
|
||||||
import static io.opentelemetry.api.trace.SpanKind.INTERNAL
|
|
||||||
import static io.opentelemetry.api.trace.SpanKind.SERVER
|
|
||||||
|
|
||||||
class ReactorNettyWithSpanTest extends InstrumentationSpecification implements AgentTestTrait {
|
|
||||||
|
|
||||||
@Shared
|
|
||||||
private HttpClientTestServer server
|
|
||||||
|
|
||||||
def setupSpec() {
|
|
||||||
server = new HttpClientTestServer(openTelemetry)
|
|
||||||
server.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
def cleanupSpec() {
|
|
||||||
server.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
def "test successful nested under WithSpan"() {
|
|
||||||
when:
|
|
||||||
def httpClient = HttpClient.create()
|
|
||||||
|
|
||||||
def httpRequest = Mono.defer({ ->
|
|
||||||
httpClient.get().uri("http://localhost:${server.httpPort()}/success")
|
|
||||||
.responseSingle({ resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map { resp }
|
|
||||||
})
|
|
||||||
.map({ r -> r.status().code() })
|
|
||||||
})
|
|
||||||
|
|
||||||
def getResponse = new TracedWithSpan().mono(
|
|
||||||
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/4348
|
|
||||||
// our HTTP server is synchronous, i.e. it returns Mono.just with response
|
|
||||||
// which is not supported by TracingSubscriber - it does not instrument scalar calls
|
|
||||||
// so we delay here to fake async http request and let Reactor context instrumentation work
|
|
||||||
Mono.delay(Duration.ofMillis(1)).then(httpRequest))
|
|
||||||
|
|
||||||
then:
|
|
||||||
StepVerifier.create(getResponse)
|
|
||||||
.expectNext(200)
|
|
||||||
.expectComplete()
|
|
||||||
.verify()
|
|
||||||
|
|
||||||
assertTraces(1) {
|
|
||||||
trace(0, 3) {
|
|
||||||
span(0) {
|
|
||||||
name "TracedWithSpan.mono"
|
|
||||||
kind INTERNAL
|
|
||||||
hasNoParent()
|
|
||||||
}
|
|
||||||
span(1) {
|
|
||||||
name "HTTP GET"
|
|
||||||
kind CLIENT
|
|
||||||
childOf(span(0))
|
|
||||||
}
|
|
||||||
span(2) {
|
|
||||||
name "test-http-server"
|
|
||||||
kind SERVER
|
|
||||||
childOf(span(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,278 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright The OpenTelemetry Authors
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0
|
|
||||||
|
|
||||||
import io.netty.resolver.AddressResolver
|
|
||||||
import io.netty.resolver.AddressResolverGroup
|
|
||||||
import io.netty.resolver.InetNameResolver
|
|
||||||
import io.netty.util.concurrent.EventExecutor
|
|
||||||
import io.netty.util.concurrent.Promise
|
|
||||||
import io.opentelemetry.api.common.AttributeKey
|
|
||||||
import io.opentelemetry.api.trace.Span
|
|
||||||
import io.opentelemetry.api.trace.SpanKind
|
|
||||||
import io.opentelemetry.api.trace.StatusCode
|
|
||||||
import io.opentelemetry.instrumentation.test.AgentTestTrait
|
|
||||||
import io.opentelemetry.instrumentation.test.base.HttpClientTest
|
|
||||||
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest
|
|
||||||
import io.opentelemetry.sdk.trace.data.SpanData
|
|
||||||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
|
|
||||||
import java.time.Duration
|
|
||||||
import reactor.netty.http.client.HttpClient
|
|
||||||
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
|
|
||||||
import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT
|
|
||||||
|
|
||||||
abstract class AbstractReactorNettyHttpClientTest extends HttpClientTest<HttpClient.ResponseReceiver> implements AgentTestTrait {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean testRedirects() {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean testReadTimeout() {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String userAgent() {
|
|
||||||
return "ReactorNetty"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
HttpClient.ResponseReceiver buildRequest(String method, URI uri, Map<String, String> headers) {
|
|
||||||
def client = createHttpClient()
|
|
||||||
.followRedirect(true)
|
|
||||||
.headers({ h -> headers.each { k, v -> h.add(k, v) } })
|
|
||||||
.baseUrl(resolveAddress("").toString())
|
|
||||||
if (uri.toString().contains("/read-timeout")) {
|
|
||||||
client = client.responseTimeout(Duration.ofMillis(READ_TIMEOUT_MS))
|
|
||||||
}
|
|
||||||
return client."${method.toLowerCase()}"()
|
|
||||||
.uri(uri.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
int sendRequest(HttpClient.ResponseReceiver request, String method, URI uri, Map<String, String> headers) {
|
|
||||||
return request.responseSingle { resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map {
|
|
||||||
resp
|
|
||||||
}
|
|
||||||
}.block().status().code()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void sendRequestWithCallback(HttpClient.ResponseReceiver request, String method, URI uri, Map<String, String> headers, AbstractHttpClientTest.RequestResult requestResult) {
|
|
||||||
request.responseSingle { resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map { resp }
|
|
||||||
}.subscribe({
|
|
||||||
requestResult.complete(it.status().code())
|
|
||||||
}, { throwable ->
|
|
||||||
requestResult.complete(throwable)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Throwable clientSpanError(URI uri, Throwable exception) {
|
|
||||||
if (exception.class.getName().endsWith("ReactiveException")) {
|
|
||||||
switch (uri.toString()) {
|
|
||||||
case "http://localhost:61/": // unopened port
|
|
||||||
case "https://192.0.2.1/": // non routable address
|
|
||||||
exception = exception.getCause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return exception
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Set<AttributeKey<?>> httpAttributes(URI uri) {
|
|
||||||
switch (uri.toString()) {
|
|
||||||
case "http://localhost:61/": // unopened port
|
|
||||||
case "https://192.0.2.1/": // non routable address
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
def attributes = super.httpAttributes(uri)
|
|
||||||
if (uri.toString().contains("/read-timeout")) {
|
|
||||||
attributes.remove(SemanticAttributes.NET_PEER_NAME)
|
|
||||||
attributes.remove(SemanticAttributes.NET_PEER_PORT)
|
|
||||||
attributes.remove(SemanticAttributes.HTTP_FLAVOR)
|
|
||||||
}
|
|
||||||
attributes
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract HttpClient createHttpClient()
|
|
||||||
|
|
||||||
AddressResolverGroup getAddressResolverGroup() {
|
|
||||||
return CustomNameResolverGroup.INSTANCE
|
|
||||||
}
|
|
||||||
|
|
||||||
def "should expose context to http client callbacks"() {
|
|
||||||
given:
|
|
||||||
def onRequestSpan = new AtomicReference<Span>()
|
|
||||||
def afterRequestSpan = new AtomicReference<Span>()
|
|
||||||
def onResponseSpan = new AtomicReference<Span>()
|
|
||||||
def afterResponseSpan = new AtomicReference<Span>()
|
|
||||||
def latch = new CountDownLatch(1)
|
|
||||||
|
|
||||||
def httpClient = createHttpClient()
|
|
||||||
.doOnRequest({ rq, con -> onRequestSpan.set(Span.current()) })
|
|
||||||
.doAfterRequest({ rq, con -> afterRequestSpan.set(Span.current()) })
|
|
||||||
.doOnResponse({ rs, con -> onResponseSpan.set(Span.current()) })
|
|
||||||
.doAfterResponseSuccess({ rs, con ->
|
|
||||||
afterResponseSpan.set(Span.current())
|
|
||||||
latch.countDown()
|
|
||||||
})
|
|
||||||
|
|
||||||
when:
|
|
||||||
runWithSpan("parent") {
|
|
||||||
httpClient.baseUrl(resolveAddress("").toString())
|
|
||||||
.get()
|
|
||||||
.uri("/success")
|
|
||||||
.responseSingle { resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map { resp }
|
|
||||||
}
|
|
||||||
.block()
|
|
||||||
}
|
|
||||||
latch.await()
|
|
||||||
|
|
||||||
then:
|
|
||||||
assertTraces(1) {
|
|
||||||
trace(0, 3) {
|
|
||||||
def parentSpan = span(0)
|
|
||||||
def nettyClientSpan = span(1)
|
|
||||||
|
|
||||||
span(0) {
|
|
||||||
name "parent"
|
|
||||||
kind SpanKind.INTERNAL
|
|
||||||
hasNoParent()
|
|
||||||
}
|
|
||||||
clientSpan(it, 1, parentSpan, "GET", resolveAddress("/success"))
|
|
||||||
serverSpan(it, 2, nettyClientSpan)
|
|
||||||
|
|
||||||
assertSameSpan(parentSpan, onRequestSpan)
|
|
||||||
assertSameSpan(nettyClientSpan, afterRequestSpan)
|
|
||||||
assertSameSpan(nettyClientSpan, onResponseSpan)
|
|
||||||
assertSameSpan(parentSpan, afterResponseSpan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def "should expose context to http request error callback"() {
|
|
||||||
given:
|
|
||||||
def onRequestErrorSpan = new AtomicReference<Span>()
|
|
||||||
|
|
||||||
def httpClient = createHttpClient()
|
|
||||||
.doOnRequestError({ rq, err -> onRequestErrorSpan.set(Span.current()) })
|
|
||||||
|
|
||||||
when:
|
|
||||||
runWithSpan("parent") {
|
|
||||||
httpClient.get()
|
|
||||||
.uri("http://localhost:$UNUSABLE_PORT/")
|
|
||||||
.response()
|
|
||||||
.block()
|
|
||||||
}
|
|
||||||
|
|
||||||
then:
|
|
||||||
def ex = thrown(Exception)
|
|
||||||
|
|
||||||
assertTraces(1) {
|
|
||||||
trace(0, 2) {
|
|
||||||
def parentSpan = span(0)
|
|
||||||
|
|
||||||
span(0) {
|
|
||||||
name "parent"
|
|
||||||
kind SpanKind.INTERNAL
|
|
||||||
hasNoParent()
|
|
||||||
status StatusCode.ERROR
|
|
||||||
errorEvent(ex.class, ex.message)
|
|
||||||
}
|
|
||||||
span(1) {
|
|
||||||
def actualException = ex.cause
|
|
||||||
kind SpanKind.CLIENT
|
|
||||||
childOf parentSpan
|
|
||||||
status StatusCode.ERROR
|
|
||||||
errorEvent(actualException.class, actualException.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertSameSpan(parentSpan, onRequestErrorSpan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def "should not leak connections"() {
|
|
||||||
given:
|
|
||||||
def uniqueChannelHashes = new HashSet<>()
|
|
||||||
def httpClient = createHttpClient()
|
|
||||||
.doOnConnect({ uniqueChannelHashes.add(it.channelHash()) })
|
|
||||||
def uri = "http://localhost:${server.httpPort()}/success"
|
|
||||||
|
|
||||||
def count = 100
|
|
||||||
|
|
||||||
when:
|
|
||||||
(1..count).forEach({
|
|
||||||
runWithSpan("parent") {
|
|
||||||
def status = httpClient.get().uri(uri)
|
|
||||||
.responseSingle { resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map { resp.status().code() }
|
|
||||||
}.block()
|
|
||||||
assert status == 200
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
then:
|
|
||||||
traces.size() == count
|
|
||||||
uniqueChannelHashes.size() == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
static void assertSameSpan(SpanData expected, AtomicReference<Span> actual) {
|
|
||||||
def expectedSpanContext = expected.spanContext
|
|
||||||
def actualSpanContext = actual.get().spanContext
|
|
||||||
assert expectedSpanContext.traceId == actualSpanContext.traceId
|
|
||||||
assert expectedSpanContext.spanId == actualSpanContext.spanId
|
|
||||||
}
|
|
||||||
|
|
||||||
// custom address resolver that returns at most one address for each host
|
|
||||||
// adapted from io.netty.resolver.DefaultAddressResolverGroup
|
|
||||||
static class CustomNameResolverGroup extends AddressResolverGroup<InetSocketAddress> {
|
|
||||||
public static final CustomNameResolverGroup INSTANCE = new CustomNameResolverGroup()
|
|
||||||
|
|
||||||
private CustomNameResolverGroup() {
|
|
||||||
}
|
|
||||||
|
|
||||||
protected AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) throws Exception {
|
|
||||||
return (new CustomNameResolver(executor)).asAddressResolver()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class CustomNameResolver extends InetNameResolver {
|
|
||||||
CustomNameResolver(EventExecutor executor) {
|
|
||||||
super(executor)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
|
|
||||||
try {
|
|
||||||
promise.setSuccess(InetAddress.getByName(inetHost))
|
|
||||||
} catch (UnknownHostException exception) {
|
|
||||||
promise.setFailure(exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void doResolveAll(String inetHost, Promise<List<InetAddress>> promise) throws Exception {
|
|
||||||
try {
|
|
||||||
// default implementation calls InetAddress.getAllByName
|
|
||||||
promise.setSuccess(Collections.singletonList(InetAddress.getByName(inetHost)))
|
|
||||||
} catch (UnknownHostException exception) {
|
|
||||||
promise.setFailure(exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,208 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright The OpenTelemetry Authors
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0
|
|
||||||
|
|
||||||
import io.netty.handler.ssl.SslContextBuilder
|
|
||||||
import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
|
|
||||||
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer
|
|
||||||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
|
|
||||||
import reactor.netty.http.client.HttpClient
|
|
||||||
import reactor.netty.tcp.SslProvider
|
|
||||||
import spock.lang.Shared
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLHandshakeException
|
|
||||||
|
|
||||||
import static io.opentelemetry.api.trace.SpanKind.CLIENT
|
|
||||||
import static io.opentelemetry.api.trace.SpanKind.INTERNAL
|
|
||||||
import static io.opentelemetry.api.trace.SpanKind.SERVER
|
|
||||||
import static io.opentelemetry.api.trace.StatusCode.ERROR
|
|
||||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_1_1
|
|
||||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP
|
|
||||||
|
|
||||||
class ReactorNettyClientSslTest extends AgentInstrumentationSpecification {
|
|
||||||
|
|
||||||
@Shared
|
|
||||||
private HttpClientTestServer server
|
|
||||||
|
|
||||||
def setupSpec() {
|
|
||||||
server = new HttpClientTestServer(openTelemetry)
|
|
||||||
server.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
def cleanupSpec() {
|
|
||||||
server.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
def "should fail SSL handshake"() {
|
|
||||||
given:
|
|
||||||
def httpClient = createHttpClient(["SSLv3"])
|
|
||||||
def uri = "https://localhost:${server.httpsPort()}/success"
|
|
||||||
|
|
||||||
when:
|
|
||||||
def responseMono = httpClient.get().uri(uri)
|
|
||||||
.responseSingle { resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map { resp }
|
|
||||||
}
|
|
||||||
|
|
||||||
runWithSpan("parent") {
|
|
||||||
responseMono.block()
|
|
||||||
}
|
|
||||||
|
|
||||||
then:
|
|
||||||
Throwable thrownException = thrown()
|
|
||||||
|
|
||||||
assertTraces(1) {
|
|
||||||
trace(0, 5) {
|
|
||||||
def list = Arrays.asList("RESOLVE", "CONNECT", "SSL handshake")
|
|
||||||
spans.subList(2, 5).sort(Comparator.comparing { item -> list.indexOf(item.name) })
|
|
||||||
span(0) {
|
|
||||||
name "parent"
|
|
||||||
status ERROR
|
|
||||||
errorEvent(thrownException.class, thrownException.message)
|
|
||||||
}
|
|
||||||
span(1) {
|
|
||||||
name "HTTP GET"
|
|
||||||
kind CLIENT
|
|
||||||
childOf span(0)
|
|
||||||
status ERROR
|
|
||||||
// netty swallows the exception, it doesn't make any sense to hard-code the message
|
|
||||||
errorEventWithAnyMessage(SSLHandshakeException)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.HTTP_METHOD" "GET"
|
|
||||||
"$SemanticAttributes.HTTP_URL" uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(2) {
|
|
||||||
name "RESOLVE"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpsPort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(3) {
|
|
||||||
name "CONNECT"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpsPort()
|
|
||||||
"net.sock.peer.addr" "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(4) {
|
|
||||||
name "SSL handshake"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
status ERROR
|
|
||||||
// netty swallows the exception, it doesn't make any sense to hard-code the message
|
|
||||||
errorEventWithAnyMessage(SSLHandshakeException)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpsPort()
|
|
||||||
"net.sock.peer.addr" "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def "should successfully establish SSL handshake"() {
|
|
||||||
given:
|
|
||||||
def httpClient = createHttpClient()
|
|
||||||
def uri = "https://localhost:${server.httpsPort()}/success"
|
|
||||||
|
|
||||||
when:
|
|
||||||
def responseMono = httpClient.get().uri(uri)
|
|
||||||
.responseSingle { resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map { resp }
|
|
||||||
}
|
|
||||||
|
|
||||||
runWithSpan("parent") {
|
|
||||||
responseMono.block()
|
|
||||||
}
|
|
||||||
|
|
||||||
then:
|
|
||||||
assertTraces(1) {
|
|
||||||
trace(0, 6) {
|
|
||||||
def list = Arrays.asList("RESOLVE", "CONNECT", "SSL handshake")
|
|
||||||
spans.subList(2, 5).sort(Comparator.comparing { item -> list.indexOf(item.name) })
|
|
||||||
span(0) {
|
|
||||||
name "parent"
|
|
||||||
}
|
|
||||||
span(1) {
|
|
||||||
name "HTTP GET"
|
|
||||||
kind CLIENT
|
|
||||||
childOf span(0)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.HTTP_METHOD" "GET"
|
|
||||||
"$SemanticAttributes.HTTP_URL" uri
|
|
||||||
"$SemanticAttributes.HTTP_FLAVOR" HTTP_1_1
|
|
||||||
"$SemanticAttributes.HTTP_STATUS_CODE" 200
|
|
||||||
"$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpsPort()
|
|
||||||
"net.sock.peer.addr" "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(2) {
|
|
||||||
name "RESOLVE"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpsPort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(3) {
|
|
||||||
name "CONNECT"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpsPort()
|
|
||||||
"net.sock.peer.addr" "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(4) {
|
|
||||||
name "SSL handshake"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpsPort()
|
|
||||||
"net.sock.peer.addr" "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(5) {
|
|
||||||
name "test-http-server"
|
|
||||||
kind SERVER
|
|
||||||
childOf span(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HttpClient createHttpClient(List<String> enabledProtocols = null) {
|
|
||||||
def sslContext = SslContextBuilder.forClient()
|
|
||||||
if (enabledProtocols != null) {
|
|
||||||
sslContext = sslContext.protocols(enabledProtocols)
|
|
||||||
}
|
|
||||||
def sslProvider = SslProvider.builder()
|
|
||||||
.sslContext(sslContext.build())
|
|
||||||
.build()
|
|
||||||
HttpClient.create().secure(sslProvider)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,179 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright The OpenTelemetry Authors
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0
|
|
||||||
|
|
||||||
import io.opentelemetry.instrumentation.test.AgentTestTrait
|
|
||||||
import io.opentelemetry.instrumentation.test.InstrumentationSpecification
|
|
||||||
import io.opentelemetry.instrumentation.test.utils.PortUtils
|
|
||||||
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer
|
|
||||||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
|
|
||||||
import reactor.netty.http.client.HttpClient
|
|
||||||
import spock.lang.Shared
|
|
||||||
|
|
||||||
import static io.opentelemetry.api.trace.SpanKind.CLIENT
|
|
||||||
import static io.opentelemetry.api.trace.SpanKind.INTERNAL
|
|
||||||
import static io.opentelemetry.api.trace.SpanKind.SERVER
|
|
||||||
import static io.opentelemetry.api.trace.StatusCode.ERROR
|
|
||||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_1_1
|
|
||||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP
|
|
||||||
|
|
||||||
class ReactorNettyConnectionSpanTest extends InstrumentationSpecification implements AgentTestTrait {
|
|
||||||
|
|
||||||
@Shared
|
|
||||||
private HttpClientTestServer server
|
|
||||||
|
|
||||||
def setupSpec() {
|
|
||||||
server = new HttpClientTestServer(openTelemetry)
|
|
||||||
server.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
def cleanupSpec() {
|
|
||||||
server.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
def "test successful request"() {
|
|
||||||
given:
|
|
||||||
def httpClient = HttpClient.create()
|
|
||||||
def uri = "http://localhost:${server.httpPort()}/success"
|
|
||||||
|
|
||||||
when:
|
|
||||||
def responseCode =
|
|
||||||
runWithSpan("parent") {
|
|
||||||
httpClient.get().uri(uri)
|
|
||||||
.responseSingle { resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map { resp }
|
|
||||||
}
|
|
||||||
.block()
|
|
||||||
.status().code()
|
|
||||||
}
|
|
||||||
|
|
||||||
then:
|
|
||||||
responseCode == 200
|
|
||||||
assertTraces(1) {
|
|
||||||
trace(0, 5) {
|
|
||||||
def list = Arrays.asList("RESOLVE", "CONNECT")
|
|
||||||
spans.subList(2, 4).sort(Comparator.comparing { item -> list.indexOf(item.name) })
|
|
||||||
span(0) {
|
|
||||||
name "parent"
|
|
||||||
kind INTERNAL
|
|
||||||
hasNoParent()
|
|
||||||
}
|
|
||||||
span(1) {
|
|
||||||
name "HTTP GET"
|
|
||||||
kind CLIENT
|
|
||||||
childOf span(0)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.HTTP_METHOD" "GET"
|
|
||||||
"$SemanticAttributes.HTTP_URL" uri
|
|
||||||
"$SemanticAttributes.HTTP_FLAVOR" HTTP_1_1
|
|
||||||
"$SemanticAttributes.HTTP_STATUS_CODE" 200
|
|
||||||
"$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpPort()
|
|
||||||
"net.sock.peer.addr" "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(2) {
|
|
||||||
name "RESOLVE"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpPort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(3) {
|
|
||||||
name "CONNECT"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" server.httpPort()
|
|
||||||
"net.sock.peer.addr" "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(4) {
|
|
||||||
name "test-http-server"
|
|
||||||
kind SERVER
|
|
||||||
childOf span(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def "test failing request"() {
|
|
||||||
given:
|
|
||||||
def httpClient = HttpClient.create()
|
|
||||||
def uri = "http://localhost:${PortUtils.UNUSABLE_PORT}"
|
|
||||||
|
|
||||||
when:
|
|
||||||
runWithSpan("parent") {
|
|
||||||
httpClient.get().uri(uri)
|
|
||||||
.responseSingle { resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map { resp }
|
|
||||||
}
|
|
||||||
.block()
|
|
||||||
.status().code()
|
|
||||||
}
|
|
||||||
|
|
||||||
then:
|
|
||||||
def thrownException = thrown(Exception)
|
|
||||||
def connectException = thrownException.getCause()
|
|
||||||
|
|
||||||
and:
|
|
||||||
assertTraces(1) {
|
|
||||||
trace(0, 4) {
|
|
||||||
def list = Arrays.asList("RESOLVE", "CONNECT")
|
|
||||||
spans.subList(2, 4).sort(Comparator.comparing { item -> list.indexOf(item.name) })
|
|
||||||
span(0) {
|
|
||||||
name "parent"
|
|
||||||
kind INTERNAL
|
|
||||||
hasNoParent()
|
|
||||||
status ERROR
|
|
||||||
errorEvent(thrownException.class, thrownException.message)
|
|
||||||
}
|
|
||||||
span(1) {
|
|
||||||
name "HTTP GET"
|
|
||||||
kind CLIENT
|
|
||||||
childOf span(0)
|
|
||||||
status ERROR
|
|
||||||
errorEvent(connectException.class, connectException.message)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.HTTP_METHOD" "GET"
|
|
||||||
"$SemanticAttributes.HTTP_URL" uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(2) {
|
|
||||||
name "RESOLVE"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" PortUtils.UNUSABLE_PORT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(3) {
|
|
||||||
name "CONNECT"
|
|
||||||
kind INTERNAL
|
|
||||||
childOf span(1)
|
|
||||||
status ERROR
|
|
||||||
errorEvent(connectException.class, connectException.message)
|
|
||||||
attributes {
|
|
||||||
"$SemanticAttributes.NET_TRANSPORT" IP_TCP
|
|
||||||
"$SemanticAttributes.NET_PEER_NAME" "localhost"
|
|
||||||
"$SemanticAttributes.NET_PEER_PORT" PortUtils.UNUSABLE_PORT
|
|
||||||
"net.sock.peer.addr" "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright The OpenTelemetry Authors
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0
|
|
||||||
|
|
||||||
import io.netty.channel.ChannelOption
|
|
||||||
import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection
|
|
||||||
import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames
|
|
||||||
import reactor.netty.http.client.HttpClient
|
|
||||||
|
|
||||||
import java.util.concurrent.ExecutionException
|
|
||||||
import java.util.concurrent.TimeoutException
|
|
||||||
|
|
||||||
class ReactorNettyHttpClientTest extends AbstractReactorNettyHttpClientTest {
|
|
||||||
|
|
||||||
HttpClient createHttpClient() {
|
|
||||||
return HttpClient.create()
|
|
||||||
.tcpConfiguration({ tcpClient ->
|
|
||||||
tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS)
|
|
||||||
})
|
|
||||||
.resolver(getAddressResolverGroup())
|
|
||||||
.headers({ headers -> headers.set(HttpHeaderNames.USER_AGENT, userAgent()) })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
SingleConnection createSingleConnection(String host, int port) {
|
|
||||||
def httpClient = HttpClient
|
|
||||||
.newConnection()
|
|
||||||
.host(host)
|
|
||||||
.port(port)
|
|
||||||
.headers({ headers -> headers.set(HttpHeaderNames.USER_AGENT, userAgent()) })
|
|
||||||
|
|
||||||
return new SingleConnection() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
int doRequest(String path, Map<String, String> headers) throws ExecutionException, InterruptedException, TimeoutException {
|
|
||||||
return httpClient
|
|
||||||
.headers({ h -> headers.each { k, v -> h.add(k, v) } })
|
|
||||||
.get()
|
|
||||||
.uri(path)
|
|
||||||
.responseSingle { resp, content ->
|
|
||||||
// Make sure to consume content since that's when we close the span.
|
|
||||||
content.map { resp }
|
|
||||||
}
|
|
||||||
.block()
|
|
||||||
.status().code()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright The OpenTelemetry Authors
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0
|
|
||||||
|
|
||||||
import io.netty.channel.ChannelOption
|
|
||||||
import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames
|
|
||||||
import reactor.netty.http.client.HttpClient
|
|
||||||
import reactor.netty.tcp.TcpClient
|
|
||||||
|
|
||||||
class ReactorNettyHttpClientUsingFromTest extends AbstractReactorNettyHttpClientTest {
|
|
||||||
|
|
||||||
HttpClient createHttpClient() {
|
|
||||||
return HttpClient.from(TcpClient.create())
|
|
||||||
.tcpConfiguration({ tcpClient ->
|
|
||||||
tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS)
|
|
||||||
})
|
|
||||||
.resolver(getAddressResolverGroup())
|
|
||||||
.headers({ headers -> headers.set(HttpHeaderNames.USER_AGENT, userAgent()) })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0;
|
||||||
|
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.CLIENT;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.SERVER;
|
||||||
|
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
|
||||||
|
import static java.util.Collections.emptySet;
|
||||||
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||||
|
|
||||||
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
|
import io.netty.resolver.AddressResolverGroup;
|
||||||
|
import io.opentelemetry.api.common.AttributeKey;
|
||||||
|
import io.opentelemetry.api.trace.Span;
|
||||||
|
import io.opentelemetry.api.trace.SpanContext;
|
||||||
|
import io.opentelemetry.instrumentation.test.utils.PortUtils;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
|
||||||
|
import io.opentelemetry.sdk.trace.data.SpanData;
|
||||||
|
import io.opentelemetry.sdk.trace.data.StatusData;
|
||||||
|
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
|
||||||
|
abstract class AbstractReactorNettyHttpClientTest
|
||||||
|
extends AbstractHttpClientTest<HttpClient.ResponseReceiver<?>> {
|
||||||
|
|
||||||
|
static final String USER_AGENT = "ReactorNetty";
|
||||||
|
|
||||||
|
@RegisterExtension
|
||||||
|
static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent();
|
||||||
|
|
||||||
|
protected abstract HttpClient createHttpClient();
|
||||||
|
|
||||||
|
protected AddressResolverGroup<InetSocketAddress> getAddressResolverGroup() {
|
||||||
|
return CustomNameResolverGroup.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient.ResponseReceiver<?> buildRequest(
|
||||||
|
String method, URI uri, Map<String, String> headers) {
|
||||||
|
HttpClient client =
|
||||||
|
createHttpClient()
|
||||||
|
.followRedirect(true)
|
||||||
|
.headers(h -> headers.forEach(h::add))
|
||||||
|
.baseUrl(resolveAddress("").toString());
|
||||||
|
if (uri.toString().contains("/read-timeout")) {
|
||||||
|
client = client.responseTimeout(READ_TIMEOUT);
|
||||||
|
}
|
||||||
|
return client.request(HttpMethod.valueOf(method)).uri(uri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendRequest(
|
||||||
|
HttpClient.ResponseReceiver<?> request, String method, URI uri, Map<String, String> headers) {
|
||||||
|
return request
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
})
|
||||||
|
.block()
|
||||||
|
.status()
|
||||||
|
.code();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendRequestWithCallback(
|
||||||
|
HttpClient.ResponseReceiver<?> request,
|
||||||
|
String method,
|
||||||
|
URI uri,
|
||||||
|
Map<String, String> headers,
|
||||||
|
AbstractHttpClientTest.RequestResult requestResult) {
|
||||||
|
request
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
response -> requestResult.complete(response.status().code()), requestResult::complete);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpClientTestOptions options) {
|
||||||
|
options.disableTestRedirects();
|
||||||
|
options.enableTestReadTimeout();
|
||||||
|
options.setUserAgent(USER_AGENT);
|
||||||
|
|
||||||
|
options.setClientSpanErrorMapper(
|
||||||
|
(uri, exception) -> {
|
||||||
|
if (exception.getClass().getName().endsWith("ReactiveException")) {
|
||||||
|
// unopened port or non routable address
|
||||||
|
if ("http://localhost:61/".equals(uri.toString())
|
||||||
|
|| "https://192.0.2.1/".equals(uri.toString())) {
|
||||||
|
exception = exception.getCause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exception;
|
||||||
|
});
|
||||||
|
|
||||||
|
options.setHttpAttributes(
|
||||||
|
uri -> {
|
||||||
|
// unopened port or non routable address
|
||||||
|
if ("http://localhost:61/".equals(uri.toString())
|
||||||
|
|| "https://192.0.2.1/".equals(uri.toString())) {
|
||||||
|
return emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<AttributeKey<?>> attributes =
|
||||||
|
new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES);
|
||||||
|
if (uri.toString().contains("/read-timeout")) {
|
||||||
|
attributes.remove(SemanticAttributes.NET_PEER_NAME);
|
||||||
|
attributes.remove(SemanticAttributes.NET_PEER_PORT);
|
||||||
|
attributes.remove(SemanticAttributes.HTTP_FLAVOR);
|
||||||
|
}
|
||||||
|
return attributes;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeContextToHttpClientCallbacks() throws InterruptedException {
|
||||||
|
AtomicReference<Span> onRequestSpan = new AtomicReference<>();
|
||||||
|
AtomicReference<Span> afterRequestSpan = new AtomicReference<>();
|
||||||
|
AtomicReference<Span> onResponseSpan = new AtomicReference<>();
|
||||||
|
AtomicReference<Span> afterResponseSpan = new AtomicReference<>();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
HttpClient httpClient =
|
||||||
|
createHttpClient()
|
||||||
|
.doOnRequest((rq, con) -> onRequestSpan.set(Span.current()))
|
||||||
|
.doAfterRequest((rq, con) -> afterRequestSpan.set(Span.current()))
|
||||||
|
.doOnResponse((rs, con) -> onResponseSpan.set(Span.current()))
|
||||||
|
.doAfterResponseSuccess(
|
||||||
|
(rs, con) -> {
|
||||||
|
afterResponseSpan.set(Span.current());
|
||||||
|
latch.countDown();
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.runWithSpan(
|
||||||
|
"parent",
|
||||||
|
() ->
|
||||||
|
httpClient
|
||||||
|
.baseUrl(resolveAddress("").toString())
|
||||||
|
.get()
|
||||||
|
.uri("/success")
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
})
|
||||||
|
.block());
|
||||||
|
|
||||||
|
latch.await(10, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
testing.waitAndAssertTraces(
|
||||||
|
trace -> {
|
||||||
|
SpanData parentSpan = trace.getSpan(0);
|
||||||
|
SpanData nettyClientSpan = trace.getSpan(1);
|
||||||
|
|
||||||
|
trace.hasSpansSatisfyingExactly(
|
||||||
|
span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
|
||||||
|
span -> span.hasName("HTTP GET").hasKind(CLIENT).hasParent(parentSpan),
|
||||||
|
span -> span.hasName("test-http-server").hasKind(SERVER).hasParent(nettyClientSpan));
|
||||||
|
|
||||||
|
assertSameSpan(parentSpan, onRequestSpan);
|
||||||
|
assertSameSpan(nettyClientSpan, afterRequestSpan);
|
||||||
|
assertSameSpan(nettyClientSpan, onResponseSpan);
|
||||||
|
assertSameSpan(parentSpan, afterResponseSpan);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeContextToHttpRequestErrorCallback() {
|
||||||
|
AtomicReference<Span> onRequestErrorSpan = new AtomicReference<>();
|
||||||
|
|
||||||
|
HttpClient httpClient =
|
||||||
|
createHttpClient().doOnRequestError((rq, err) -> onRequestErrorSpan.set(Span.current()));
|
||||||
|
|
||||||
|
Throwable thrown =
|
||||||
|
catchThrowable(
|
||||||
|
() ->
|
||||||
|
testing.runWithSpan(
|
||||||
|
"parent",
|
||||||
|
() ->
|
||||||
|
httpClient
|
||||||
|
.get()
|
||||||
|
.uri("http://localhost:" + PortUtils.UNUSABLE_PORT + "/")
|
||||||
|
.response()
|
||||||
|
.block()));
|
||||||
|
|
||||||
|
testing.waitAndAssertTraces(
|
||||||
|
trace -> {
|
||||||
|
SpanData parentSpan = trace.getSpan(0);
|
||||||
|
|
||||||
|
trace.hasSpansSatisfyingExactly(
|
||||||
|
span ->
|
||||||
|
span.hasName("parent")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasNoParent()
|
||||||
|
.hasStatus(StatusData.error())
|
||||||
|
.hasException(thrown),
|
||||||
|
span ->
|
||||||
|
span.hasKind(CLIENT)
|
||||||
|
.hasParent(parentSpan)
|
||||||
|
.hasStatus(StatusData.error())
|
||||||
|
.hasException(thrown.getCause()));
|
||||||
|
|
||||||
|
assertSameSpan(parentSpan, onRequestErrorSpan);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotLeakConnections() {
|
||||||
|
HashSet<Integer> uniqueChannelHashes = new HashSet<>();
|
||||||
|
HttpClient httpClient =
|
||||||
|
createHttpClient().doOnConnect(config -> uniqueChannelHashes.add(config.channelHash()));
|
||||||
|
|
||||||
|
int count = 100;
|
||||||
|
IntStream.range(0, count)
|
||||||
|
.forEach(
|
||||||
|
i ->
|
||||||
|
testing.runWithSpan(
|
||||||
|
"parent",
|
||||||
|
() -> {
|
||||||
|
int status =
|
||||||
|
httpClient
|
||||||
|
.get()
|
||||||
|
.uri(resolveAddress("/success"))
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the
|
||||||
|
// span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
})
|
||||||
|
.block()
|
||||||
|
.status()
|
||||||
|
.code();
|
||||||
|
assertThat(status).isEqualTo(200);
|
||||||
|
}));
|
||||||
|
|
||||||
|
testing.waitForTraces(count);
|
||||||
|
assertThat(uniqueChannelHashes).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSameSpan(SpanData expected, AtomicReference<Span> actual) {
|
||||||
|
SpanContext expectedSpanContext = expected.getSpanContext();
|
||||||
|
SpanContext actualSpanContext = actual.get().getSpanContext();
|
||||||
|
assertThat(actualSpanContext.getTraceId()).isEqualTo(expectedSpanContext.getTraceId());
|
||||||
|
assertThat(actualSpanContext.getSpanId()).isEqualTo(expectedSpanContext.getSpanId());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0;
|
||||||
|
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
|
||||||
|
import io.netty.resolver.AddressResolver;
|
||||||
|
import io.netty.resolver.AddressResolverGroup;
|
||||||
|
import io.netty.resolver.InetNameResolver;
|
||||||
|
import io.netty.util.concurrent.EventExecutor;
|
||||||
|
import io.netty.util.concurrent.Promise;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CustomNameResolverGroup extends AddressResolverGroup<InetSocketAddress> {
|
||||||
|
|
||||||
|
public static final CustomNameResolverGroup INSTANCE = new CustomNameResolverGroup();
|
||||||
|
|
||||||
|
private CustomNameResolverGroup() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) {
|
||||||
|
return new CustomNameResolver(executor).asAddressResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CustomNameResolver extends InetNameResolver {
|
||||||
|
|
||||||
|
private CustomNameResolver(EventExecutor executor) {
|
||||||
|
super(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doResolve(String inetHost, Promise<InetAddress> promise) {
|
||||||
|
try {
|
||||||
|
promise.setSuccess(InetAddress.getByName(inetHost));
|
||||||
|
} catch (UnknownHostException exception) {
|
||||||
|
promise.setFailure(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doResolveAll(String inetHost, Promise<List<InetAddress>> promise) {
|
||||||
|
try {
|
||||||
|
// default implementation calls InetAddress.getAllByName
|
||||||
|
promise.setSuccess(singletonList(InetAddress.getByName(inetHost)));
|
||||||
|
} catch (UnknownHostException exception) {
|
||||||
|
promise.setFailure(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0;
|
||||||
|
|
||||||
|
import static io.opentelemetry.api.common.AttributeKey.stringKey;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.CLIENT;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.SERVER;
|
||||||
|
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
|
||||||
|
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
|
||||||
|
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
|
||||||
|
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_1_1;
|
||||||
|
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP;
|
||||||
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||||
|
|
||||||
|
import io.netty.handler.ssl.SslContextBuilder;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer;
|
||||||
|
import io.opentelemetry.sdk.trace.data.EventData;
|
||||||
|
import io.opentelemetry.sdk.trace.data.StatusData;
|
||||||
|
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
|
import org.assertj.core.api.AbstractLongAssert;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
import reactor.netty.http.client.HttpClientResponse;
|
||||||
|
import reactor.netty.tcp.SslProvider;
|
||||||
|
|
||||||
|
class ReactorNettyClientSslTest {
|
||||||
|
|
||||||
|
@RegisterExtension
|
||||||
|
static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
|
||||||
|
|
||||||
|
static HttpClientTestServer server;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUp() {
|
||||||
|
server = new HttpClientTestServer(testing.getOpenTelemetry());
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void tearDown() {
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailSslHandshake() throws SSLException {
|
||||||
|
HttpClient httpClient = createHttpClient("SSLv3");
|
||||||
|
String uri = "https://localhost:" + server.httpsPort() + "/success";
|
||||||
|
|
||||||
|
Mono<HttpClientResponse> responseMono =
|
||||||
|
httpClient
|
||||||
|
.get()
|
||||||
|
.uri(uri)
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
});
|
||||||
|
|
||||||
|
Throwable thrown =
|
||||||
|
catchThrowable(() -> testing.runWithSpan("parent", () -> responseMono.block()));
|
||||||
|
|
||||||
|
testing.waitAndAssertTraces(
|
||||||
|
trace ->
|
||||||
|
trace.hasSpansSatisfyingExactlyInAnyOrder(
|
||||||
|
span ->
|
||||||
|
span.hasName("parent")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasNoParent()
|
||||||
|
.hasStatus(StatusData.error())
|
||||||
|
.hasException(thrown),
|
||||||
|
span ->
|
||||||
|
span.hasName("HTTP GET")
|
||||||
|
.hasKind(CLIENT)
|
||||||
|
.hasParent(trace.getSpan(0))
|
||||||
|
.hasStatus(StatusData.error())
|
||||||
|
// netty swallows the exception, it doesn't make any sense to hard-code the
|
||||||
|
// message
|
||||||
|
.hasEventsSatisfying(ReactorNettyClientSslTest::isSslHandshakeException)
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
|
||||||
|
equalTo(SemanticAttributes.HTTP_URL, uri)),
|
||||||
|
span ->
|
||||||
|
span.hasName("RESOLVE")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort())),
|
||||||
|
span ->
|
||||||
|
span.hasName("CONNECT")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort()),
|
||||||
|
equalTo(stringKey("net.sock.peer.addr"), "127.0.0.1")),
|
||||||
|
span ->
|
||||||
|
span.hasName("SSL handshake")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasStatus(StatusData.error())
|
||||||
|
// netty swallows the exception, it doesn't make any sense to hard-code the
|
||||||
|
// message
|
||||||
|
.hasEventsSatisfying(ReactorNettyClientSslTest::isSslHandshakeException)
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort()),
|
||||||
|
equalTo(stringKey("net.sock.peer.addr"), "127.0.0.1"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSuccessfullyEstablishSslHandshake() throws SSLException {
|
||||||
|
HttpClient httpClient = createHttpClient();
|
||||||
|
String uri = "https://localhost:" + server.httpsPort() + "/success";
|
||||||
|
|
||||||
|
Mono<HttpClientResponse> responseMono =
|
||||||
|
httpClient
|
||||||
|
.get()
|
||||||
|
.uri(uri)
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.runWithSpan("parent", () -> responseMono.block());
|
||||||
|
|
||||||
|
testing.waitAndAssertTraces(
|
||||||
|
trace ->
|
||||||
|
trace.hasSpansSatisfyingExactlyInAnyOrder(
|
||||||
|
span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
|
||||||
|
span ->
|
||||||
|
span.hasName("HTTP GET")
|
||||||
|
.hasKind(CLIENT)
|
||||||
|
.hasParent(trace.getSpan(0))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
|
||||||
|
equalTo(SemanticAttributes.HTTP_URL, uri),
|
||||||
|
equalTo(SemanticAttributes.HTTP_FLAVOR, HTTP_1_1),
|
||||||
|
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200),
|
||||||
|
satisfies(
|
||||||
|
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
|
||||||
|
AbstractLongAssert::isNotNegative),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort()),
|
||||||
|
equalTo(stringKey("net.sock.peer.addr"), "127.0.0.1")),
|
||||||
|
span ->
|
||||||
|
span.hasName("RESOLVE")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort())),
|
||||||
|
span ->
|
||||||
|
span.hasName("CONNECT")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort()),
|
||||||
|
equalTo(stringKey("net.sock.peer.addr"), "127.0.0.1")),
|
||||||
|
span ->
|
||||||
|
span.hasName("SSL handshake")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort()),
|
||||||
|
equalTo(stringKey("net.sock.peer.addr"), "127.0.0.1")),
|
||||||
|
span ->
|
||||||
|
span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient createHttpClient() throws SSLException {
|
||||||
|
return ReactorNettyClientSslTest.createHttpClient(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient createHttpClient(@Nullable String enabledProtocol) throws SSLException {
|
||||||
|
SslContextBuilder sslContext = SslContextBuilder.forClient();
|
||||||
|
if (enabledProtocol != null) {
|
||||||
|
sslContext = sslContext.protocols(enabledProtocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
SslProvider sslProvider = SslProvider.builder().sslContext(sslContext.build()).build();
|
||||||
|
return HttpClient.create().secure(sslProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void isSslHandshakeException(List<? extends EventData> events) {
|
||||||
|
assertThat(events)
|
||||||
|
.filteredOn(event -> event.getName().equals(SemanticAttributes.EXCEPTION_EVENT_NAME))
|
||||||
|
.satisfiesExactly(
|
||||||
|
event ->
|
||||||
|
assertThat(event)
|
||||||
|
.hasAttributesSatisfying(
|
||||||
|
attributes ->
|
||||||
|
assertThat(attributes)
|
||||||
|
.hasSize(3)
|
||||||
|
.containsEntry(
|
||||||
|
SemanticAttributes.EXCEPTION_TYPE,
|
||||||
|
SSLHandshakeException.class.getCanonicalName())
|
||||||
|
.hasEntrySatisfying(
|
||||||
|
SemanticAttributes.EXCEPTION_MESSAGE,
|
||||||
|
s -> assertThat(s).isNotEmpty())
|
||||||
|
.hasEntrySatisfying(
|
||||||
|
SemanticAttributes.EXCEPTION_STACKTRACE,
|
||||||
|
s -> assertThat(s).isNotEmpty())));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0;
|
||||||
|
|
||||||
|
import static io.opentelemetry.api.common.AttributeKey.stringKey;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.CLIENT;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.SERVER;
|
||||||
|
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
|
||||||
|
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
|
||||||
|
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_1_1;
|
||||||
|
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||||
|
|
||||||
|
import io.opentelemetry.instrumentation.test.utils.PortUtils;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer;
|
||||||
|
import io.opentelemetry.sdk.trace.data.StatusData;
|
||||||
|
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
|
||||||
|
import org.assertj.core.api.AbstractLongAssert;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
|
||||||
|
class ReactorNettyConnectionSpanTest {
|
||||||
|
|
||||||
|
@RegisterExtension
|
||||||
|
static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
|
||||||
|
|
||||||
|
static HttpClientTestServer server;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUp() {
|
||||||
|
server = new HttpClientTestServer(testing.getOpenTelemetry());
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void tearDown() {
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuccessfulRequest() {
|
||||||
|
HttpClient httpClient = HttpClient.create();
|
||||||
|
String uri = "http://localhost:" + server.httpPort() + "/success";
|
||||||
|
|
||||||
|
int responseCode =
|
||||||
|
testing.runWithSpan(
|
||||||
|
"parent",
|
||||||
|
() ->
|
||||||
|
httpClient
|
||||||
|
.get()
|
||||||
|
.uri(uri)
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
})
|
||||||
|
.block()
|
||||||
|
.status()
|
||||||
|
.code());
|
||||||
|
|
||||||
|
assertThat(responseCode).isEqualTo(200);
|
||||||
|
|
||||||
|
testing.waitAndAssertTraces(
|
||||||
|
trace ->
|
||||||
|
trace.hasSpansSatisfyingExactlyInAnyOrder(
|
||||||
|
span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
|
||||||
|
span ->
|
||||||
|
span.hasName("HTTP GET")
|
||||||
|
.hasKind(CLIENT)
|
||||||
|
.hasParent(trace.getSpan(0))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
|
||||||
|
equalTo(SemanticAttributes.HTTP_URL, uri),
|
||||||
|
equalTo(SemanticAttributes.HTTP_FLAVOR, HTTP_1_1),
|
||||||
|
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200),
|
||||||
|
satisfies(
|
||||||
|
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
|
||||||
|
AbstractLongAssert::isNotNegative),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpPort()),
|
||||||
|
equalTo(stringKey("net.sock.peer.addr"), "127.0.0.1")),
|
||||||
|
span ->
|
||||||
|
span.hasName("RESOLVE")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpPort())),
|
||||||
|
span ->
|
||||||
|
span.hasName("CONNECT")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, server.httpPort()),
|
||||||
|
equalTo(stringKey("net.sock.peer.addr"), "127.0.0.1")),
|
||||||
|
span ->
|
||||||
|
span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFailingRequest() {
|
||||||
|
HttpClient httpClient = HttpClient.create();
|
||||||
|
String uri = "http://localhost:" + PortUtils.UNUSABLE_PORT;
|
||||||
|
|
||||||
|
Throwable thrown =
|
||||||
|
catchThrowable(
|
||||||
|
() ->
|
||||||
|
testing.runWithSpan(
|
||||||
|
"parent",
|
||||||
|
() ->
|
||||||
|
httpClient
|
||||||
|
.get()
|
||||||
|
.uri(uri)
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the
|
||||||
|
// span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
})
|
||||||
|
.block()
|
||||||
|
.status()
|
||||||
|
.code()));
|
||||||
|
|
||||||
|
Throwable connectException = thrown.getCause();
|
||||||
|
|
||||||
|
testing.waitAndAssertTraces(
|
||||||
|
trace ->
|
||||||
|
trace.hasSpansSatisfyingExactlyInAnyOrder(
|
||||||
|
span ->
|
||||||
|
span.hasName("parent")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasNoParent()
|
||||||
|
.hasStatus(StatusData.error())
|
||||||
|
.hasException(thrown),
|
||||||
|
span ->
|
||||||
|
span.hasName("HTTP GET")
|
||||||
|
.hasKind(CLIENT)
|
||||||
|
.hasParent(trace.getSpan(0))
|
||||||
|
.hasStatus(StatusData.error())
|
||||||
|
.hasException(connectException)
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
|
||||||
|
equalTo(SemanticAttributes.HTTP_URL, uri)),
|
||||||
|
span ->
|
||||||
|
span.hasName("RESOLVE")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, PortUtils.UNUSABLE_PORT)),
|
||||||
|
span ->
|
||||||
|
span.hasName("CONNECT")
|
||||||
|
.hasKind(INTERNAL)
|
||||||
|
.hasParent(trace.getSpan(1))
|
||||||
|
.hasStatus(StatusData.error())
|
||||||
|
.hasException(connectException)
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.NET_PEER_PORT, PortUtils.UNUSABLE_PORT),
|
||||||
|
equalTo(stringKey("net.sock.peer.addr"), "127.0.0.1"))));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelOption;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
|
||||||
|
import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
|
||||||
|
class ReactorNettyHttpClientTest extends AbstractReactorNettyHttpClientTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HttpClient createHttpClient() {
|
||||||
|
int connectionTimeoutMillis = (int) CONNECTION_TIMEOUT.toMillis();
|
||||||
|
return HttpClient.create()
|
||||||
|
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutMillis)
|
||||||
|
.resolver(getAddressResolverGroup())
|
||||||
|
.headers(headers -> headers.set(HttpHeaderNames.USER_AGENT, USER_AGENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpClientTestOptions options) {
|
||||||
|
super.configure(options);
|
||||||
|
|
||||||
|
options.setSingleConnectionFactory(
|
||||||
|
(host, port) -> {
|
||||||
|
HttpClient httpClient =
|
||||||
|
HttpClient.newConnection()
|
||||||
|
.host(host)
|
||||||
|
.port(port)
|
||||||
|
.headers(headers -> headers.set(HttpHeaderNames.USER_AGENT, USER_AGENT));
|
||||||
|
|
||||||
|
return (path, headers) ->
|
||||||
|
httpClient
|
||||||
|
.headers(h -> headers.forEach(h::add))
|
||||||
|
.get()
|
||||||
|
.uri(path)
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
})
|
||||||
|
.block()
|
||||||
|
.status()
|
||||||
|
.code();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelOption;
|
||||||
|
import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
import reactor.netty.tcp.TcpClient;
|
||||||
|
|
||||||
|
class ReactorNettyHttpClientUsingFromTest extends AbstractReactorNettyHttpClientTest {
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation") // from(TcpClient) is deprecated, but we want to test it anyway
|
||||||
|
@Override
|
||||||
|
protected HttpClient createHttpClient() {
|
||||||
|
int connectionTimeoutMillis = (int) CONNECTION_TIMEOUT.toMillis();
|
||||||
|
return HttpClient.from(TcpClient.create())
|
||||||
|
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutMillis)
|
||||||
|
.resolver(getAddressResolverGroup())
|
||||||
|
.headers(headers -> headers.set(HttpHeaderNames.USER_AGENT, USER_AGENT));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0;
|
||||||
|
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.CLIENT;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.SERVER;
|
||||||
|
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer;
|
||||||
|
import java.time.Duration;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
class ReactorNettyWithSpanTest {
|
||||||
|
|
||||||
|
@RegisterExtension
|
||||||
|
static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
|
||||||
|
|
||||||
|
static HttpClientTestServer server;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUp() {
|
||||||
|
server = new HttpClientTestServer(testing.getOpenTelemetry());
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void tearDown() {
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccessfulNestedUnderWithSpan() {
|
||||||
|
HttpClient httpClient = HttpClient.create();
|
||||||
|
|
||||||
|
Mono<Integer> httpRequest =
|
||||||
|
Mono.defer(
|
||||||
|
() ->
|
||||||
|
httpClient
|
||||||
|
.get()
|
||||||
|
.uri("http://localhost:" + server.httpPort() + "/success")
|
||||||
|
.responseSingle(
|
||||||
|
(resp, content) -> {
|
||||||
|
// Make sure to consume content since that's when we close the span.
|
||||||
|
return content.map(unused -> resp);
|
||||||
|
})
|
||||||
|
.map(r -> r.status().code()));
|
||||||
|
|
||||||
|
Mono<Integer> getResponse =
|
||||||
|
new TracedWithSpan()
|
||||||
|
.mono(
|
||||||
|
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/4348
|
||||||
|
// our HTTP server is synchronous, i.e. it returns Mono.just with response
|
||||||
|
// which is not supported by TracingSubscriber - it does not instrument scalar calls
|
||||||
|
// so we delay here to fake async http request and let Reactor context
|
||||||
|
// instrumentation work
|
||||||
|
Mono.delay(Duration.ofMillis(1)).then(httpRequest));
|
||||||
|
|
||||||
|
StepVerifier.create(getResponse).expectNext(200).expectComplete().verify();
|
||||||
|
|
||||||
|
testing.waitAndAssertTraces(
|
||||||
|
trace ->
|
||||||
|
trace.hasSpansSatisfyingExactly(
|
||||||
|
span -> span.hasName("TracedWithSpan.mono").hasKind(INTERNAL).hasNoParent(),
|
||||||
|
span -> span.hasName("HTTP GET").hasKind(CLIENT).hasParent(trace.getSpan(0)),
|
||||||
|
span ->
|
||||||
|
span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1))));
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.opentelemetry.test.reactor.netty;
|
package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0;
|
||||||
|
|
||||||
import io.opentelemetry.instrumentation.annotations.WithSpan;
|
import io.opentelemetry.instrumentation.annotations.WithSpan;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
Loading…
Reference in New Issue