diff --git a/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md b/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md index 1f029f24c773f..da3ba92a0dddc 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md +++ b/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md @@ -8,6 +8,8 @@ ### Bugs Fixed +- Fixed unreliable HTTP span reporting when response is not closed. + ### Other Changes ## 1.0.0-beta.44 (2024-03-01) diff --git a/sdk/core/azure-core-tracing-opentelemetry/src/main/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracer.java b/sdk/core/azure-core-tracing-opentelemetry/src/main/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracer.java index 6b29677b51686..1a0389024a3cf 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/src/main/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracer.java +++ b/sdk/core/azure-core-tracing-opentelemetry/src/main/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracer.java @@ -102,7 +102,7 @@ public Context start(String spanName, StartSpanOptions options, Context context) return startSuppressedSpan(context); } context = unsuppress(context); - if (spanKind == SpanKind.INTERNAL && !context.getData(CLIENT_METHOD_CALL_FLAG).isPresent()) { + if (isInternalOrClientSpan(spanKind) && !context.getData(CLIENT_METHOD_CALL_FLAG).isPresent()) { context = context.addData(CLIENT_METHOD_CALL_FLAG, true); } @@ -184,6 +184,9 @@ private SpanBuilder createSpanBuilder(String spanName, StartSpanOptions options, return spanBuilder; } + /** + * {@inheritDoc} + */ @Override public void injectContext(BiConsumer headerSetter, Context context) { io.opentelemetry.context.Context otelContext = getTraceContextOrDefault(context, null); @@ -192,6 +195,9 @@ public void injectContext(BiConsumer headerSetter, Context conte } } + /** + * {@inheritDoc} + */ @Override public void setAttribute(String key, long value, Context context) { Objects.requireNonNull(context, "'context' cannot be null"); @@ -235,6 +241,28 @@ public void setAttribute(String key, String value, Context context) { } } + /** + * {@inheritDoc} + */ + @Override + public void setAttribute(String key, Object value, Context context) { + Objects.requireNonNull(value, "'value' cannot be null"); + Objects.requireNonNull(context, "'context' cannot be null"); + + if (!isEnabled) { + return; + } + + final Span span = getSpanOrNull(context); + if (span == null) { + return; + } + + if (span.isRecording()) { + OpenTelemetryUtils.addAttribute(span, key, value); + } + } + /** * {@inheritDoc} */ @@ -267,6 +295,24 @@ public Context extractContext(Function headerGetter) { return new Context(SPAN_CONTEXT_KEY, Span.fromContext(traceContext).getSpanContext()); } + /** + * {@inheritDoc} + */ + @Override + public boolean isRecording(Context context) { + Objects.requireNonNull(context, "'context' cannot be null"); + if (!isEnabled) { + return false; + } + + Span span = getSpanOrNull(context); + if (span != null) { + return span.isRecording(); + } + + return false; + } + private static class Getter implements TextMapGetter> { public static final TextMapGetter> INSTANCE = new Getter(); @@ -420,4 +466,8 @@ private static TracerProvider getTracerProvider(TracingOptions options) { return GlobalOpenTelemetry.getTracerProvider(); } + + private static boolean isInternalOrClientSpan(SpanKind kind) { + return kind == SpanKind.INTERNAL || kind == SpanKind.CLIENT; + } } diff --git a/sdk/core/azure-core-tracing-opentelemetry/src/test/java/com/azure/core/tracing/opentelemetry/OpenTelemetryHttpPolicyTests.java b/sdk/core/azure-core-tracing-opentelemetry/src/test/java/com/azure/core/tracing/opentelemetry/OpenTelemetryHttpPolicyTests.java index c452de4d6387c..9b5fc6309144a 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/src/test/java/com/azure/core/tracing/opentelemetry/OpenTelemetryHttpPolicyTests.java +++ b/sdk/core/azure-core-tracing-opentelemetry/src/test/java/com/azure/core/tracing/opentelemetry/OpenTelemetryHttpPolicyTests.java @@ -112,7 +112,7 @@ public void openTelemetryHttpPolicyTest() { request.setHeader(HttpHeaderName.USER_AGENT, "user-agent"); try (Scope scope = parentSpan.makeCurrent()) { - createHttpPipeline(azTracer).send(request, tracingContext).block().close(); + createHttpPipeline(azTracer).send(request, tracingContext).block().getBodyAsBinaryData(); } // Assert List exportedSpans = exporter.getFinishedSpanItems(); @@ -181,8 +181,9 @@ public String getDescription() { public void clientRequestIdIsStamped() { try (Scope scope = tracer.spanBuilder("test").startSpan().makeCurrent()) { HttpRequest request = new HttpRequest(HttpMethod.PUT, "https://httpbin.org/hello?there#otel"); - HttpResponse response = createHttpPipeline(azTracer, new RequestIdPolicy()).send(request).block(); - response.close(); + HttpResponse response = createHttpPipeline(azTracer, new RequestIdPolicy()).send(request) + .flatMap(r -> r.getBodyAsByteArray().thenReturn(r)) + .block(); // Assert List exportedSpans = exporter.getFinishedSpanItems(); @@ -293,8 +294,8 @@ public void endStatusDependingOnStatusCode(int statusCode, StatusCode status) { StepVerifier.create(pipeline.send(new HttpRequest(HttpMethod.GET, "http://localhost/hello"))) .assertNext(response -> { + response.getBodyAsByteArray().block(); assertEquals(statusCode, response.getStatusCode()); - response.close(); }) .verifyComplete(); @@ -366,12 +367,10 @@ public void timeoutIsTraced() { = new Context(PARENT_TRACE_CONTEXT_KEY, io.opentelemetry.context.Context.root().with(parentSpan)) .addData("az.namespace", "foo"); - StepVerifier.create(pipeline.send(new HttpRequest(HttpMethod.GET, "http://localhost/hello"), tracingContext)) - .assertNext(response -> { + StepVerifier.create(pipeline.send(new HttpRequest(HttpMethod.GET, "http://localhost/hello"), tracingContext) + .flatMap(r -> r.getBody().collectList().thenReturn(r))).assertNext(response -> { assertEquals(200, response.getStatusCode()); - response.close(); - }) - .verifyComplete(); + }).verifyComplete(); List exportedSpans = exporter.getFinishedSpanItems(); assertEquals(2, exportedSpans.size()); @@ -398,11 +397,8 @@ public void connectionErrorAfterResponseCodeIsTraced() { .tracer(azTracer) .build(); - StepVerifier - .create(pipeline.send(new HttpRequest(HttpMethod.GET, "http://localhost/hello"), Context.NONE) - .flatMap(response -> response.getBodyAsInputStream().doFinally(i -> response.close()))) - .expectError(IOException.class) - .verify(); + StepVerifier.create(pipeline.send(new HttpRequest(HttpMethod.GET, "http://localhost/hello"), Context.NONE) + .flatMap(response -> response.getBodyAsInputStream())).expectError(IOException.class).verify(); List exportedSpans = exporter.getFinishedSpanItems(); assertEquals(1, exportedSpans.size()); diff --git a/sdk/core/azure-core-tracing-opentelemetry/src/test/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracerTest.java b/sdk/core/azure-core-tracing-opentelemetry/src/test/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracerTest.java index c2f5c76ae12c8..63663e2f04368 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/src/test/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracerTest.java +++ b/sdk/core/azure-core-tracing-opentelemetry/src/test/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracerTest.java @@ -36,6 +36,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.Exceptions; import java.io.IOException; @@ -285,6 +286,28 @@ public void startWithRemoteParent() { assertEquals(SpanKind.CONSUMER, spanData.getKind()); } + @ParameterizedTest + @ValueSource(booleans = { true, false }) + public void testIsRecording(boolean isRecording) { + // Arrange + SpanContext remoteParent + = SpanContext.create(IdGenerator.random().generateTraceId(), IdGenerator.random().generateSpanId(), + isRecording ? TraceFlags.getSampled() : TraceFlags.getDefault(), TraceState.getDefault()); + StartSpanOptions options = new StartSpanOptions(com.azure.core.util.tracing.SpanKind.CLIENT) + .setRemoteParent(new Context(SPAN_CONTEXT_KEY, remoteParent)); + + // Act + final Context span = openTelemetryTracer.start(METHOD_NAME, options, Context.NONE); + + // Assert + assertEquals(isRecording, openTelemetryTracer.isRecording(span)); + } + + @Test + public void testIsRecordingNoSpan() { + assertFalse(openTelemetryTracer.isRecording(Context.NONE)); + } + @Test @SuppressWarnings("deprecation") public void startSpanProcessKindSend() { @@ -462,7 +485,6 @@ public void startConsumeSpanWitStartTimeInContext() { @Test @SuppressWarnings("deprecation") public void startSpanOverloadNullPointerException() { - // Assert assertThrows(NullPointerException.class, () -> openTelemetryTracer.start("", Context.NONE, null)); } @@ -476,23 +498,19 @@ public void startSpanInvalid() { new StartSpanOptions(com.azure.core.util.tracing.SpanKind.CONSUMER), Context.NONE)); assertThrows(NullPointerException.class, () -> openTelemetryTracer.start("span", new StartSpanOptions(com.azure.core.util.tracing.SpanKind.CONSUMER), null)); - } @Test - @SuppressWarnings("deprecation") public void addLinkTest() { // Arrange - StartSpanOptions spanBuilder = new StartSpanOptions(com.azure.core.util.tracing.SpanKind.INTERNAL); Span toLinkSpan = tracer.spanBuilder("new test span").startSpan(); - Context linkContext = new Context(SPAN_CONTEXT_KEY, toLinkSpan.getSpanContext()); LinkData expectedLink = LinkData.create(toLinkSpan.getSpanContext()); + StartSpanOptions startOptions + = new StartSpanOptions(com.azure.core.util.tracing.SpanKind.INTERNAL).addLink(new TracingLink(linkContext)); // Act - openTelemetryTracer.addLink(linkContext.addData(SPAN_BUILDER_KEY, spanBuilder)); - - Context span = openTelemetryTracer.start(METHOD_NAME, spanBuilder, Context.NONE); + Context span = openTelemetryTracer.start(METHOD_NAME, startOptions, Context.NONE); ReadableSpan span1 = getSpan(span); // Assert @@ -886,6 +904,36 @@ public void startSpanWithAttributes() { verifySpanAttributes(expectedAttributes, span.toSpanData().getAttributes()); } + @Test + public void spanAttributes() { + Map attributes = new HashMap<>(); + attributes.put("S", "foo"); + attributes.put("I", 1); + attributes.put("L", 10L); + attributes.put("D", 0.1d); + attributes.put("B", true); + attributes.put("S[]", new String[] { "foo" }); + attributes.put("L[]", new long[] { 10L }); + attributes.put("D[]", new double[] { 0.1d }); + attributes.put("B[]", new boolean[] { true }); + attributes.put("I[]", new int[] { 1 }); + attributes.put("Complex", Collections.singletonMap("key", "value")); + + Attributes expectedAttributes = Attributes.builder() + .put("S", "foo") + .put("L", 10L) + .put("I", 1) + .put("D", 0.1d) + .put("B", true) + .put("az.namespace", AZ_NAMESPACE_VALUE) + .build(); + + Context spanCtx = openTelemetryTracer.start(METHOD_NAME, tracingContext); + attributes.forEach((key, value) -> openTelemetryTracer.setAttribute(key, value, spanCtx)); + final ReadableSpan span = getSpan(spanCtx); + verifySpanAttributes(expectedAttributes, span.toSpanData().getAttributes()); + } + @Test public void suppressNestedClientSpan() { Context outer = openTelemetryTracer.start("outer", Context.NONE); @@ -909,9 +957,9 @@ public void suppressNestedClientSpan() { } @Test - @SuppressWarnings("deprecation") public void suppressNestedInterleavedClientSpan() { - Context outer = openTelemetryTracer.start("outer", Context.NONE, com.azure.core.util.tracing.ProcessKind.SEND); + Context outer = openTelemetryTracer.start("outer", + new StartSpanOptions(com.azure.core.util.tracing.SpanKind.CONSUMER), Context.NONE); Context inner1NotSuppressed = openTelemetryTracer.start("innerSuppressed", outer); Context inner2Suppressed = openTelemetryTracer.start("innerSuppressed", inner1NotSuppressed); @@ -940,7 +988,7 @@ public void suppressNestedInterleavedClientSpanWithOptions() { Context inner1Suppressed = openTelemetryTracer.start("innerSuppressed", outer); Context inner1NotSuppressed = openTelemetryTracer.start("innerNotSuppressed", - new StartSpanOptions(com.azure.core.util.tracing.SpanKind.CLIENT), inner1Suppressed); + new StartSpanOptions(com.azure.core.util.tracing.SpanKind.PRODUCER), inner1Suppressed); Context inner2Suppressed = openTelemetryTracer.start("innerSuppressed", inner1NotSuppressed); openTelemetryTracer.end("ok", null, inner2Suppressed); @@ -1009,7 +1057,7 @@ public static Stream spanKinds() { Arguments.of(com.azure.core.util.tracing.SpanKind.CLIENT, com.azure.core.util.tracing.SpanKind.CLIENT, false), Arguments.of(com.azure.core.util.tracing.SpanKind.CLIENT, com.azure.core.util.tracing.SpanKind.INTERNAL, - false), + true), Arguments.of(com.azure.core.util.tracing.SpanKind.CLIENT, com.azure.core.util.tracing.SpanKind.PRODUCER, false), Arguments.of(com.azure.core.util.tracing.SpanKind.CLIENT, com.azure.core.util.tracing.SpanKind.CONSUMER, diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 7b8546e9b8cae..246794c098fae 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added new methods on `com.azure.core.util.tracing.Tracer` - `isRecording` and `addAttribute(String, Object, Context)`. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/policy/InstrumentationPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/policy/InstrumentationPolicy.java index 9a3b22760d2e7..3dc2f4e665b87 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/policy/InstrumentationPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/policy/InstrumentationPolicy.java @@ -25,6 +25,7 @@ import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import static com.azure.core.http.HttpHeaderName.X_MS_CLIENT_REQUEST_ID; import static com.azure.core.http.HttpHeaderName.X_MS_REQUEST_ID; @@ -54,6 +55,8 @@ public class InstrumentationPolicy implements HttpPipelinePolicy { private static final String SERVER_PORT = "server.port"; private static final ClientLogger LOGGER = new ClientLogger(InstrumentationPolicy.class); + // magic OpenTelemetry string that represents unknown error. + private static final String OTHER_ERROR_TYPE = "_OTHER"; private Tracer tracer; /** @@ -82,7 +85,7 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN return next.process() .doOnSuccess(response -> onResponseCode(response, span)) // TODO: maybe we can optimize it? /~https://github.com/Azure/azure-sdk-for-java/issues/38228 - .map(response -> new TraceableResponse(response, span)) + .map(response -> TraceableResponse.create(response, tracer, span)) .doOnCancel(() -> tracer.end(CANCELLED_ERROR_TYPE, null, span)) .doOnError(exception -> tracer.end(null, exception, span)); }); @@ -100,7 +103,7 @@ public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNex HttpResponse response = next.processSync(); onResponseCode(response, span); // TODO: maybe we can optimize it? /~https://github.com/Azure/azure-sdk-for-java/issues/38228 - return new TraceableResponse(response, span); + return TraceableResponse.create(response, tracer, span); } catch (RuntimeException ex) { tracer.end(null, ex, span); throw ex; @@ -150,7 +153,7 @@ private void addPostSamplingAttributes(Context span, HttpRequest request) { } private void onResponseCode(HttpResponse response, Context span) { - if (response != null) { + if (response != null && tracer.isRecording(span)) { int statusCode = response.getStatusCode(); tracer.setAttribute(HTTP_STATUS_CODE, statusCode, span); String requestId = response.getHeaderValue(X_MS_REQUEST_ID); @@ -164,16 +167,29 @@ private boolean isTracingEnabled(HttpPipelineCallContext context) { return tracer != null && tracer.isEnabled() && !((boolean) context.getData(DISABLE_TRACING_KEY).orElse(false)); } - private final class TraceableResponse extends HttpResponse { + private static final class TraceableResponse extends HttpResponse { private final HttpResponse response; private final Context span; - private Throwable exception; - private String errorType; + private final Tracer tracer; + private volatile int ended = 0; + private static final AtomicIntegerFieldUpdater ENDED_UPDATER + = AtomicIntegerFieldUpdater.newUpdater(TraceableResponse.class, "ended"); - TraceableResponse(HttpResponse response, Context span) { + private TraceableResponse(HttpResponse response, Tracer tracer, Context span) { super(response.getRequest()); this.response = response; this.span = span; + this.tracer = tracer; + } + + public static HttpResponse create(HttpResponse response, Tracer tracer, Context span) { + if (tracer.isRecording(span)) { + return new TraceableResponse(response, tracer, span); + } + + // OTel does not need to end sampled-out spans, but let's do it just in case + tracer.end(null, null, span); + return response; } @Override @@ -199,21 +215,21 @@ public HttpHeaders getHeaders() { @Override public Flux getBody() { - return response.getBody().doOnError(e -> exception = e).doOnCancel(() -> errorType = CANCELLED_ERROR_TYPE); + return Flux.using(() -> span, + s -> response.getBody() + .doOnError(e -> onError(null, e)) + .doOnCancel(() -> onError(CANCELLED_ERROR_TYPE, null)), + s -> endNoError()); } @Override public Mono getBodyAsByteArray() { - return response.getBodyAsByteArray() - .doOnError(e -> exception = e) - .doOnCancel(() -> errorType = CANCELLED_ERROR_TYPE); + return endSpanWhen(response.getBodyAsByteArray()); } @Override public Mono getBodyAsString() { - return response.getBodyAsString() - .doOnError(e -> exception = e) - .doOnCancel(() -> errorType = CANCELLED_ERROR_TYPE); + return endSpanWhen(response.getBodyAsString()); } @Override @@ -221,34 +237,52 @@ public BinaryData getBodyAsBinaryData() { try { return response.getBodyAsBinaryData(); } catch (Exception e) { - exception = e; + onError(null, e); throw e; + } finally { + endNoError(); } } @Override public Mono getBodyAsString(Charset charset) { - return response.getBodyAsString(charset) - .doOnError(e -> exception = e) - .doOnCancel(() -> errorType = CANCELLED_ERROR_TYPE); + return endSpanWhen(response.getBodyAsString(charset)); } @Override public Mono getBodyAsInputStream() { - return response.getBodyAsInputStream() - .doOnError(e -> exception = e) - .doOnCancel(() -> errorType = CANCELLED_ERROR_TYPE); + return endSpanWhen(response.getBodyAsInputStream()); } @Override public void close() { response.close(); - int statusCode = response.getStatusCode(); + endNoError(); + } + + private Mono endSpanWhen(Mono publisher) { + return Mono.using(() -> span, + s -> publisher.doOnError(e -> onError(null, e)).doOnCancel(() -> onError(CANCELLED_ERROR_TYPE, null)), + s -> endNoError()); + } + + private void onError(String errorType, Throwable error) { + if (ENDED_UPDATER.compareAndSet(this, 0, 1)) { + tracer.end(errorType, error, span); + } + } + + private void endNoError() { + if (ENDED_UPDATER.compareAndSet(this, 0, 1)) { + String errorType = null; + if (response == null) { + errorType = OTHER_ERROR_TYPE; + } else if (response.getStatusCode() >= 400) { + errorType = String.valueOf(response.getStatusCode()); + } - if (errorType == null && statusCode >= 400) { - errorType = String.valueOf(statusCode); + tracer.end(errorType, null, span); } - tracer.end(errorType, exception, span); } } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/AsyncRestProxy.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/AsyncRestProxy.java index 37e7693d40b87..e45a5a7dec7be 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/AsyncRestProxy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/AsyncRestProxy.java @@ -19,6 +19,7 @@ import com.azure.core.util.FluxUtil; import com.azure.core.util.serializer.SerializerAdapter; import com.azure.core.util.serializer.SerializerEncoding; +import com.azure.core.util.tracing.Tracer; import com.azure.json.JsonSerializable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -277,7 +278,7 @@ private Object handleRestReturnType(Mono tracer.end(CANCELLED_ERROR_TYPE, null, span)) - .contextWrite(reactor.util.context.Context.of("TRACING_CONTEXT", span)); + .contextWrite(reactor.util.context.Context.of(Tracer.PARENT_TRACE_CONTEXT_KEY, span)); } return getResponse; diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java index 2314b0f46d269..1aa122f8b38b5 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java @@ -227,7 +227,7 @@ public Response createResponse(HttpResponseDecoder.HttpDecodedResponse response, */ Context startTracingSpan(SwaggerMethodParser method, Context context) { if (isTracingEnabled(context)) { - Object tracingContextObj = context.getData("TRACING_CONTEXT").orElse(null); + Object tracingContextObj = context.getData(Tracer.PARENT_TRACE_CONTEXT_KEY).orElse(null); Context tracingContext = tracingContextObj instanceof Context ? (Context) tracingContextObj : context; return tracer.start(method.getSpanName(), tracingContext); } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/tracing/Tracer.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/tracing/Tracer.java index 4409333255242..ea343c3a75278 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/util/tracing/Tracer.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/tracing/Tracer.java @@ -371,7 +371,6 @@ default void end(int responseCode, Throwable error, Context context) { * tracer.setAttribute("foo", 42, span); * * - * @param key attribute name * @param value atteribute value * @param context tracing context @@ -380,6 +379,28 @@ default void setAttribute(String key, long value, Context context) { setAttribute(key, Long.toString(value), context); } + /** + * Sets an attribute on span. + * Adding duplicate attributes, update, or removal is discouraged, since underlying implementations + * behavior can vary. + * + * @param key attribute key. + * @param value attribute value. Note that underlying tracer implementations limit supported value types. + * OpenTelemetry implementation supports following types: + *
    + *
  • {@link String}
  • + *
  • {@code int}
  • + *
  • {@code double}
  • + *
  • {@code boolean}
  • + *
  • {@code long}
  • + *
+ * @param context context containing span to which attribute is added. + */ + default void setAttribute(String key, Object value, Context context) { + Objects.requireNonNull(value, "'value' cannot be null."); + setAttribute(key, value.toString(), context); + } + /** * Sets the name for spans that are created. * @@ -620,6 +641,16 @@ default AutoCloseable makeSpanCurrent(Context context) { return NoopTracer.INSTANCE.makeSpanCurrent(context); } + /** + * Checks if span is sampled in. + * + * @param span Span to check. + * @return true if span is recording, false otherwise. + */ + default boolean isRecording(Context span) { + return true; + } + /** * Checks if tracer is enabled. * diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/util/tracing/RestProxyTracingTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/util/tracing/RestProxyTracingTests.java index 933deddbdc979..98f2faeb48f37 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/util/tracing/RestProxyTracingTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/util/tracing/RestProxyTracingTests.java @@ -56,8 +56,8 @@ void beforeEach() { @SyncAsyncTest public void restProxySuccess() throws Exception { - SyncAsyncExtension.execute(() -> testInterface.testMethodReturnsMonoVoidSync(), - () -> testInterface.testMethodReturnsMonoVoid().block()); + SyncAsyncExtension.execute(() -> testInterface.testMethodReturnsMonoVoidSync(Context.NONE), + () -> testInterface.testMethodReturnsMonoVoid(Context.NONE).block()); assertEquals(2, tracer.getSpans().size()); Span restProxy = tracer.getSpans().get(0); @@ -69,9 +69,28 @@ public void restProxySuccess() throws Exception { assertNull(restProxy.getErrorMessage()); } + @SyncAsyncTest + public void restProxyNested() throws Exception { + Context outerSpan = tracer.start("outer", Context.NONE); + SyncAsyncExtension.execute(() -> testInterface.testMethodReturnsMonoVoidSync(outerSpan), + () -> testInterface.testMethodReturnsMonoVoid(outerSpan).block()); + tracer.end(null, null, outerSpan); + + assertEquals(3, tracer.getSpans().size()); + Span outer = tracer.getSpans().get(0); + Span restProxy = tracer.getSpans().get(1); + Span http = tracer.getSpans().get(2); + + assertEquals(getSpan(restProxy.getStartContext()), outer); + assertEquals(getSpan(http.getStartContext()), restProxy); + assertTrue(restProxy.getName().startsWith("myService.testMethodReturnsMonoVoid")); + assertNull(restProxy.getThrowable()); + assertNull(restProxy.getErrorMessage()); + } + @Test public void restProxyCancelAsync() { - testInterface.testMethodDelays().timeout(Duration.ofMillis(10)).toFuture().cancel(true); + StepVerifier.create(testInterface.testMethodDelays()).expectSubscription().thenCancel().verify(); assertEquals(2, tracer.getSpans().size()); Span restProxy = tracer.getSpans().get(0); @@ -235,11 +254,11 @@ public HttpResponse sendSync(HttpRequest request, Context context) { interface TestInterface { @Get("my/url/path") @ExpectedResponses({ 200 }) - Mono testMethodReturnsMonoVoid(); + Mono testMethodReturnsMonoVoid(Context context); @Get("my/url/path") @ExpectedResponses({ 200 }) - Response testMethodReturnsMonoVoidSync(); + Response testMethodReturnsMonoVoidSync(Context context); @Post("my/url/path") @ExpectedResponses({ 500 })