Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,50 @@
@InternalApi
public class LoggingUtils {

private static boolean loggingEnabled = isLoggingEnabled();
static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING";
static final String GOOGLE_SDK_JAVA_LOGGING_V2 = "GOOGLE_SDK_JAVA_LOGGING_V2";

static boolean isLoggingEnabled() {
String enableLogging = System.getenv(GOOGLE_SDK_JAVA_LOGGING);
private static boolean loggingEnabled = checkLoggingEnabled(GOOGLE_SDK_JAVA_LOGGING);
private static boolean loggingV2Enabled = checkLoggingEnabled(GOOGLE_SDK_JAVA_LOGGING_V2);

/**
* Returns whether client-side logging is enabled (V1 or V2).
*
* @return true if logging is enabled, false otherwise.
*/
public static boolean isLoggingEnabled() {
return loggingEnabled || loggingV2Enabled;
}

/**
* Returns whether client-side logging V2 (Actionable Errors) is enabled.
*
* @return true if V2 logging is enabled, false otherwise.
*/
public static boolean isLoggingV2Enabled() {
return loggingV2Enabled;
}

/**
* Sets whether client-side logging is enabled. Visible for testing.
*
* @param enabled true to enable logging, false to disable.
*/
public static void setLoggingEnabled(boolean enabled) {
loggingEnabled = enabled;
}

/**
* Sets whether client-side logging V2 is enabled. Visible for testing.
*
* @param enabled true to enable logging, false to disable.
*/
public static void setLoggingV2Enabled(boolean enabled) {
loggingV2Enabled = enabled;
}

private static boolean checkLoggingEnabled(String envVar) {
String enableLogging = System.getenv(envVar);
return "true".equalsIgnoreCase(enableLogging);
}

Expand Down Expand Up @@ -126,6 +165,26 @@ public static <RespT> void logRequest(
}
}

/**
* Logs an actionable error message with structured context at a specific log level.
*
* @param logContext A map containing the structured logging context (e.g., RPC service, method,
* error details).
* @param loggerProvider The provider used to obtain the logger.
* @param level The slf4j level to log the actionable error at.
* @param message The human-readable error message.
*/
public static void logActionableError(
Map<String, Object> logContext,
LoggerProvider loggerProvider,
org.slf4j.event.Level level,
String message) {
if (loggingV2Enabled) {
org.slf4j.Logger logger = loggerProvider.getLogger();
Slf4jUtils.log(logger, level, logContext, message);
}
}

public static void executeWithTryCatch(ThrowingRunnable action) {
try {
action.run();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,11 @@ static void logResponse(
LoggingUtils.executeWithTryCatch(
() -> {
Logger logger = loggerProvider.getLogger();
if (logger.isInfoEnabled()) {
boolean isV2 = LoggingUtils.isLoggingV2Enabled();
if (!isV2 && logger.isInfoEnabled()) {
logDataBuilder.responseStatus(status);
}
if (logger.isInfoEnabled() && !logger.isDebugEnabled()) {
if (!isV2 && logger.isInfoEnabled() && !logger.isDebugEnabled()) {
Map<String, Object> responseData = logDataBuilder.build().toMapResponse();
Slf4jUtils.log(logger, Level.INFO, responseData, "Received response");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ public static ClientContext create(StubSettings settings) throws IOException {
if (apiTracerFactory instanceof SpanTracerFactory) {
apiTracerFactory = apiTracerFactory.withContext(apiTracerContext);
}
if (com.google.api.gax.logging.LoggingUtils.isLoggingV2Enabled()) {
com.google.api.gax.tracing.ApiTracerFactory loggingTracerFactory =
new com.google.api.gax.tracing.LoggingTracerFactory().withContext(apiTracerContext);
apiTracerFactory =
new com.google.api.gax.tracing.CompositeTracerFactory(
apiTracerFactory, loggingTracerFactory);
}

return newBuilder()
.setBackgroundResources(backgroundResources.build())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* Copyright 2026 Google LLC
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.google.api.gax.tracing;

import com.google.api.core.BetaApi;
import com.google.api.core.InternalApi;
import java.util.List;

/** A composite {@link ApiTracer} that delegates to a list of {@link ApiTracer}s. */
@BetaApi
@InternalApi
public class CompositeTracer implements ApiTracer {
private final List<ApiTracer> tracers;

public CompositeTracer(List<ApiTracer> tracers) {
this.tracers = tracers;
}

@Override
public Scope inScope() {
// Returning a scope that closes all sub-scopes
final Scope[] scopes = new Scope[tracers.size()];
for (int i = 0; i < tracers.size(); i++) {
scopes[i] = tracers.get(i).inScope();
}
return () -> {
for (Scope scope : scopes) {
if (scope != null) {
scope.close();
}
}
};
}

@Override
public void operationSucceeded() {
for (ApiTracer tracer : tracers) {
tracer.operationSucceeded();
}
}

@Override
public void operationCancelled() {
for (ApiTracer tracer : tracers) {
tracer.operationCancelled();
}
}

@Override
public void operationFailed(Throwable error) {
for (ApiTracer tracer : tracers) {
tracer.operationFailed(error);
}
}

@Override
public void connectionSelected(String id) {
for (ApiTracer tracer : tracers) {
tracer.connectionSelected(id);
}
}

@Override
@SuppressWarnings("deprecation")
public void attemptStarted(int attemptNumber) {
for (ApiTracer tracer : tracers) {
tracer.attemptStarted(attemptNumber);
}
}

@Override
public void attemptStarted(Object request, int attemptNumber) {
for (ApiTracer tracer : tracers) {
tracer.attemptStarted(request, attemptNumber);
}
}

@Override
public void attemptSucceeded() {
for (ApiTracer tracer : tracers) {
tracer.attemptSucceeded();
}
}

@Override
public void attemptCancelled() {
for (ApiTracer tracer : tracers) {
tracer.attemptCancelled();
}
}

@Override
@SuppressWarnings("deprecation")
public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) {
for (ApiTracer tracer : tracers) {
tracer.attemptFailed(error, delay);
}
}

@Override
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
for (ApiTracer tracer : tracers) {
tracer.attemptFailedDuration(error, delay);
}
}

@Override
public void attemptFailedRetriesExhausted(Throwable error) {
for (ApiTracer tracer : tracers) {
tracer.attemptFailedRetriesExhausted(error);
}
}

@Override
public void attemptPermanentFailure(Throwable error) {
for (ApiTracer tracer : tracers) {
tracer.attemptPermanentFailure(error);
}
}

@Override
public void lroStartFailed(Throwable error) {
for (ApiTracer tracer : tracers) {
tracer.lroStartFailed(error);
}
}

@Override
public void lroStartSucceeded() {
for (ApiTracer tracer : tracers) {
tracer.lroStartSucceeded();
}
}

@Override
public void responseReceived() {
for (ApiTracer tracer : tracers) {
tracer.responseReceived();
}
}

@Override
public void requestSent() {
for (ApiTracer tracer : tracers) {
tracer.requestSent();
}
}

@Override
public void batchRequestSent(long elementCount, long requestSize) {
for (ApiTracer tracer : tracers) {
tracer.batchRequestSent(elementCount, requestSize);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2026 Google LLC
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.google.api.gax.tracing;

import com.google.api.core.BetaApi;
import com.google.api.core.InternalApi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* A composite {@link ApiTracerFactory} that creates a {@link CompositeTracer} containing multiple
* {@link ApiTracer} instances.
*/
@BetaApi
@InternalApi
public class CompositeTracerFactory implements ApiTracerFactory {
private final List<ApiTracerFactory> factories;
private final ApiTracerContext apiTracerContext;

public CompositeTracerFactory(ApiTracerFactory... factories) {
this(Arrays.asList(factories), ApiTracerContext.empty());
}

public CompositeTracerFactory(List<ApiTracerFactory> factories) {
this(factories, ApiTracerContext.empty());
}

private CompositeTracerFactory(
List<ApiTracerFactory> factories, ApiTracerContext apiTracerContext) {
this.factories = factories;
this.apiTracerContext = apiTracerContext;
}

@Override
public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) {
List<ApiTracer> tracers = new ArrayList<>(factories.size());
for (ApiTracerFactory factory : factories) {
tracers.add(factory.newTracer(parent, spanName, operationType));
}
return new CompositeTracer(tracers);
}

@Override
public ApiTracer newTracer(ApiTracer parent, ApiTracerContext context) {
List<ApiTracer> tracers = new ArrayList<>(factories.size());
for (ApiTracerFactory factory : factories) {
tracers.add(factory.newTracer(parent, context));
}
return new CompositeTracer(tracers);
}

@Override
public ApiTracerContext getApiTracerContext() {
return apiTracerContext;
}

@Override
public ApiTracerFactory withContext(ApiTracerContext context) {
List<ApiTracerFactory> updatedFactories = new ArrayList<>(factories.size());
for (ApiTracerFactory factory : factories) {
updatedFactories.add(factory.withContext(context));
}
return new CompositeTracerFactory(updatedFactories, apiTracerContext.merge(context));
}
}
Loading
Loading