Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
ca57ee4
feat(gax): link T3 attempt spans to parent context and clean up on op…
jinseopkim0 Sep 25, 2026
d599721
fix(gax): prevent new attempt spans after operation completion
jinseopkim0 Sep 25, 2026
cda9ac6
fix(gax): use imported ReentrantLock
jinseopkim0 Sep 25, 2026
18a5cff
chore(gax): add integration test for OpenTelemetry attempt span tracing
jinseopkim0 Sep 28, 2026
2d5db07
chore(gax): add explanatory comments to OpenTelemetryTracingTracer
jinseopkim0 Sep 28, 2026
ed0e443
chore(gax): document new and overridden methods in OpenTelemetryTraci…
jinseopkim0 Sep 28, 2026
e15ae2d
fix(gax): add opentelemetry-sdk-trace test dependency in maven and bazel
jinseopkim0 Sep 28, 2026
7ba64dc
feat(gax): implement operation-level client request spans (T3)
jinseopkim0 Sep 29, 2026
349c14a
fix(gax): address review feedback on span names and span completion h…
jinseopkim0 Oct 5, 2026
b542ff1
fix(gax): avoid potential NPE when resolving attempt span name
jinseopkim0 Oct 5, 2026
23b22fc
fix(tracing): record CancellationException on lingering attempt when …
jinseopkim0 Oct 5, 2026
018b3d8
style(tracing): use simple names Map and HashMap in OpenTelemetryTrac…
jinseopkim0 Oct 5, 2026
4cab2e1
style(tracing): use simple name StatusCode in OpenTelemetryTracingTra…
jinseopkim0 Oct 5, 2026
8274051
fix(tracing): end superseded attempt span without error status
jinseopkim0 Oct 5, 2026
1e66751
style(tracing): use simple names Context and W3CTraceContextPropagato…
jinseopkim0 Oct 5, 2026
33b1d23
style(tracing): static import OpenTelemetry StatusCode.ERROR to avoid…
jinseopkim0 Oct 5, 2026
412f398
fix(tracing): restore canonical attempt span name and suppress MustBe…
jinseopkim0 Oct 5, 2026
4b851ad
docs(tracing): add Javadoc and explanatory comments for T3 operation …
jinseopkim0 Oct 5, 2026
ff8e448
feat(gax): verify T3/T4 span hierarchy and retry aggregation
jinseopkim0 Oct 5, 2026
22ca25d
test(tracing): explicitly verify exactly one internal operation span …
jinseopkim0 Oct 5, 2026
f27fdb3
docs(tracing): add explanatory comments to T3/T4 hierarchy and retry …
jinseopkim0 Oct 5, 2026
8de17a0
refactor(tracing): extract verifySpanHierarchyAndGetOperationSpan tes…
jinseopkim0 Oct 5, 2026
8eaceaf
feat(gax): correlate M3 client duration metrics with T3 span exemplars
jinseopkim0 Oct 5, 2026
8acab46
fix(tracing): avoid self-suppression in CompositeTracer lifecycle not…
jinseopkim0 Oct 5, 2026
92ae605
fix(tracing): avoid mutating caller exception in CompositeTracer oper…
jinseopkim0 Oct 5, 2026
f745292
refactor(tracing): extract runInScope helper in CompositeTracer
jinseopkim0 Oct 5, 2026
ec8f900
refactor(tracing): use NO_OP_SCOPE and enterScope in CompositeTracer
jinseopkim0 Oct 5, 2026
d772112
feat(tracing): scope attempt lifecycle callbacks in CompositeTracer
jinseopkim0 Oct 5, 2026
c9dacd5
style(tracing): declare NO_OP_SCOPE at top of CompositeTracer
jinseopkim0 Oct 5, 2026
9bbfc02
perf(tracing): optimize inScope fast path for 0 and 1 child tracers i…
jinseopkim0 Oct 5, 2026
4d2d592
perf(tracing): short-circuit enterScope when children is empty in Com…
jinseopkim0 Oct 5, 2026
c8aa931
fix(tracing): ensure exception-safety across all throwables in inScope
jinseopkim0 Oct 5, 2026
c807e00
perf(tracing): avoid lambda allocation when childScopes is empty or s…
jinseopkim0 Oct 5, 2026
ea41ebd
fix(tracing): catch RuntimeException | Error in enterScope
jinseopkim0 Oct 5, 2026
dd2c8f7
fix(tracing): preserve Error type without wrapping in RuntimeExceptio…
jinseopkim0 Oct 5, 2026
779a1ae
fix(tracing): catch Throwable in close loop and restrict enterScope t…
jinseopkim0 Oct 5, 2026
cac5640
fix(tracing): catch RuntimeException | Error in childScopes close and…
jinseopkim0 Oct 5, 2026
c405581
refactor(tracing): extract CompositeScope class in CompositeTracer
jinseopkim0 Oct 5, 2026
64e5795
fix(tracing): catch RuntimeException | Error in enterScope to prevent…
jinseopkim0 Oct 5, 2026
7a0a671
fix(tracing): throw throwException in CompositeTracer and catch Throw…
jinseopkim0 Oct 5, 2026
f1647d2
refactor(tracing): simplify NO_OP_SCOPE lambda and catch RuntimeExcep…
jinseopkim0 Oct 5, 2026
5e1d118
refactor(tracing): return RuntimeException from throwException in Com…
jinseopkim0 Oct 5, 2026
e835565
refactor(tracing): optimize CompositeTracer scope allocation and exce…
jinseopkim0 Oct 5, 2026
db7203f
fix(tracing): guard against self-suppression in CompositeTracer
jinseopkim0 Oct 5, 2026
0c20afd
fix(tracing): catch RuntimeException in CompositeTracer enterScope
jinseopkim0 Oct 5, 2026
5a3fb6b
docs(tracing): add Javadoc and explanatory comments to CompositeTrace…
jinseopkim0 Oct 5, 2026
55cee78
fix(tracing): make CompositeScope.close idempotent and clear references
jinseopkim0 Oct 5, 2026
92a9458
test(showcase): add GAPIC Showcase integration tests for OpenTelemetr…
jinseopkim0 Oct 5, 2026
ad7ca9c
style(showcase): use simple name StatusCode in ITOtelT3Tracing
jinseopkim0 Oct 5, 2026
cc1553b
chore(showcase): use Awaitility untilAsserted and avoid FQCN in showc…
jinseopkim0 Oct 5, 2026
7d9cebb
chore(showcase): address review comments on try-with-resources, durat…
jinseopkim0 Oct 5, 2026
2fb3112
chore(showcase): avoid global OpenTelemetrySdk registration and incre…
jinseopkim0 Oct 5, 2026
ca95b10
test(showcase): use java.time.Duration in ITOtelT3Tracing
jinseopkim0 Oct 5, 2026
96dc083
test(showcase): update timeout comment in ITOtelT3Tracing
jinseopkim0 Oct 5, 2026
8d4643e
test(showcase): address review comments on exemplar types, timeout as…
jinseopkim0 Oct 5, 2026
cd8642a
test(showcase): assert exact exemplar points size in ITOtelT3MetricsE…
jinseopkim0 Oct 5, 2026
37288a8
test(showcase): remove redundant certificate validation and use exact…
jinseopkim0 Oct 5, 2026
1253bdf
docs(showcase): add Javadoc and explanatory comments to T3 integratio…
jinseopkim0 Oct 5, 2026
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
@@ -0,0 +1,289 @@
/*
* 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.showcase.v1beta1.it;

import static com.google.common.truth.Truth.assertThat;

import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.gax.core.NoCredentialsProvider;
import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.api.gax.tracing.CompositeTracerFactory;
import com.google.api.gax.tracing.OpenTelemetryMetricsFactory;
import com.google.api.gax.tracing.OpenTelemetryTracingFactory;
import com.google.showcase.v1beta1.EchoClient;
import com.google.showcase.v1beta1.EchoRequest;
import com.google.showcase.v1beta1.EchoSettings;
import com.google.showcase.v1beta1.stub.EchoStub;
import com.google.showcase.v1beta1.stub.EchoStubSettings;
import io.grpc.ManagedChannelBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.ExemplarFilter;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.data.ExemplarData;
import io.opentelemetry.sdk.metrics.data.HistogramPointData;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/** Integration tests for Feature 3 (F3.1–F3.2): T3 Tracing and M3 Metrics Exemplar Correlation. */
class ITOtelT3MetricsExemplar {
private static final String SHOWCASE_SERVER_ADDRESS = "localhost";
private static final long SHOWCASE_SERVER_PORT = 7469;
private static final String SHOWCASE_GRPC_ENDPOINT =
String.format("%s:%s", SHOWCASE_SERVER_ADDRESS, SHOWCASE_SERVER_PORT);
private static final String SHOWCASE_HTTPJSON_ENDPOINT =
String.format("http://%s:%s", SHOWCASE_SERVER_ADDRESS, SHOWCASE_SERVER_PORT);
private static final String SHOWCASE_SERVICE_NAME = "showcase";

private InMemorySpanExporter spanExporter;
private InMemoryMetricReader metricReader;
private OpenTelemetrySdk openTelemetrySdk;

@BeforeEach
void setUp() {
spanExporter = InMemorySpanExporter.create();
metricReader = InMemoryMetricReader.create();

SdkTracerProvider tracerProvider =
SdkTracerProvider.builder()
.addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
.build();

SdkMeterProvider meterProvider =
SdkMeterProvider.builder()
.registerMetricReader(metricReader)
.setExemplarFilter(ExemplarFilter.traceBased())
.build();

openTelemetrySdk =
OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setMeterProvider(meterProvider)
.build();
}

@AfterEach
void tearDown() {
if (openTelemetrySdk != null) {
openTelemetrySdk.close();
}
}

// F3.1: HTTP M3 metric records T3 span as exemplar
@Test
void testHttpJson_m3ExemplarMatchesT3Span() throws Exception {
// Verifies that for HTTP/JSON, client request duration metrics attach exemplars
// pointing directly to the overall T3 operation span (traceId and spanId match).
ApiTracerFactory compositeTracerFactory = createCompositeTracerFactory();
EchoSettings settings = createEchoSettings(true);
EchoStub stub = createStubWithServiceName(settings, compositeTracerFactory);

try (EchoClient client = EchoClient.create(stub)) {
client.echo(EchoRequest.newBuilder().setContent("exemplar-test-http").build());

List<SpanData> spans = waitAndCollectSpans(2);
SpanData t3Span =
spans.stream()
.filter(s -> s.getKind() == SpanKind.INTERNAL)
.findFirst()
.orElseThrow(() -> new AssertionError("T3 INTERNAL span not found in: " + spans));

Awaitility.await()
.atMost(Duration.ofSeconds(5))
.untilAsserted(
() -> {
Collection<MetricData> metrics = metricReader.collectAllMetrics();
MetricData durationMetric =
metrics.stream()
.filter(m -> m.getName().equals("gcp.client.request.duration"))
.findFirst()
.orElseThrow(
() -> new AssertionError("Duration metric not found in: " + metrics));

Collection<HistogramPointData> points =
durationMetric.getHistogramData().getPoints();
assertThat(points).hasSize(1);
HistogramPointData point = points.iterator().next();
List<? extends ExemplarData> exemplars = point.getExemplars();
assertThat(exemplars).isNotEmpty();

ExemplarData exemplar = exemplars.get(0);
assertThat(exemplar.getSpanContext().getTraceId()).isEqualTo(t3Span.getTraceId());
assertThat(exemplar.getSpanContext().getSpanId()).isEqualTo(t3Span.getSpanId());
});
}
}

// F3.2: gRPC M3 metric records T3 span as exemplar
@Test
void testGrpc_m3ExemplarMatchesT3Span() throws Exception {
// Verifies that for gRPC, client request duration metrics attach exemplars
// pointing directly to the overall T3 operation span (traceId and spanId match).
ApiTracerFactory compositeTracerFactory = createCompositeTracerFactory();
EchoSettings settings = createEchoSettings(false);
EchoStub stub = createStubWithServiceName(settings, compositeTracerFactory);

try (EchoClient client = EchoClient.create(stub)) {
client.echo(EchoRequest.newBuilder().setContent("exemplar-test-grpc").build());

List<SpanData> spans = waitAndCollectSpans(2);
SpanData t3Span =
spans.stream()
.filter(s -> s.getKind() == SpanKind.INTERNAL)
.findFirst()
.orElseThrow(() -> new AssertionError("T3 INTERNAL span not found in: " + spans));

Awaitility.await()
.atMost(Duration.ofSeconds(5))
.untilAsserted(
() -> {
Collection<MetricData> metrics = metricReader.collectAllMetrics();
MetricData durationMetric =
metrics.stream()
.filter(m -> m.getName().equals("gcp.client.request.duration"))
.findFirst()
.orElseThrow(
() -> new AssertionError("Duration metric not found in: " + metrics));

Collection<HistogramPointData> points =
durationMetric.getHistogramData().getPoints();
assertThat(points).hasSize(1);
HistogramPointData point = points.iterator().next();
List<? extends ExemplarData> exemplars = point.getExemplars();
assertThat(exemplars).isNotEmpty();

ExemplarData exemplar = exemplars.get(0);
assertThat(exemplar.getSpanContext().getTraceId()).isEqualTo(t3Span.getTraceId());
assertThat(exemplar.getSpanContext().getSpanId()).isEqualTo(t3Span.getSpanId());
});
}
}

/**
* Creates a composite tracer factory combining both OpenTelemetry tracing and metrics factories.
*
* @return the configured {@link CompositeTracerFactory}
*/
private CompositeTracerFactory createCompositeTracerFactory() {
OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
OpenTelemetryMetricsFactory metricsFactory = new OpenTelemetryMetricsFactory(openTelemetrySdk);
return new CompositeTracerFactory(Arrays.asList(tracingFactory, metricsFactory));
}

/**
* Waits until the in-memory span exporter records at least {@code minSpans} completed spans.
*
* @param minSpans the minimum number of spans expected
* @return the list of completed {@link SpanData} items
*/
private List<SpanData> waitAndCollectSpans(int minSpans) {
Awaitility.await()
.atMost(Duration.ofSeconds(5))
.until(() -> spanExporter.getFinishedSpanItems().size() >= minSpans);
return spanExporter.getFinishedSpanItems();
}

/**
* Constructs {@link EchoSettings} configured for the local Showcase test server.
*
* @param isHttpJson {@code true} for HTTP/JSON transport; {@code false} for gRPC transport
* @return the configured {@link EchoSettings}
* @throws Exception if transport provider initialization fails
*/
private EchoSettings createEchoSettings(boolean isHttpJson) throws Exception {
if (isHttpJson) {
return EchoSettings.newHttpJsonBuilder()
.setCredentialsProvider(NoCredentialsProvider.create())
.setTransportChannelProvider(
EchoSettings.defaultHttpJsonTransportProviderBuilder()
.setHttpTransport(new NetHttpTransport.Builder().build())
.build())
.setEndpoint(SHOWCASE_HTTPJSON_ENDPOINT)
.build();
} else {
return EchoSettings.newBuilder()
.setCredentialsProvider(NoCredentialsProvider.create())
.setTransportChannelProvider(
EchoSettings.defaultGrpcTransportProviderBuilder()
.setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
.build())
.setEndpoint(SHOWCASE_GRPC_ENDPOINT)
.build();
}
}

/**
* Instantiates an {@link EchoStub} with custom service name and tracer factory.
*
* @param settings the client settings to base the stub on
* @param tracerFactory the tracer factory to register with the stub
* @return the initialized {@link EchoStub}
* @throws IOException if stub creation fails
*/
private EchoStub createStubWithServiceName(EchoSettings settings, ApiTracerFactory tracerFactory)
throws IOException {
EchoStubSettings.Builder builder =
(EchoStubSettings.Builder) settings.getStubSettings().toBuilder();
builder.setTracerFactory(tracerFactory);
return new ExtendedEchoStubSettings(builder).createStub();
}

/** Extended {@link EchoStubSettings} that overrides {@link #getServiceName()} for testing. */
private static class ExtendedEchoStubSettings extends EchoStubSettings {
/**
* Constructs settings wrapping the specified builder.
*
* @param builder the settings builder
* @throws IOException if base settings construction fails
*/
protected ExtendedEchoStubSettings(EchoStubSettings.Builder builder) throws IOException {
super(builder);
}

@Override
public String getServiceName() {
return SHOWCASE_SERVICE_NAME;
}
}
}
Loading
Loading