Repository navigation
feat(gax): implement operation-level client request spans (T3) #14549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2dcc21d
dac1816
390ab56
da65e2e
5b14eb0
a17ec91
cf1e495
6ee7326
6a5be3a
764beec
ef62122
2f08818
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,10 @@ | |
| import io.opentelemetry.api.trace.Span; | ||
| import io.opentelemetry.api.trace.SpanBuilder; | ||
| import io.opentelemetry.api.trace.SpanKind; | ||
| import io.opentelemetry.api.trace.StatusCode; | ||
| import io.opentelemetry.api.trace.Tracer; | ||
| import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; | ||
| import io.opentelemetry.context.Context; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.concurrent.CancellationException; | ||
|
|
@@ -51,15 +54,24 @@ | |
| private final Tracer tracer; | ||
| private final Map<String, Object> attemptAttributes; | ||
| private final String attemptSpanName; | ||
| private final String operationSpanName; | ||
| private final ApiTracerContext apiTracerContext; | ||
| // Captures the active trace context from the calling thread at RPC initiation. | ||
| // This allows the operation span and attempt spans to link back to the caller's trace. | ||
| private final Context parentContext; | ||
| // Trace context containing the operationSpan, serving as the parent for attempt spans. | ||
| private final Context operationContext; | ||
| private @Nullable Span operationSpan; | ||
| private @Nullable Span attemptSpan; | ||
|
|
||
| @Override | ||
| public void injectTraceContext(java.util.Map<String, String> carrier) { | ||
| if (attemptSpan != null) { | ||
| io.opentelemetry.context.Context context = | ||
| io.opentelemetry.context.Context.current().with(attemptSpan); | ||
| io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator.getInstance() | ||
| public void injectTraceContext(Map<String, String> carrier) { | ||
| // Prefer the active attempt span so outgoing RPC wire context reflects the specific attempt; | ||
| // fall back to the overall operation span if no attempt is currently in-flight. | ||
| Span currentSpan = attemptSpan != null ? attemptSpan : operationSpan; | ||
| if (currentSpan != null) { | ||
| Context context = Context.current().with(currentSpan); | ||
| W3CTraceContextPropagator.getInstance() | ||
| .inject( | ||
| context, | ||
| carrier, | ||
|
|
@@ -78,11 +90,7 @@ | |
| * @param apiTracerContext the {@link ApiTracerContext} to use for recording spans | ||
| */ | ||
| OpenTelemetryTracingTracer(Tracer tracer, ApiTracerContext apiTracerContext) { | ||
| this.tracer = tracer; | ||
| this.apiTracerContext = apiTracerContext; | ||
| this.attemptSpanName = resolveAttemptSpanName(apiTracerContext); | ||
| this.attemptAttributes = new HashMap<>(); | ||
| buildAttributes(); | ||
| this(tracer, apiTracerContext, resolveAttemptSpanName(apiTracerContext)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -97,12 +105,53 @@ | |
| OpenTelemetryTracingTracer( | ||
| Tracer tracer, ApiTracerContext apiTracerContext, String attemptSpanName) { | ||
| this.tracer = tracer; | ||
| this.attemptSpanName = attemptSpanName; | ||
| this.apiTracerContext = apiTracerContext; | ||
| this.attemptSpanName = attemptSpanName; | ||
| this.operationSpanName = resolveOperationSpanName(attemptSpanName); | ||
| this.attemptAttributes = new HashMap<>(); | ||
| this.parentContext = Context.current(); | ||
| buildAttributes(); | ||
| this.operationSpan = startOperationSpan(); | ||
| this.operationContext = parentContext.with(this.operationSpan); | ||
| } | ||
|
|
||
| /** | ||
| * Starts and initializes the operation-level client request span. | ||
| * | ||
| * @return the newly started {@link Span} for the overall operation | ||
| */ | ||
| private Span startOperationSpan() { | ||
| SpanBuilder operationSpanBuilder = tracer.spanBuilder(operationSpanName); | ||
| operationSpanBuilder.setSpanKind(SpanKind.INTERNAL); | ||
| operationSpanBuilder.setParent(parentContext); | ||
| operationSpanBuilder.setAllAttributes( | ||
| ObservabilityUtils.toOtelAttributes(this.apiTracerContext.getOperationAttributes())); | ||
| return operationSpanBuilder.startSpan(); | ||
| } | ||
|
|
||
| /** | ||
| * Derives the operation-level span name from the attempt span name. | ||
| * | ||
| * @param attemptSpanName the attempt span name | ||
| * @return the operation span name | ||
| */ | ||
| private static String resolveOperationSpanName(String attemptSpanName) { | ||
| if (!Strings.isNullOrEmpty(attemptSpanName)) { | ||
| if (attemptSpanName.endsWith("/attempt")) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the attemptSpanName would end with |
||
| String name = attemptSpanName.substring(0, attemptSpanName.length() - "/attempt".length()); | ||
| return name.isEmpty() ? "operation" : name; | ||
|
Check failure on line 142 in sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracer.java
|
||
| } | ||
| return "attempt".equals(attemptSpanName) ? "operation" : attemptSpanName; | ||
| } | ||
| return "operation"; | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the canonical attempt-level span name based on transport and context. | ||
| * | ||
| * @param apiTracerContext the tracer context containing transport and method metadata | ||
| * @return the attempt span name | ||
| */ | ||
| private static String resolveAttemptSpanName(ApiTracerContext apiTracerContext) { | ||
| if (apiTracerContext.transport() == ApiTracerContext.Transport.GRPC) { | ||
| // gRPC Uses the full method name as span name. | ||
|
|
@@ -118,6 +167,7 @@ | |
| } | ||
| } | ||
|
|
||
| /** Copies attempt-level attributes from the tracer context into the local attribute cache. */ | ||
| private void buildAttributes() { | ||
| this.attemptAttributes.putAll(this.apiTracerContext.getAttemptAttributes()); | ||
| } | ||
|
|
@@ -142,19 +192,72 @@ | |
| // Attempt spans are of the CLIENT kind | ||
| spanBuilder.setSpanKind(SpanKind.CLIENT); | ||
|
|
||
| // Link attempt span to operation context (parent operation span) | ||
| spanBuilder.setParent(operationContext); | ||
|
|
||
| // Pass the combined attributes to the new SpanBuilder method | ||
| spanBuilder.setAllAttributes(ObservabilityUtils.toOtelAttributes(currentAttemptAttributes)); | ||
|
|
||
| this.attemptSpan = spanBuilder.startSpan(); | ||
| } | ||
|
|
||
| /** | ||
| * Signals that the overall logical operation succeeded. | ||
| * | ||
| * <p>Closes any remaining in-flight attempt span and ends the operation span. | ||
| */ | ||
| @Override | ||
| public void operationSucceeded() { | ||
| recordErrorAndEndOperation(null); | ||
| } | ||
|
|
||
| /** | ||
| * Signals that the overall logical operation was cancelled. | ||
| * | ||
| * <p>Closes any remaining in-flight attempt span with a {@link CancellationException} and ends | ||
| * the operation span with an ERROR status. | ||
| */ | ||
| @Override | ||
| public void operationCancelled() { | ||
| recordErrorAndEndOperation(new CancellationException()); | ||
| } | ||
|
|
||
| /** | ||
| * Signals that the overall logical operation failed permanently. | ||
| * | ||
| * <p>Closes any remaining in-flight attempt span with the provided error details and ends the | ||
| * operation span with an ERROR status. | ||
| * | ||
| * @param error the cause of the operation failure | ||
| */ | ||
| @Override | ||
| public void operationFailed(Throwable error) { | ||
| recordErrorAndEndOperation(error); | ||
| } | ||
|
|
||
| /** | ||
| * Records error details and ends both the active attempt span and the operation span. | ||
| * | ||
| * @param error the exception associated with the operation failure, or {@code null} if successful | ||
| */ | ||
| private void recordErrorAndEndOperation(@Nullable Throwable error) { | ||
| if (attemptSpan != null) { | ||
| endSpan(attemptSpan, error); | ||
| attemptSpan = null; | ||
| } | ||
| if (operationSpan != null) { | ||
| endSpan(operationSpan, error); | ||
| operationSpan = null; | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void attemptSucceeded() { | ||
| recordErrorAndEndAttempt(null); | ||
| } | ||
|
|
||
| @Override | ||
| public void responseHeadersReceived(java.util.Map<String, Object> headers) { | ||
| public void responseHeadersReceived(Map<String, Object> headers) { | ||
| if (attemptSpan == null) { | ||
| return; | ||
| } | ||
|
|
@@ -174,7 +277,7 @@ | |
| * @param headers the map of response headers. | ||
| * @return the content length in bytes, or -1 if the header is missing or malformed. | ||
| */ | ||
| private long extractContentLength(java.util.Map<String, Object> headers) { | ||
| private long extractContentLength(Map<String, Object> headers) { | ||
| try { | ||
| if (headers == null || headers.isEmpty()) return -1; | ||
| // google-http-client HttpHeaders uses a case-insensitive map but we copy it for safety | ||
|
|
@@ -215,31 +318,40 @@ | |
| recordErrorAndEndAttempt(error); | ||
| } | ||
|
|
||
| /** | ||
| * Records error details and ends the current attempt span. | ||
| * | ||
| * @param error the exception associated with the attempt failure, or {@code null} if successful | ||
| */ | ||
| private void recordErrorAndEndAttempt(@Nullable Throwable error) { | ||
| if (attemptSpan == null) { | ||
| return; | ||
| } | ||
| endSpan(attemptSpan, error); | ||
| attemptSpan = null; | ||
| } | ||
|
|
||
| /** | ||
| * Attaches response status attributes and error messages to the span and ends it. | ||
| * | ||
| * @param span the span to finish | ||
| * @param error the exception that caused the span to end, or {@code null} if successful | ||
| */ | ||
| private void endSpan(Span span, @Nullable Throwable error) { | ||
| Map<String, Object> responseAttributes = | ||
| ObservabilityUtils.getResponseAttributes(error, this.apiTracerContext.transport()); | ||
| if (!responseAttributes.isEmpty()) { | ||
| attemptSpan.setAllAttributes(ObservabilityUtils.toOtelAttributes(responseAttributes)); | ||
| span.setAllAttributes(ObservabilityUtils.toOtelAttributes(responseAttributes)); | ||
| } | ||
|
|
||
| if (error != null && !Strings.isNullOrEmpty(error.getMessage())) { | ||
| attemptSpan.setAttribute( | ||
| ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE, error.getMessage()); | ||
| } | ||
|
|
||
| endAttempt(); | ||
| } | ||
|
|
||
| private void endAttempt() { | ||
| if (attemptSpan == null) { | ||
| return; | ||
| if (error != null) { | ||
| span.setStatus(StatusCode.ERROR); | ||
| if (!Strings.isNullOrEmpty(error.getMessage())) { | ||
| span.setAttribute(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE, error.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| attemptSpan.end(); | ||
| attemptSpan = null; | ||
| span.end(); | ||
| } | ||
|
|
||
| @Override | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should give operation span name more thought. It is typically the language package name + language method name, which are different from the proto package name and the proto method name. Something like
com.google.cloud.secretmanager.v1.SecretManagerServiceClient.accessSecretVersion. I don't think we have all the info available right now, so we may need to pass them toApiTracerContext.