Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions gax-java/dependencies.properties
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-aut
maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.42.1
maven.io_opentelemetry_opentelemetry_api=io.opentelemetry:opentelemetry-api:1.47.0
maven.io_opentelemetry_opentelemetry_context=io.opentelemetry:opentelemetry-context:1.47.0
maven.io_opentelemetry_opentelemetry_sdk_testing=io.opentelemetry:opentelemetry_sdk_testing:1.47.0
maven.io_opencensus_opencensus_api=io.opencensus:opencensus-api:0.31.1
maven.io_opencensus_opencensus_contrib_grpc_metrics=io.opencensus:opencensus-contrib-grpc-metrics:0.31.1
maven.io_opencensus_opencensus_contrib_http_util=io.opencensus:opencensus-contrib-http-util:0.31.1
Expand Down
5 changes: 5 additions & 0 deletions gax-java/gax/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@
<artifactId>opentelemetry-context</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-testing</artifactId>
<scope>test</scope>
</dependency>
<!-- Logging dependency -->
<dependency>
<groupId>org.slf4j</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public abstract class LibraryMetadata {
@Nullable
public abstract String artifactName();

@Nullable
public abstract String version();

public static LibraryMetadata empty() {
return newBuilder().build();
}
Expand All @@ -65,6 +68,8 @@ public abstract static class Builder {

public abstract Builder setArtifactName(@Nullable String artifactName);

public abstract Builder setVersion(@Nullable String version);

public abstract LibraryMetadata build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,22 @@
import com.google.api.core.InternalApi;
import com.google.api.gax.rpc.LibraryMetadata;
import com.google.auto.value.AutoValue;
import org.apache.http.protocol.HTTP;

import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
//gcp.client.service [consistent across T4 spans]
//gcp.client.version [consistent across T4 spans]
// rpc.system.name (e.g., grpc, http) [consistent across T4 spans]
// rpc.response.status_code (for gRPC and HTTP) [from last T4 span]
// rpc.method (for gRPC and HTTP) [from the last T4 span]
//url.domain [consistent across T4 spans]
// url.template (for HTTP) [from the first T4 span]
// http.response.status_code (for HTTP) [from the last T4 span]
//server.address [from the last T4 span]
//server.port [from the last T4 span]
// error.type (if the overall T3 operation failed)

/**
* A context object that contains information used to infer attributes that are common for all
Expand All @@ -51,6 +64,15 @@ public abstract class ApiTracerContext {

public abstract LibraryMetadata libraryMetadata();

@Nullable
public abstract String serviceName();

@Nullable
public abstract String urlDomain();

@Nullable
public abstract String urlTemplate();

/**
* @return a map of attributes to be included in attempt-level spans
*/
Expand All @@ -68,6 +90,25 @@ public Map<String, String> getAttemptAttributes() {
return attributes;
}

Map<String, String> getMetricsAttributes() {
Map<String, String> attributes = new HashMap<>();
if (serverAddress() != null) {
attributes.put(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE, serverAddress());
}
if (serviceName() != null) {
attributes.put("gcp.client.service", serviceName());
}
if (transport() == HTTP) {
if (urlDomain() != null) {
attributes.put("url.domain", serviceName());
}
if (serviceName() != null) {
attributes.put("url.template", serviceName());
}
Comment thread
blakeli0 marked this conversation as resolved.
Outdated
}
return attributes;
Comment thread
blakeli0 marked this conversation as resolved.
Outdated
}

public static ApiTracerContext empty() {
return newBuilder().setLibraryMetadata(LibraryMetadata.empty()).build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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 static com.google.api.gax.tracing.ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE;

import com.google.api.gax.rpc.StatusCode;
import com.google.common.base.Stopwatch;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* This class computes golden signal metrics that can be observed in the lifecycle of an RPC
* operation. The responsibility of recording metrics should delegate to {@link
* GoldenSignalsMetricsRecorder}, hence this class should not have any knowledge about the
* observability framework (e.g. OpenTelemetry).
*/
class GoldenSignalMetricsTracer implements ApiTracer {
private final Stopwatch clientRequestTimer = Stopwatch.createStarted();
private final GoldenSignalsMetricsRecorder metricsRecorder;
private final Map<String, String> attributes = new HashMap<>();
private final ApiTracerContext apiTracerContext;

/**
* Creates the following instruments for the following metrics:
*
* <ul>
* <li>Client Request Duration: Histogram
* </ul>
*
* @param metricsRecorder OpenTelemetry
* @param apiTracerContext
Comment thread
blakeli0 marked this conversation as resolved.
Outdated
*/
GoldenSignalMetricsTracer(GoldenSignalsMetricsRecorder metricsRecorder, ApiTracerContext apiTracerContext) {
this.metricsRecorder = metricsRecorder;
this.apiTracerContext = apiTracerContext;
}

@Override
public void operationSucceeded() {
attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, StatusCode.Code.OK.toString());
attributes.putAll(apiTracerContext.getMetricsAttributes());
metricsRecorder.recordOperationLatency(
clientRequestTimer.elapsed(TimeUnit.SECONDS), attributes);
}

@Override
public void operationCancelled() {
attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, StatusCode.Code.CANCELLED.toString());
attributes.putAll(apiTracerContext.getMetricsAttributes());
metricsRecorder.recordOperationLatency(
clientRequestTimer.elapsed(TimeUnit.SECONDS), attributes);
}

@Override
public void operationFailed(Throwable error) {
attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error));
attributes.putAll(apiTracerContext.getMetricsAttributes());
metricsRecorder.recordOperationLatency(
clientRequestTimer.elapsed(TimeUnit.SECONDS), attributes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 io.opentelemetry.api.OpenTelemetry;

/**
* A {@link ApiTracerFactory} to build instances of {@link GoldenSignalMetricsTracer}.
*
* <p>This class is expected to be initialized once during client initialization.
*/
@BetaApi
@InternalApi
public class GoldenSignalMetricsTracerFactory implements ApiTracerFactory {

private ApiTracerContext apiTracerContext;
private final OpenTelemetry openTelemetry;
private GoldenSignalsMetricsRecorder metricsRecorder;

public GoldenSignalMetricsTracerFactory(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}

@Override
public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) {
if (metricsRecorder == null) {
// This should never happen, in case it happens, create a no-op api tracer to not block
// regular requests.
return new BaseApiTracer();
}
return new GoldenSignalMetricsTracer(metricsRecorder, apiTracerContext);
}

@Override
public ApiTracerFactory withContext(ApiTracerContext context) {
this.apiTracerContext = context;
this.metricsRecorder =
new GoldenSignalsMetricsRecorder(
openTelemetry, apiTracerContext.libraryMetadata().artifactName());
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.Meter;
import java.util.Map;

/**
* This class takes an OpenTelemetry object, and creates instruments (meters, histograms etc.) from
* it for recording golden signal metrics. There must be only one instance of
* GoldenSignalsMetricsRecorder per client, all the methods in this class are expected to be called
* from multiple threads, hence they need to be thread safe.
*/
class GoldenSignalsMetricsRecorder {
static final String CLIENT_REQUEST_DURATION_METRIC_NAME = "gcp.client.request.duration";
static final String CLIENT_REQUEST_DURATION_METRIC_DESCRIPTION =
"Measures the total time taken for a logical client request, including any retries, backoff, and pre/post-processing";

final DoubleHistogram clientRequestDurationRecorder;

GoldenSignalsMetricsRecorder(OpenTelemetry openTelemetry, String libraryName) {
Meter meter = openTelemetry.meterBuilder(libraryName).build();

this.clientRequestDurationRecorder =
meter
.histogramBuilder(CLIENT_REQUEST_DURATION_METRIC_NAME)
.setDescription(CLIENT_REQUEST_DURATION_METRIC_DESCRIPTION)
.setUnit("s")
.build();
}

void recordOperationLatency(double operationLatency, Map<String, String> attributes) {
clientRequestDurationRecorder.record(
operationLatency, ObservabilityUtils.toOtelAttributes(attributes));
}
}
Loading
Loading