Skip to content

Commit bf1a07a

Browse files
authored
feat: Integration test for End to End tracing (#3691)
Add End to End tracing in Integration Test to verify Spanner side trace span generation
1 parent dda2e1d commit bf1a07a

File tree

3 files changed

+241
-1
lines changed

3 files changed

+241
-1
lines changed

google-cloud-spanner/pom.xml

+18
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,24 @@
455455
<artifactId>opentelemetry-sdk-testing</artifactId>
456456
<scope>test</scope>
457457
</dependency>
458+
<dependency>
459+
<groupId>com.google.cloud.opentelemetry</groupId>
460+
<artifactId>exporter-trace</artifactId>
461+
<version>0.33.0</version>
462+
<scope>test</scope>
463+
</dependency>
464+
<dependency>
465+
<groupId>com.google.cloud</groupId>
466+
<artifactId>google-cloud-trace</artifactId>
467+
<version>2.51.0</version>
468+
<scope>test</scope>
469+
</dependency>
470+
<dependency>
471+
<groupId>com.google.api.grpc</groupId>
472+
<artifactId>proto-google-cloud-trace-v1</artifactId>
473+
<version>2.51.0</version>
474+
<scope>test</scope>
475+
</dependency>
458476
</dependencies>
459477
<profiles>
460478
<profile>

google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java

+65-1
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,27 @@
2222
import com.google.api.client.util.ExponentialBackOff;
2323
import com.google.api.gax.longrunning.OperationFuture;
2424
import com.google.cloud.Timestamp;
25+
import com.google.cloud.opentelemetry.trace.TraceConfiguration;
26+
import com.google.cloud.opentelemetry.trace.TraceExporter;
2527
import com.google.cloud.spanner.DatabaseInfo.DatabaseField;
2628
import com.google.cloud.spanner.testing.EmulatorSpannerHelper;
2729
import com.google.cloud.spanner.testing.RemoteSpannerHelper;
2830
import com.google.common.collect.Iterators;
2931
import com.google.spanner.admin.instance.v1.CreateInstanceMetadata;
32+
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
33+
import io.opentelemetry.context.propagation.ContextPropagators;
34+
import io.opentelemetry.sdk.OpenTelemetrySdk;
35+
import io.opentelemetry.sdk.resources.Resource;
36+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
37+
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
38+
import io.opentelemetry.sdk.trace.export.SpanExporter;
39+
import io.opentelemetry.sdk.trace.samplers.Sampler;
40+
import java.util.Collection;
41+
import java.util.Collections;
3042
import java.util.Objects;
3143
import java.util.Random;
3244
import java.util.concurrent.ExecutionException;
45+
import java.util.concurrent.ThreadLocalRandom;
3346
import java.util.concurrent.TimeUnit;
3447
import java.util.logging.Level;
3548
import java.util.logging.Logger;
@@ -67,10 +80,22 @@ public class IntegrationTestEnv extends ExternalResource {
6780
private final boolean alwaysCreateNewInstance;
6881
private RemoteSpannerHelper testHelper;
6982

83+
private Collection<TestEnvOptions> testEnvOptions = Collections.emptyList();
84+
85+
public enum TestEnvOptions {
86+
USE_END_TO_END_TRACING;
87+
// TODO : Move alwaysCreateNewInstance to TestEnvOptions
88+
}
89+
7090
public IntegrationTestEnv() {
7191
this(false);
7292
}
7393

94+
public IntegrationTestEnv(Collection<TestEnvOptions> testEnvOptions) {
95+
this(false);
96+
this.testEnvOptions = testEnvOptions;
97+
}
98+
7499
public IntegrationTestEnv(final boolean alwaysCreateNewInstance) {
75100
this.alwaysCreateNewInstance = alwaysCreateNewInstance;
76101
}
@@ -107,8 +132,15 @@ protected void before() throws Throwable {
107132
assumeFalse(alwaysCreateNewInstance && isCloudDevel());
108133

109134
this.config.setUp();
110-
111135
SpannerOptions options = config.spannerOptions();
136+
if (testEnvOptions.stream()
137+
.anyMatch(testEnvOption -> TestEnvOptions.USE_END_TO_END_TRACING.equals(testEnvOption))) {
138+
// OpenTelemetry set up for enabling End to End tracing for all integration test env.
139+
// The gRPC stub and connections are created during test env set up using SpannerOptions and
140+
// are
141+
// reused for executing statements.
142+
options = spannerOptionsWithEndToEndTracing(options);
143+
}
112144
String instanceProperty = System.getProperty(TEST_INSTANCE_PROPERTY, "");
113145
InstanceId instanceId;
114146
if (!instanceProperty.isEmpty() && !alwaysCreateNewInstance) {
@@ -133,6 +165,38 @@ protected void before() throws Throwable {
133165
}
134166
}
135167

168+
public SpannerOptions spannerOptionsWithEndToEndTracing(SpannerOptions options) {
169+
assumeFalse("This test requires credentials", EmulatorSpannerHelper.isUsingEmulator());
170+
171+
TraceConfiguration.Builder traceConfigurationBuilder = TraceConfiguration.builder();
172+
if (options.getCredentials() != null) {
173+
traceConfigurationBuilder.setCredentials(options.getCredentials());
174+
}
175+
SpanExporter traceExporter =
176+
TraceExporter.createWithConfiguration(
177+
traceConfigurationBuilder.setProjectId(options.getProjectId()).build());
178+
179+
String serviceName = "java-spanner-integration-tests-" + ThreadLocalRandom.current().nextInt();
180+
SdkTracerProvider sdkTracerProvider =
181+
SdkTracerProvider.builder()
182+
// Always sample in this test to ensure we know what we get.
183+
.setSampler(Sampler.alwaysOn())
184+
.setResource(Resource.builder().put("service.name", serviceName).build())
185+
.addSpanProcessor(BatchSpanProcessor.builder(traceExporter).build())
186+
.build();
187+
OpenTelemetrySdk openTelemetry =
188+
OpenTelemetrySdk.builder()
189+
.setTracerProvider(sdkTracerProvider)
190+
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
191+
.build();
192+
SpannerOptions.enableOpenTelemetryTraces();
193+
return options
194+
.toBuilder()
195+
.setOpenTelemetry(openTelemetry)
196+
.setEnableEndToEndTracing(true)
197+
.build();
198+
}
199+
136200
RemoteSpannerHelper createTestHelper(SpannerOptions options, InstanceId instanceId)
137201
throws Throwable {
138202
return RemoteSpannerHelper.create(options, instanceId);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.it;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.Assert.assertTrue;
21+
import static org.junit.Assume.assumeTrue;
22+
23+
import com.google.api.gax.core.FixedCredentialsProvider;
24+
import com.google.api.gax.rpc.ApiException;
25+
import com.google.api.gax.rpc.ResourceExhaustedException;
26+
import com.google.api.gax.rpc.StatusCode;
27+
import com.google.cloud.spanner.Database;
28+
import com.google.cloud.spanner.DatabaseClient;
29+
import com.google.cloud.spanner.IntegrationTestEnv;
30+
import com.google.cloud.spanner.IntegrationTestEnv.TestEnvOptions;
31+
import com.google.cloud.spanner.ParallelIntegrationTest;
32+
import com.google.cloud.spanner.ResultSet;
33+
import com.google.cloud.spanner.SpannerOptions;
34+
import com.google.cloud.spanner.SpannerOptionsHelper;
35+
import com.google.cloud.spanner.Statement;
36+
import com.google.cloud.spanner.Struct;
37+
import com.google.cloud.spanner.Type;
38+
import com.google.cloud.spanner.Type.StructField;
39+
import com.google.cloud.spanner.connection.ConnectionOptions;
40+
import com.google.cloud.trace.v1.TraceServiceClient;
41+
import com.google.cloud.trace.v1.TraceServiceSettings;
42+
import com.google.common.base.Stopwatch;
43+
import io.opentelemetry.api.trace.Span;
44+
import io.opentelemetry.api.trace.Tracer;
45+
import io.opentelemetry.context.Scope;
46+
import java.io.IOException;
47+
import java.util.Arrays;
48+
import java.util.Collection;
49+
import java.util.concurrent.TimeUnit;
50+
import org.junit.AfterClass;
51+
import org.junit.BeforeClass;
52+
import org.junit.ClassRule;
53+
import org.junit.Test;
54+
import org.junit.experimental.categories.Category;
55+
import org.junit.runner.RunWith;
56+
import org.junit.runners.JUnit4;
57+
58+
/** Integration tests for End to End Tracing. */
59+
@Category(ParallelIntegrationTest.class)
60+
@RunWith(JUnit4.class)
61+
public class ITEndToEndTracingTest {
62+
public static Collection<TestEnvOptions> testEnvOptions =
63+
Arrays.asList(TestEnvOptions.USE_END_TO_END_TRACING);
64+
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(testEnvOptions);
65+
private static DatabaseClient googleStandardSQLClient;
66+
67+
static {
68+
SpannerOptionsHelper.resetActiveTracingFramework();
69+
SpannerOptions.enableOpenTelemetryTraces();
70+
}
71+
72+
private static String selectValueQuery = "SELECT @p1 + @p1";
73+
74+
@BeforeClass
75+
public static void setUp() {
76+
setUpDatabase();
77+
}
78+
79+
public static void setUpDatabase() {
80+
// Empty database.
81+
Database googleStandardSQLDatabase = env.getTestHelper().createTestDatabase();
82+
googleStandardSQLClient = env.getTestHelper().getDatabaseClient(googleStandardSQLDatabase);
83+
}
84+
85+
@AfterClass
86+
public static void teardown() {
87+
ConnectionOptions.closeSpanner();
88+
}
89+
90+
private void assertTrace(String traceId) throws IOException, InterruptedException {
91+
TraceServiceSettings settings =
92+
env.getTestHelper().getOptions().getCredentials() == null
93+
? TraceServiceSettings.newBuilder().build()
94+
: TraceServiceSettings.newBuilder()
95+
.setCredentialsProvider(
96+
FixedCredentialsProvider.create(
97+
env.getTestHelper().getOptions().getCredentials()))
98+
.build();
99+
try (TraceServiceClient client = TraceServiceClient.create(settings)) {
100+
boolean foundTrace = false;
101+
Stopwatch metricsPollingStopwatch = Stopwatch.createStarted();
102+
while (!foundTrace && metricsPollingStopwatch.elapsed(TimeUnit.SECONDS) < 30) {
103+
// Try every 5 seconds
104+
Thread.sleep(5000);
105+
try {
106+
foundTrace =
107+
client.getTrace(env.getTestHelper().getInstanceId().getProject(), traceId)
108+
.getSpansList().stream()
109+
.anyMatch(span -> "Spanner.ExecuteStreamingSql".equals(span.getName()));
110+
} catch (ApiException apiException) {
111+
assumeTrue(
112+
apiException.getStatusCode() != null
113+
&& StatusCode.Code.NOT_FOUND.equals(apiException.getStatusCode().getCode()));
114+
System.out.println("Trace NOT_FOUND error ignored");
115+
}
116+
}
117+
assertTrue(foundTrace);
118+
} catch (ResourceExhaustedException resourceExhaustedException) {
119+
if (resourceExhaustedException
120+
.getMessage()
121+
.contains("Quota exceeded for quota metric 'Read requests (free)'")) {
122+
// Ignore and allow the test to succeed.
123+
System.out.println("RESOURCE_EXHAUSTED error ignored");
124+
} else {
125+
throw resourceExhaustedException;
126+
}
127+
}
128+
}
129+
130+
private Struct executeWithRowResultType(Statement statement, Type expectedRowType) {
131+
ResultSet resultSet = statement.executeQuery(googleStandardSQLClient.singleUse());
132+
assertThat(resultSet.next()).isTrue();
133+
assertThat(resultSet.getType()).isEqualTo(expectedRowType);
134+
Struct row = resultSet.getCurrentRowAsStruct();
135+
assertThat(resultSet.next()).isFalse();
136+
return row;
137+
}
138+
139+
@Test
140+
public void simpleSelect() throws IOException, InterruptedException {
141+
Tracer tracer =
142+
env.getTestHelper()
143+
.getOptions()
144+
.getOpenTelemetry()
145+
.getTracer(ITEndToEndTracingTest.class.getName());
146+
Span span = tracer.spanBuilder("simpleSelect").startSpan();
147+
Scope scope = span.makeCurrent();
148+
Type rowType = Type.struct(StructField.of("", Type.int64()));
149+
Struct row =
150+
executeWithRowResultType(
151+
Statement.newBuilder(selectValueQuery).bind("p1").to(1234).build(), rowType);
152+
assertThat(row.isNull(0)).isFalse();
153+
assertThat(row.getLong(0)).isEqualTo(2468);
154+
scope.close();
155+
span.end();
156+
assertTrace(span.getSpanContext().getTraceId());
157+
}
158+
}

0 commit comments

Comments
 (0)