Skip to content

Commit fadc3ec

Browse files
feat(spanner): provide opt-in auto-tagging feature in client library
Spanner transaction and lock debugging heavily relies on transaction and request tags. This introduces an opt-in mechanism via `enableAutoTagTransactions()` in `SpannerOptions` to automatically discover and append tags from calling runtime stack frames when explicit tags are absent. Additionally, a dedicated emergency override environment variable `SPANNER_DISABLE_AUTO_TAGGING` is provided to allow disabling auto-tagging globally.
1 parent ca594e4 commit fadc3ec

6 files changed

Lines changed: 285 additions & 2 deletions

File tree

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@ private void initTransactionInternal(BeginTransactionRequest request) {
644644
private final DirectedReadOptions defaultDirectedReadOptions;
645645
private final DecodeMode defaultDecodeMode;
646646
private final Clock clock;
647+
private volatile String cachedRequestTag;
647648

648649
@GuardedBy("lock")
649650
private boolean isValid = true;
@@ -831,6 +832,14 @@ QueryOptions buildQueryOptions(QueryOptions requestOptions) {
831832

832833
RequestOptions buildRequestOptions(Options options) {
833834
RequestOptions.Builder builder = options.toRequestOptionsProto(false).toBuilder();
835+
if (builder.getRequestTag().isEmpty()) {
836+
if (this.cachedRequestTag == null) {
837+
this.cachedRequestTag = AutoTagHelper.getAutoTag(session.getSpanner().getOptions());
838+
}
839+
if (this.cachedRequestTag != null) {
840+
builder.setRequestTag(this.cachedRequestTag);
841+
}
842+
}
834843
RequestOptions.ClientContext defaultClientContext =
835844
session.getSpanner().getOptions().getClientContext();
836845
if (defaultClientContext != null) {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2026 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;
18+
19+
import java.util.List;
20+
21+
class AutoTagHelper {
22+
23+
private static final String[] INTERNAL_PACKAGES = {
24+
"java.",
25+
"javax.",
26+
"jdk.",
27+
"sun.",
28+
"io.grpc.",
29+
"com.google.cloud.spanner.",
30+
"com.google.api."
31+
};
32+
33+
static String getAutoTag(SpannerOptions options) {
34+
if (!options.isAutoTagTransactionsEnabled()) {
35+
return null;
36+
}
37+
StackTraceElement[] stackTrace = new Throwable().getStackTrace();
38+
int limit = Math.min(stackTrace.length, options.getAutoTagTransactionsTracerLimit());
39+
List<String> targetPackages = options.getAutoTagTransactionsPackages();
40+
boolean hasTargetPackages = targetPackages != null && !targetPackages.isEmpty();
41+
42+
for (int i = 0; i < limit; i++) {
43+
StackTraceElement element = stackTrace[i];
44+
String className = element.getClassName();
45+
if (hasTargetPackages) {
46+
for (String targetPackage : targetPackages) {
47+
if (className.startsWith(targetPackage)) {
48+
return formatTag(className, element.getMethodName());
49+
}
50+
}
51+
} else {
52+
boolean isInternal = false;
53+
for (String internalPackage : INTERNAL_PACKAGES) {
54+
if (className.startsWith(internalPackage)) {
55+
isInternal = true;
56+
break;
57+
}
58+
}
59+
if (isInternal) {
60+
continue;
61+
}
62+
return formatTag(className, element.getMethodName());
63+
}
64+
}
65+
return null;
66+
}
67+
68+
private static String formatTag(String className, String methodName) {
69+
int lastDot = className.lastIndexOf('.');
70+
String simpleClassName = lastDot == -1 ? className : className.substring(lastDot + 1);
71+
String tag = simpleClassName + "." + methodName;
72+
if (tag.length() > 50) {
73+
tag = tag.substring(0, 50);
74+
}
75+
return tag;
76+
}
77+
}

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,19 @@ public CommitResponse writeAtLeastOnceWithOptions(
315315

316316
private RequestOptions getRequestOptions(TransactionOption... transactionOptions) {
317317
Options requestOptions = Options.fromTransactionOptions(transactionOptions);
318-
if (requestOptions.hasPriority() || requestOptions.hasTag()) {
318+
String autoTag = null;
319+
if (!requestOptions.hasTag()) {
320+
autoTag = AutoTagHelper.getAutoTag(spanner.getOptions());
321+
}
322+
if (requestOptions.hasPriority() || requestOptions.hasTag() || autoTag != null) {
319323
RequestOptions.Builder requestOptionsBuilder = RequestOptions.newBuilder();
320324
if (requestOptions.hasPriority()) {
321325
requestOptionsBuilder.setPriority(requestOptions.priority());
322326
}
323327
if (requestOptions.hasTag()) {
324328
requestOptionsBuilder.setTransactionTag(requestOptions.tag());
329+
} else if (autoTag != null) {
330+
requestOptionsBuilder.setTransactionTag(autoTag);
325331
}
326332
return requestOptionsBuilder.build();
327333
}
@@ -501,6 +507,12 @@ ApiFuture<Transaction> beginTransactionAsync(
501507
}
502508
RequestOptions.Builder optionsBuilder =
503509
transactionOptions.toRequestOptionsProto(true).toBuilder();
510+
if (!transactionOptions.hasTag()) {
511+
String autoTag = AutoTagHelper.getAutoTag(spanner.getOptions());
512+
if (autoTag != null) {
513+
optionsBuilder.setTransactionTag(autoTag);
514+
}
515+
}
504516
RequestOptions.ClientContext defaultClientContext = spanner.getOptions().getClientContext();
505517
if (defaultClientContext != null) {
506518
RequestOptions.ClientContext.Builder builder = defaultClientContext.toBuilder();

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
import java.time.Duration;
9797
import java.util.ArrayList;
9898
import java.util.Base64;
99+
import java.util.Collections;
99100
import java.util.HashMap;
100101
import java.util.List;
101102
import java.util.Map;
@@ -308,6 +309,9 @@ static GcpChannelPoolOptions mergeWithDefaultChannelPoolOptions(
308309
private final String monitoringHost;
309310
private final TransactionOptions defaultTransactionOptions;
310311
private final RequestOptions.ClientContext clientContext;
312+
private final boolean autoTagTransactionsEnabled;
313+
private final List<String> autoTagTransactionsPackages;
314+
private final int autoTagTransactionsTracerLimit;
311315

312316
enum TracingFramework {
313317
OPEN_CENSUS,
@@ -993,6 +997,9 @@ protected SpannerOptions(Builder builder) {
993997
monitoringHost = builder.monitoringHost;
994998
defaultTransactionOptions = builder.defaultTransactionOptions;
995999
clientContext = builder.clientContext;
1000+
autoTagTransactionsEnabled = builder.autoTagTransactionsEnabled;
1001+
autoTagTransactionsPackages = builder.autoTagTransactionsPackages;
1002+
autoTagTransactionsTracerLimit = builder.autoTagTransactionsTracerLimit;
9961003
}
9971004

9981005
private String getResolvedUniverseDomain() {
@@ -1064,6 +1071,10 @@ default boolean isEnableLocationApi() {
10641071
return false;
10651072
}
10661073

1074+
default boolean isAutoTagTransactionsDisabled() {
1075+
return false;
1076+
}
1077+
10671078
@Deprecated
10681079
@ObsoleteApi(
10691080
"This will be removed in an upcoming version without a major version bump. You should use"
@@ -1168,6 +1179,12 @@ public boolean isEnableLocationApi() {
11681179
return Boolean.parseBoolean(System.getenv(EXPERIMENTAL_LOCATION_API_ENV_VAR));
11691180
}
11701181

1182+
@Override
1183+
public boolean isAutoTagTransactionsDisabled() {
1184+
return Boolean.parseBoolean(System.getenv("SPANNER_DISABLE_AUTO_TAGGING"))
1185+
|| Boolean.parseBoolean(System.getProperty("spanner.disable_auto_tagging"));
1186+
}
1187+
11711188
@Override
11721189
public String getMonitoringHost() {
11731190
return System.getenv(SPANNER_MONITORING_HOST);
@@ -1256,6 +1273,9 @@ public static class Builder
12561273
private boolean usePlainText = false;
12571274
private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance();
12581275
private RequestOptions.ClientContext clientContext;
1276+
private boolean autoTagTransactionsEnabled = false;
1277+
private List<String> autoTagTransactionsPackages = Collections.emptyList();
1278+
private int autoTagTransactionsTracerLimit = 50;
12591279

12601280
private static String createCustomClientLibToken(String token) {
12611281
return token + " " + ServiceOptions.getGoogApiClientLibName();
@@ -1362,6 +1382,9 @@ protected Builder() {
13621382
this.monitoringHost = options.monitoringHost;
13631383
this.defaultTransactionOptions = options.defaultTransactionOptions;
13641384
this.clientContext = options.clientContext;
1385+
this.autoTagTransactionsEnabled = options.autoTagTransactionsEnabled;
1386+
this.autoTagTransactionsPackages = options.autoTagTransactionsPackages;
1387+
this.autoTagTransactionsTracerLimit = options.autoTagTransactionsTracerLimit;
13651388
}
13661389

13671390
@Override
@@ -2120,6 +2143,33 @@ public Builder setDefaultClientContext(RequestOptions.ClientContext clientContex
21202143
return this;
21212144
}
21222145

2146+
public Builder enableAutoTagTransactions() {
2147+
this.autoTagTransactionsEnabled = true;
2148+
return this;
2149+
}
2150+
2151+
public Builder disableAutoTagTransactions() {
2152+
this.autoTagTransactionsEnabled = false;
2153+
return this;
2154+
}
2155+
2156+
public Builder setAutoTagTransactionsPackage(String autoTagTransactionsPackage) {
2157+
this.autoTagTransactionsPackages = Collections.singletonList(autoTagTransactionsPackage);
2158+
return this;
2159+
}
2160+
2161+
public Builder setAutoTagTransactionsPackages(List<String> autoTagTransactionsPackages) {
2162+
this.autoTagTransactionsPackages =
2163+
Collections.unmodifiableList(
2164+
new ArrayList<>(Preconditions.checkNotNull(autoTagTransactionsPackages)));
2165+
return this;
2166+
}
2167+
2168+
public Builder setAutoTagTransactionsTracerLimit(int autoTagTransactionsTracerLimit) {
2169+
this.autoTagTransactionsTracerLimit = autoTagTransactionsTracerLimit;
2170+
return this;
2171+
}
2172+
21232173
@SuppressWarnings("rawtypes")
21242174
@Override
21252175
public SpannerOptions build() {
@@ -2547,6 +2597,22 @@ public TransactionOptions getDefaultTransactionOptions() {
25472597
return defaultTransactionOptions;
25482598
}
25492599

2600+
public boolean isAutoTagTransactionsEnabled() {
2601+
return autoTagTransactionsEnabled && !environment.isAutoTagTransactionsDisabled();
2602+
}
2603+
2604+
public List<String> getAutoTagTransactionsPackages() {
2605+
return autoTagTransactionsPackages;
2606+
}
2607+
2608+
public int getAutoTagTransactionsTracerLimit() {
2609+
return autoTagTransactionsTracerLimit;
2610+
}
2611+
2612+
public boolean isAutoTagTransactionsDisabled() {
2613+
return environment.isAutoTagTransactionsDisabled();
2614+
}
2615+
25502616
@BetaApi
25512617
public boolean isUseVirtualThreads() {
25522618
return useVirtualThreads;

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ public void removeListener(Runnable listener) {
198198
private boolean aborted;
199199

200200
private final Options options;
201+
private volatile String cachedTransactionTag;
201202

202203
/** Default to -1 to indicate not available. */
203204
@GuardedBy("lock")
@@ -780,7 +781,10 @@ String getTransactionTag() {
780781
if (this.options.hasTag()) {
781782
return this.options.tag();
782783
}
783-
return null;
784+
if (this.cachedTransactionTag == null) {
785+
this.cachedTransactionTag = AutoTagHelper.getAutoTag(session.getSpanner().getOptions());
786+
}
787+
return this.cachedTransactionTag;
784788
}
785789

786790
@Nullable
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2026 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;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertFalse;
21+
import static org.junit.Assert.assertNotNull;
22+
import static org.junit.Assert.assertTrue;
23+
24+
import com.google.cloud.NoCredentials;
25+
import com.google.spanner.v1.CommitRequest;
26+
import com.google.spanner.v1.ExecuteSqlRequest;
27+
import org.junit.Before;
28+
import org.junit.Test;
29+
import org.junit.runner.RunWith;
30+
import org.junit.runners.JUnit4;
31+
32+
@RunWith(JUnit4.class)
33+
public class MockAutoTaggingTest extends AbstractMockServerTest {
34+
35+
@Before
36+
public void createSpannerInstanceWithAutoTagging() {
37+
// Overrides spanner instance in base class to enable auto-tagging targeting org.junit
38+
spanner =
39+
SpannerOptions.newBuilder()
40+
.setProjectId("test-project")
41+
.setChannelProvider(channelProvider)
42+
.setCredentials(NoCredentials.getInstance())
43+
.enableAutoTagTransactions()
44+
.setAutoTagTransactionsPackage("org.junit.runners")
45+
.setAutoTagTransactionsTracerLimit(200)
46+
.build()
47+
.getService();
48+
}
49+
50+
@Test
51+
public void testReadWriteTransactionAutoTagging() {
52+
DatabaseClient databaseClient = spanner.getDatabaseClient(DatabaseId.of("proj", "inst", "db"));
53+
mockSpanner.putStatementResult(
54+
MockSpannerServiceImpl.StatementResult.update(
55+
Statement.of("UPDATE Venues SET Capacity = 100"), 1L));
56+
57+
databaseClient
58+
.readWriteTransaction()
59+
.run(transaction -> {
60+
transaction.executeUpdate(Statement.of("UPDATE Venues SET Capacity = 100"));
61+
return null;
62+
});
63+
64+
// 1. Verify transaction tag is successfully populated on the ExecuteSqlRequest
65+
assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
66+
ExecuteSqlRequest sqlRequest = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
67+
String transactionTag = sqlRequest.getRequestOptions().getTransactionTag();
68+
assertEquals("FrameworkMethod$1.runReflectiveCall", transactionTag);
69+
70+
// 2. Verify transaction tag is successfully populated on CommitRequest and matches
71+
assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class));
72+
CommitRequest commitRequest = mockSpanner.getRequestsOfType(CommitRequest.class).get(0);
73+
assertEquals(transactionTag, commitRequest.getRequestOptions().getTransactionTag());
74+
}
75+
76+
@Test
77+
public void testSingleUseQueryAutoTagging() {
78+
DatabaseClient databaseClient = spanner.getDatabaseClient(DatabaseId.of("proj", "inst", "db"));
79+
com.google.spanner.v1.ResultSet select1ResultSet =
80+
com.google.spanner.v1.ResultSet.newBuilder()
81+
.addRows(
82+
com.google.protobuf.ListValue.newBuilder()
83+
.addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build())
84+
.build())
85+
.setMetadata(
86+
com.google.spanner.v1.ResultSetMetadata.newBuilder()
87+
.setRowType(
88+
com.google.spanner.v1.StructType.newBuilder()
89+
.addFields(
90+
com.google.spanner.v1.StructType.Field.newBuilder()
91+
.setName("COL1")
92+
.setType(
93+
com.google.spanner.v1.Type.newBuilder()
94+
.setCode(com.google.spanner.v1.TypeCode.INT64)
95+
.build())
96+
.build())
97+
.build())
98+
.build())
99+
.build();
100+
mockSpanner.putStatementResult(
101+
MockSpannerServiceImpl.StatementResult.query(
102+
Statement.of("SELECT * FROM Albums"), select1ResultSet));
103+
104+
try (ResultSet resultSet =
105+
databaseClient.singleUse().executeQuery(Statement.of("SELECT * FROM Albums"))) {
106+
while (resultSet.next()) {}
107+
}
108+
109+
// Verify request tag is populated on ExecuteSqlRequest and matches junit
110+
assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
111+
ExecuteSqlRequest sqlRequest = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
112+
String requestTag = sqlRequest.getRequestOptions().getRequestTag();
113+
assertEquals("FrameworkMethod$1.runReflectiveCall", requestTag);
114+
}
115+
}

0 commit comments

Comments
 (0)