Implement `error.type` in `spring-webflux` and `reactor-netty` instrumentations (#9967)
This commit is contained in:
parent
df8b334ccf
commit
1ac8c4d606
|
@ -76,7 +76,7 @@ public interface HttpCommonAttributesGetter<REQUEST, RESPONSE> {
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
default String getErrorType(
|
default String getErrorType(
|
||||||
REQUEST request, @Nullable RESPONSE response, @Nullable Throwable throwable) {
|
REQUEST request, @Nullable RESPONSE response, @Nullable Throwable error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,13 +72,15 @@ final class ReactorNettyHttpClientAttributesGetter
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String getServerAddress(HttpClientRequest request) {
|
public String getServerAddress(HttpClientRequest request) {
|
||||||
return getHost(request);
|
String resourceUrl = request.resourceUrl();
|
||||||
|
return resourceUrl == null ? null : UrlParser.getHost(resourceUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Integer getServerPort(HttpClientRequest request) {
|
public Integer getServerPort(HttpClientRequest request) {
|
||||||
return getPort(request);
|
String resourceUrl = request.resourceUrl();
|
||||||
|
return resourceUrl == null ? null : UrlParser.getPort(resourceUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -99,14 +101,14 @@ final class ReactorNettyHttpClientAttributesGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String getHost(HttpClientRequest request) {
|
@Override
|
||||||
String resourceUrl = request.resourceUrl();
|
public String getErrorType(
|
||||||
return resourceUrl == null ? null : UrlParser.getHost(resourceUrl);
|
HttpClientRequest request, @Nullable HttpClientResponse response, @Nullable Throwable error) {
|
||||||
}
|
// if both response and error are null it means the request has been cancelled -- see the
|
||||||
|
// ConnectionWrapper class
|
||||||
@Nullable
|
if (response == null && error == null) {
|
||||||
private static Integer getPort(HttpClientRequest request) {
|
return "cancelled";
|
||||||
String resourceUrl = request.resourceUrl();
|
}
|
||||||
return resourceUrl == null ? null : UrlParser.getPort(resourceUrl);
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -310,7 +310,7 @@ abstract class AbstractReactorNettyHttpClientTest
|
||||||
equalTo(SemanticAttributes.URL_FULL, uri.toString()),
|
equalTo(SemanticAttributes.URL_FULL, uri.toString()),
|
||||||
equalTo(SemanticAttributes.SERVER_ADDRESS, "localhost"),
|
equalTo(SemanticAttributes.SERVER_ADDRESS, "localhost"),
|
||||||
equalTo(SemanticAttributes.SERVER_PORT, uri.getPort()),
|
equalTo(SemanticAttributes.SERVER_PORT, uri.getPort()),
|
||||||
equalTo(HttpAttributes.ERROR_TYPE, "_OTHER")),
|
equalTo(HttpAttributes.ERROR_TYPE, "cancelled")),
|
||||||
span ->
|
span ->
|
||||||
span.hasName("test-http-server")
|
span.hasName("test-http-server")
|
||||||
.hasKind(SpanKind.SERVER)
|
.hasKind(SpanKind.SERVER)
|
||||||
|
|
|
@ -14,7 +14,6 @@ import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.ClientInstr
|
||||||
import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientHttpAttributesGetter;
|
import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientHttpAttributesGetter;
|
||||||
import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientTracingFilter;
|
import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientTracingFilter;
|
||||||
import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig;
|
import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig;
|
||||||
import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||||
|
@ -35,9 +34,6 @@ public final class WebClientHelper {
|
||||||
HttpClientPeerServiceAttributesExtractor.create(
|
HttpClientPeerServiceAttributesExtractor.create(
|
||||||
WebClientHttpAttributesGetter.INSTANCE,
|
WebClientHttpAttributesGetter.INSTANCE,
|
||||||
CommonConfig.get().getPeerServiceResolver())),
|
CommonConfig.get().getPeerServiceResolver())),
|
||||||
InstrumentationConfig.get()
|
|
||||||
.getBoolean(
|
|
||||||
"otel.instrumentation.spring-webflux.experimental-span-attributes", false),
|
|
||||||
CommonConfig.get().shouldEmitExperimentalHttpClientTelemetry());
|
CommonConfig.get().shouldEmitExperimentalHttpClientTelemetry());
|
||||||
|
|
||||||
public static void addFilter(List<ExchangeFilterFunction> exchangeFilterFunctions) {
|
public static void addFilter(List<ExchangeFilterFunction> exchangeFilterFunctions) {
|
||||||
|
|
|
@ -45,6 +45,11 @@ interface.
|
||||||
a `WebFilter` and using the OpenTelemetry Reactor instrumentation to ensure context is
|
a `WebFilter` and using the OpenTelemetry Reactor instrumentation to ensure context is
|
||||||
passed around correctly.
|
passed around correctly.
|
||||||
|
|
||||||
|
### Web client instrumentation
|
||||||
|
|
||||||
|
The `WebClient` instrumentation will emit the `error.type` attribute with value `cancelled` whenever
|
||||||
|
an outgoing HTTP request is cancelled.
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
Here is how to set up client and server instrumentation respectively:
|
Here is how to set up client and server instrumentation respectively:
|
||||||
|
|
|
@ -53,9 +53,8 @@ public final class SpringWebfluxTelemetryBuilder {
|
||||||
clientExtractorConfigurer = builder -> {};
|
clientExtractorConfigurer = builder -> {};
|
||||||
private Consumer<HttpSpanNameExtractorBuilder<ClientRequest>> clientSpanNameExtractorConfigurer =
|
private Consumer<HttpSpanNameExtractorBuilder<ClientRequest>> clientSpanNameExtractorConfigurer =
|
||||||
builder -> {};
|
builder -> {};
|
||||||
private boolean captureExperimentalSpanAttributes = false;
|
private boolean emitExperimentalHttpClientTelemetry = false;
|
||||||
private boolean emitExperimentalHttpClientMetrics = false;
|
private boolean emitExperimentalHttpServerTelemetry = false;
|
||||||
private boolean emitExperimentalHttpServerMetrics = false;
|
|
||||||
|
|
||||||
SpringWebfluxTelemetryBuilder(OpenTelemetry openTelemetry) {
|
SpringWebfluxTelemetryBuilder(OpenTelemetry openTelemetry) {
|
||||||
this.openTelemetry = openTelemetry;
|
this.openTelemetry = openTelemetry;
|
||||||
|
@ -100,18 +99,6 @@ public final class SpringWebfluxTelemetryBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether experimental attributes should be set to spans. These attributes may be changed or
|
|
||||||
* removed in the future, so only enable this if you know you do not require attributes filled by
|
|
||||||
* this instrumentation to be stable across versions.
|
|
||||||
*/
|
|
||||||
@CanIgnoreReturnValue
|
|
||||||
public SpringWebfluxTelemetryBuilder setCaptureExperimentalSpanAttributes(
|
|
||||||
boolean captureExperimentalSpanAttributes) {
|
|
||||||
this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented
|
* Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented
|
||||||
* items.
|
* items.
|
||||||
|
@ -178,26 +165,26 @@ public final class SpringWebfluxTelemetryBuilder {
|
||||||
/**
|
/**
|
||||||
* Configures the instrumentation to emit experimental HTTP client metrics.
|
* Configures the instrumentation to emit experimental HTTP client metrics.
|
||||||
*
|
*
|
||||||
* @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics
|
* @param emitExperimentalHttpClientTelemetry {@code true} if the experimental HTTP client metrics
|
||||||
* are to be emitted.
|
* are to be emitted.
|
||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public SpringWebfluxTelemetryBuilder setEmitExperimentalHttpClientMetrics(
|
public SpringWebfluxTelemetryBuilder setEmitExperimentalHttpClientTelemetry(
|
||||||
boolean emitExperimentalHttpClientMetrics) {
|
boolean emitExperimentalHttpClientTelemetry) {
|
||||||
this.emitExperimentalHttpClientMetrics = emitExperimentalHttpClientMetrics;
|
this.emitExperimentalHttpClientTelemetry = emitExperimentalHttpClientTelemetry;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the instrumentation to emit experimental HTTP server metrics.
|
* Configures the instrumentation to emit experimental HTTP server metrics.
|
||||||
*
|
*
|
||||||
* @param emitExperimentalHttpServerMetrics {@code true} if the experimental HTTP server metrics
|
* @param emitExperimentalHttpServerTelemetry {@code true} if the experimental HTTP server metrics
|
||||||
* are to be emitted.
|
* are to be emitted.
|
||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public SpringWebfluxTelemetryBuilder setEmitExperimentalHttpServerMetrics(
|
public SpringWebfluxTelemetryBuilder setEmitExperimentalHttpServerTelemetry(
|
||||||
boolean emitExperimentalHttpServerMetrics) {
|
boolean emitExperimentalHttpServerTelemetry) {
|
||||||
this.emitExperimentalHttpServerMetrics = emitExperimentalHttpServerMetrics;
|
this.emitExperimentalHttpServerTelemetry = emitExperimentalHttpServerTelemetry;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,8 +200,7 @@ public final class SpringWebfluxTelemetryBuilder {
|
||||||
clientExtractorConfigurer,
|
clientExtractorConfigurer,
|
||||||
clientSpanNameExtractorConfigurer,
|
clientSpanNameExtractorConfigurer,
|
||||||
clientAdditionalExtractors,
|
clientAdditionalExtractors,
|
||||||
captureExperimentalSpanAttributes,
|
emitExperimentalHttpClientTelemetry);
|
||||||
emitExperimentalHttpClientMetrics);
|
|
||||||
|
|
||||||
Instrumenter<ServerWebExchange, ServerWebExchange> serverInstrumenter =
|
Instrumenter<ServerWebExchange, ServerWebExchange> serverInstrumenter =
|
||||||
buildServerInstrumenter();
|
buildServerInstrumenter();
|
||||||
|
@ -234,7 +220,7 @@ public final class SpringWebfluxTelemetryBuilder {
|
||||||
.addAttributesExtractors(serverAdditionalExtractors)
|
.addAttributesExtractors(serverAdditionalExtractors)
|
||||||
.addContextCustomizer(httpServerRouteBuilder.build())
|
.addContextCustomizer(httpServerRouteBuilder.build())
|
||||||
.addOperationMetrics(HttpServerMetrics.get());
|
.addOperationMetrics(HttpServerMetrics.get());
|
||||||
if (emitExperimentalHttpServerMetrics) {
|
if (emitExperimentalHttpServerTelemetry) {
|
||||||
builder
|
builder
|
||||||
.addAttributesExtractor(HttpExperimentalAttributesExtractor.create(getter))
|
.addAttributesExtractor(HttpExperimentalAttributesExtractor.create(getter))
|
||||||
.addOperationMetrics(HttpServerExperimentalMetrics.get());
|
.addOperationMetrics(HttpServerExperimentalMetrics.get());
|
||||||
|
|
|
@ -40,8 +40,7 @@ public final class ClientInstrumenterFactory {
|
||||||
extractorConfigurer,
|
extractorConfigurer,
|
||||||
Consumer<HttpSpanNameExtractorBuilder<ClientRequest>> spanNameExtractorConfigurer,
|
Consumer<HttpSpanNameExtractorBuilder<ClientRequest>> spanNameExtractorConfigurer,
|
||||||
List<AttributesExtractor<ClientRequest, ClientResponse>> additionalExtractors,
|
List<AttributesExtractor<ClientRequest, ClientResponse>> additionalExtractors,
|
||||||
boolean captureExperimentalSpanAttributes,
|
boolean emitExperimentalHttpClientTelemetry) {
|
||||||
boolean emitExperimentalHttpClientMetrics) {
|
|
||||||
|
|
||||||
WebClientHttpAttributesGetter httpAttributesGetter = WebClientHttpAttributesGetter.INSTANCE;
|
WebClientHttpAttributesGetter httpAttributesGetter = WebClientHttpAttributesGetter.INSTANCE;
|
||||||
|
|
||||||
|
@ -61,10 +60,7 @@ public final class ClientInstrumenterFactory {
|
||||||
.addAttributesExtractors(additionalExtractors)
|
.addAttributesExtractors(additionalExtractors)
|
||||||
.addOperationMetrics(HttpClientMetrics.get());
|
.addOperationMetrics(HttpClientMetrics.get());
|
||||||
|
|
||||||
if (captureExperimentalSpanAttributes) {
|
if (emitExperimentalHttpClientTelemetry) {
|
||||||
clientBuilder.addAttributesExtractor(new WebClientExperimentalAttributesExtractor());
|
|
||||||
}
|
|
||||||
if (emitExperimentalHttpClientMetrics) {
|
|
||||||
clientBuilder
|
clientBuilder
|
||||||
.addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter))
|
.addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter))
|
||||||
.addOperationMetrics(HttpClientExperimentalMetrics.get());
|
.addOperationMetrics(HttpClientExperimentalMetrics.get());
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright The OpenTelemetry Authors
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.opentelemetry.instrumentation.spring.webflux.v5_3.internal;
|
|
||||||
|
|
||||||
import static io.opentelemetry.api.common.AttributeKey.stringKey;
|
|
||||||
|
|
||||||
import io.opentelemetry.api.common.AttributeKey;
|
|
||||||
import io.opentelemetry.api.common.AttributesBuilder;
|
|
||||||
import io.opentelemetry.context.Context;
|
|
||||||
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
|
||||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
|
||||||
|
|
||||||
final class WebClientExperimentalAttributesExtractor
|
|
||||||
implements AttributesExtractor<ClientRequest, ClientResponse> {
|
|
||||||
|
|
||||||
private static final AttributeKey<String> SPRING_WEBFLUX_EVENT =
|
|
||||||
stringKey("spring-webflux.event");
|
|
||||||
private static final AttributeKey<String> SPRING_WEBFLUX_MESSAGE =
|
|
||||||
stringKey("spring-webflux.message");
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart(AttributesBuilder attributes, Context parentContext, ClientRequest request) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEnd(
|
|
||||||
AttributesBuilder attributes,
|
|
||||||
Context context,
|
|
||||||
ClientRequest request,
|
|
||||||
@Nullable ClientResponse response,
|
|
||||||
@Nullable Throwable error) {
|
|
||||||
|
|
||||||
// no response and no error means that the request has been cancelled
|
|
||||||
if (response == null && error == null) {
|
|
||||||
attributes.put(SPRING_WEBFLUX_EVENT, "cancelled");
|
|
||||||
attributes.put(SPRING_WEBFLUX_MESSAGE, "The subscription was cancelled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -59,4 +59,16 @@ public enum WebClientHttpAttributesGetter
|
||||||
public Integer getServerPort(ClientRequest request) {
|
public Integer getServerPort(ClientRequest request) {
|
||||||
return request.url().getPort();
|
return request.url().getPort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getErrorType(
|
||||||
|
ClientRequest request, @Nullable ClientResponse response, @Nullable Throwable error) {
|
||||||
|
// if both response and error are null it means the request has been cancelled -- see the
|
||||||
|
// WebClientTracingFilter class
|
||||||
|
if (response == null && error == null) {
|
||||||
|
return "cancelled";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,29 @@
|
||||||
|
|
||||||
package io.opentelemetry.instrumentation.spring.webflux.client;
|
package io.opentelemetry.instrumentation.spring.webflux.client;
|
||||||
|
|
||||||
|
import static io.opentelemetry.api.trace.SpanKind.CLIENT;
|
||||||
|
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
|
||||||
|
import static java.util.Collections.emptyMap;
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||||
|
|
||||||
import io.opentelemetry.api.common.AttributeKey;
|
import io.opentelemetry.api.common.AttributeKey;
|
||||||
|
import io.opentelemetry.api.trace.SpanKind;
|
||||||
|
import io.opentelemetry.instrumentation.api.semconv.http.internal.HttpAttributes;
|
||||||
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
|
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
|
||||||
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult;
|
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult;
|
||||||
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
|
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
|
||||||
|
import io.opentelemetry.sdk.trace.data.StatusData;
|
||||||
import io.opentelemetry.semconv.SemanticAttributes;
|
import io.opentelemetry.semconv.SemanticAttributes;
|
||||||
import java.lang.invoke.MethodHandle;
|
import java.lang.invoke.MethodHandle;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.lang.invoke.MethodType;
|
import java.lang.invoke.MethodType;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
@ -142,4 +151,44 @@ public abstract class AbstractSpringWebfluxClientInstrumentationTest
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEndSpanOnMonoTimeout() {
|
||||||
|
URI uri = resolveAddress("/read-timeout");
|
||||||
|
Throwable thrown =
|
||||||
|
catchThrowable(
|
||||||
|
() ->
|
||||||
|
testing.runWithSpan(
|
||||||
|
"parent",
|
||||||
|
() ->
|
||||||
|
buildRequest("GET", uri, emptyMap())
|
||||||
|
.exchange()
|
||||||
|
// apply Mono timeout that is way shorter than HTTP request timeout
|
||||||
|
.timeout(Duration.ofSeconds(1))
|
||||||
|
.block()));
|
||||||
|
|
||||||
|
testing.waitAndAssertTraces(
|
||||||
|
trace ->
|
||||||
|
trace.hasSpansSatisfyingExactly(
|
||||||
|
span ->
|
||||||
|
span.hasName("parent")
|
||||||
|
.hasKind(SpanKind.INTERNAL)
|
||||||
|
.hasNoParent()
|
||||||
|
.hasStatus(StatusData.error())
|
||||||
|
.hasException(thrown),
|
||||||
|
span ->
|
||||||
|
span.hasName("GET")
|
||||||
|
.hasKind(CLIENT)
|
||||||
|
.hasParent(trace.getSpan(0))
|
||||||
|
.hasAttributesSatisfyingExactly(
|
||||||
|
equalTo(SemanticAttributes.HTTP_REQUEST_METHOD, "GET"),
|
||||||
|
equalTo(SemanticAttributes.URL_FULL, uri.toString()),
|
||||||
|
equalTo(SemanticAttributes.SERVER_ADDRESS, "localhost"),
|
||||||
|
equalTo(SemanticAttributes.SERVER_PORT, uri.getPort()),
|
||||||
|
equalTo(HttpAttributes.ERROR_TYPE, "cancelled")),
|
||||||
|
span ->
|
||||||
|
span.hasName("test-http-server")
|
||||||
|
.hasKind(SpanKind.SERVER)
|
||||||
|
.hasParent(trace.getSpan(1))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue