Skip to content

Commit 7dfaa9f

Browse files
authored
Merge branch 'main' into trace-propagation-interceptors
2 parents 0f74f4e + 965761a commit 7dfaa9f

File tree

15 files changed

+596
-13
lines changed

15 files changed

+596
-13
lines changed

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import io.opentelemetry.api.common.AttributeKey;
2424
import io.opentelemetry.api.trace.Span;
2525
import io.opentelemetry.api.trace.Tracer;
26+
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
27+
import io.opentelemetry.context.Context;
2628
import java.io.IOException;
2729

2830
/**
@@ -76,6 +78,10 @@ public void initialize(HttpRequest request) throws IOException {
7678
// No active span to exists, skip instrumentation
7779
return;
7880
}
81+
// propagate the W3C Trace Context (traceID and spanID) from the active span in headers
82+
W3CTraceContextPropagator.getInstance()
83+
.inject(Context.current(), request.getHeaders(), HttpHeaders::set);
84+
7985
addInitialHttpAttributesToSpan(span, request);
8086

8187
HttpResponseInterceptor originalInterceptor = request.getResponseInterceptor();

java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializerTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,23 @@ public void testNoSpanIsCreatedIfNoActiveSpan() throws IOException {
145145
verify(delegateInitializer, times(1)).initialize(any(HttpRequest.class));
146146
}
147147

148+
@Test
149+
public void testTraceContextIsPropagatedInHeaders() throws IOException {
150+
HttpTransport transport = createTransport();
151+
HttpRequest request = buildGetRequest(transport, initializer, BASE_URL);
152+
153+
HttpResponse response = request.execute();
154+
response.disconnect();
155+
156+
assertEquals(
157+
String.format(
158+
"00-%s-%s-%s",
159+
parentSpan.getSpanContext().getTraceId(),
160+
parentSpan.getSpanContext().getSpanId(),
161+
parentSpan.getSpanContext().getTraceFlags().asHex()),
162+
request.getHeaders().get("traceparent"));
163+
}
164+
148165
@Test
149166
public void testDelegateInitializerIsCalled() throws IOException {
150167
HttpRequestInitializer delegateInitializer = mock(HttpRequestInitializer.class);

sdk-platform-java/gax-java/gax/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
<configuration>
140140
<argLine>@{argLine} -Djava.util.logging.SimpleFormatter.format="%1$tY %1$tl:%1$tM:%1$tS.%1$tL %2$s %4$s: %5$s%6$s%n"</argLine>
141141
<!-- These tests require an Env Var to be set. Use -PenvVarTest to ONLY run these tests -->
142-
<test>!EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,!LoggingEnabledTest</test>
142+
<test>!EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,!LoggingEnabledTest,!LoggingTracerTest</test>
143143
</configuration>
144144
</plugin>
145145
</plugins>
@@ -154,7 +154,7 @@
154154
<groupId>org.apache.maven.plugins</groupId>
155155
<artifactId>maven-surefire-plugin</artifactId>
156156
<configuration>
157-
<test>EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,LoggingEnabledTest</test>
157+
<test>EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,LoggingEnabledTest,LoggingTracerTest</test>
158158
</configuration>
159159
</plugin>
160160
</plugins>

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ public Map<String, Object> getAttemptAttributes() {
205205
attributes.put(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE, httpPathTemplate());
206206
}
207207
}
208+
if (!Strings.isNullOrEmpty(serviceName())) {
209+
attributes.put(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE, serviceName());
210+
}
208211
if (!Strings.isNullOrEmpty(destinationResourceId())) {
209212
attributes.put(
210213
ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE, destinationResourceId());

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
*/
3030
package com.google.api.gax.tracing;
3131

32+
import com.google.api.client.util.Strings;
3233
import com.google.api.core.BetaApi;
3334
import com.google.api.core.InternalApi;
35+
import com.google.api.gax.rpc.LibraryMetadata;
3436
import io.opentelemetry.api.OpenTelemetry;
3537

3638
/**
@@ -74,6 +76,13 @@ public ApiTracer newTracer(ApiTracer parent, ApiTracerContext methodLevelTracerC
7476

7577
@Override
7678
public ApiTracerFactory withContext(ApiTracerContext context) {
79+
if (context == null) {
80+
return new BaseApiTracerFactory();
81+
}
82+
LibraryMetadata metadata = context.libraryMetadata();
83+
if (metadata == null || metadata.isEmpty() || Strings.isNullOrEmpty(metadata.artifactName())) {
84+
return new BaseApiTracerFactory();
85+
}
7786
this.clientLevelTracerContext = context;
7887
this.metricsRecorder =
7988
new GoldenSignalsMetricsRecorder(
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.api.gax.tracing;
32+
33+
import com.google.api.core.BetaApi;
34+
import com.google.api.core.InternalApi;
35+
import com.google.api.gax.logging.LoggerProvider;
36+
import com.google.api.gax.logging.LoggingUtils;
37+
import com.google.common.annotations.VisibleForTesting;
38+
import com.google.rpc.ErrorInfo;
39+
import java.util.HashMap;
40+
import java.util.Map;
41+
42+
/**
43+
* An {@link ApiTracer} that logs actionable errors using {@link LoggingUtils} when an RPC attempt
44+
* fails.
45+
*/
46+
@BetaApi
47+
@InternalApi
48+
class LoggingTracer extends BaseApiTracer {
49+
private static final LoggerProvider LOGGER_PROVIDER =
50+
LoggerProvider.forClazz(LoggingTracer.class);
51+
52+
private final ApiTracerContext apiTracerContext;
53+
54+
LoggingTracer(ApiTracerContext apiTracerContext) {
55+
this.apiTracerContext = apiTracerContext;
56+
}
57+
58+
@Override
59+
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
60+
recordActionableError(error);
61+
}
62+
63+
@Override
64+
public void attemptFailedRetriesExhausted(Throwable error) {
65+
recordActionableError(error);
66+
}
67+
68+
@Override
69+
public void attemptPermanentFailure(Throwable error) {
70+
recordActionableError(error);
71+
}
72+
73+
@VisibleForTesting
74+
void recordActionableError(Throwable error) {
75+
if (error == null) {
76+
return;
77+
}
78+
79+
Map<String, Object> logContext = new HashMap<>(apiTracerContext.getAttemptAttributes());
80+
81+
logContext.put(
82+
ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE,
83+
ObservabilityUtils.extractStatus(error));
84+
85+
ErrorInfo errorInfo = ObservabilityUtils.extractErrorInfo(error);
86+
if (errorInfo != null) {
87+
if (errorInfo.getReason() != null && !errorInfo.getReason().isEmpty()) {
88+
logContext.put(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, errorInfo.getReason());
89+
}
90+
if (errorInfo.getDomain() != null && !errorInfo.getDomain().isEmpty()) {
91+
logContext.put(ObservabilityAttributes.ERROR_DOMAIN_ATTRIBUTE, errorInfo.getDomain());
92+
}
93+
if (errorInfo.getMetadataMap() != null) {
94+
for (Map.Entry<String, String> entry : errorInfo.getMetadataMap().entrySet()) {
95+
logContext.put(
96+
ObservabilityAttributes.ERROR_METADATA_ATTRIBUTE_PREFIX + entry.getKey(),
97+
entry.getValue());
98+
}
99+
}
100+
}
101+
102+
String message = error.getMessage() != null ? error.getMessage() : error.getClass().getName();
103+
LoggingUtils.logActionableError(logContext, LOGGER_PROVIDER, message);
104+
}
105+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.api.gax.tracing;
32+
33+
import com.google.api.core.BetaApi;
34+
import com.google.api.core.InternalApi;
35+
36+
/** A {@link ApiTracerFactory} that creates instances of {@link LoggingTracer}. */
37+
@BetaApi
38+
@InternalApi
39+
public class LoggingTracerFactory implements ApiTracerFactory {
40+
private final ApiTracerContext apiTracerContext;
41+
42+
public LoggingTracerFactory() {
43+
this(ApiTracerContext.empty());
44+
}
45+
46+
private LoggingTracerFactory(ApiTracerContext apiTracerContext) {
47+
this.apiTracerContext = apiTracerContext;
48+
}
49+
50+
@Override
51+
public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) {
52+
return new LoggingTracer(apiTracerContext);
53+
}
54+
55+
@Override
56+
public ApiTracer newTracer(ApiTracer parent, ApiTracerContext context) {
57+
return new LoggingTracer(apiTracerContext.merge(context));
58+
}
59+
60+
@Override
61+
public ApiTracerContext getApiTracerContext() {
62+
return apiTracerContext;
63+
}
64+
65+
@Override
66+
public ApiTracerFactory withContext(ApiTracerContext context) {
67+
return new LoggingTracerFactory(apiTracerContext.merge(context));
68+
}
69+
}

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,13 @@ public class ObservabilityAttributes {
9696

9797
/** The destination resource id of the request (e.g. projects/p/locations/l/topics/t). */
9898
public static final String DESTINATION_RESOURCE_ID_ATTRIBUTE = "gcp.resource.destination.id";
99+
100+
/** The type of error that occurred (e.g., from google.rpc.ErrorInfo.reason). */
101+
public static final String ERROR_TYPE_ATTRIBUTE = "error.type";
102+
103+
/** The domain of the error (e.g., from google.rpc.ErrorInfo.domain). */
104+
public static final String ERROR_DOMAIN_ATTRIBUTE = "gcp.errors.domain";
105+
106+
/** The prefix for error metadata (e.g., from google.rpc.ErrorInfo.metadata). */
107+
public static final String ERROR_METADATA_ATTRIBUTE_PREFIX = "gcp.errors.metadata.";
99108
}

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import com.google.api.gax.rpc.ApiException;
3333
import com.google.api.gax.rpc.StatusCode;
34+
import com.google.rpc.ErrorInfo;
3435
import io.opentelemetry.api.common.Attributes;
3536
import io.opentelemetry.api.common.AttributesBuilder;
3637
import java.util.Map;
@@ -56,6 +57,18 @@ static String extractStatus(@Nullable Throwable error) {
5657
return statusString;
5758
}
5859

60+
/** Function to extract the ErrorInfo payload from the error, if available */
61+
@Nullable
62+
static ErrorInfo extractErrorInfo(@Nullable Throwable error) {
63+
if (error instanceof ApiException) {
64+
ApiException apiException = (ApiException) error;
65+
if (apiException.getErrorDetails() != null) {
66+
return apiException.getErrorDetails().getErrorInfo();
67+
}
68+
}
69+
return null;
70+
}
71+
5972
static Attributes toOtelAttributes(Map<String, Object> attributes) {
6073
AttributesBuilder attributesBuilder = Attributes.builder();
6174
if (attributes == null) {

sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/logging/TestLogger.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,20 @@ public class TestLogger implements Logger, LoggingEventAware {
4848
List<String> messageList = new ArrayList<>();
4949
Level level;
5050

51+
public List<String> getMessageList() {
52+
return messageList;
53+
}
54+
5155
Map<String, Object> keyValuePairsMap = new HashMap<>();
5256

57+
public Map<String, String> getMDCMap() {
58+
return MDCMap;
59+
}
60+
61+
public Map<String, Object> getKeyValuePairsMap() {
62+
return keyValuePairsMap;
63+
}
64+
5365
private String loggerName;
5466
private boolean infoEnabled;
5567
private boolean debugEnabled;

0 commit comments

Comments
 (0)