Skip to content

Commit f036bcd

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 f036bcd

6 files changed

Lines changed: 557 additions & 0 deletions

File tree

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

Lines changed: 10 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;
@@ -842,6 +843,15 @@ RequestOptions buildRequestOptions(Options options) {
842843
}
843844
if (getTransactionTag() != null) {
844845
builder.setTransactionTag(getTransactionTag());
846+
} else if (session.getSpanner().getOptions().isAutoTagTransactionsEnabled()
847+
&& builder.getRequestTag().isEmpty()) {
848+
if (this.cachedRequestTag == null) {
849+
String autoTag = AutoTagHelper.getAutoTag(session.getSpanner().getOptions());
850+
this.cachedRequestTag = autoTag == null ? "" : autoTag;
851+
}
852+
if (!this.cachedRequestTag.isEmpty()) {
853+
builder.setRequestTag(this.cachedRequestTag);
854+
}
845855
}
846856
return builder.build();
847857
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
/** Helper for Spanner transaction tags. */
22+
final class AutoTagHelper {
23+
24+
/** Maximum allowed character length for resolved tags. */
25+
private static final int MAX_TAG_LENGTH = 50;
26+
27+
/** Ignored packages. */
28+
private static final String[] INTERNAL_PACKAGES;
29+
30+
static {
31+
INTERNAL_PACKAGES =
32+
new String[] {
33+
"java.",
34+
"javax.",
35+
"jdk.",
36+
"sun.",
37+
"io.grpc.",
38+
"com.google.cloud.spanner.",
39+
"com.google.api."
40+
};
41+
}
42+
43+
private AutoTagHelper() {
44+
// prevent instantiation
45+
}
46+
47+
static String getAutoTag(final SpannerOptions options) {
48+
StackTraceElement[] stackTrace = new Throwable().getStackTrace();
49+
int tracerLimit = options.getAutoTagTransactionsTracerLimit();
50+
int limit = Math.min(stackTrace.length, tracerLimit);
51+
List<String> targetPackages = options.getAutoTagTransactionsPackages();
52+
boolean hasTarget = targetPackages != null && !targetPackages.isEmpty();
53+
54+
for (int i = 0; i < limit; i++) {
55+
StackTraceElement element = stackTrace[i];
56+
String className = element.getClassName();
57+
if (hasTarget) {
58+
for (String targetPackage : targetPackages) {
59+
if (className.startsWith(targetPackage)) {
60+
return formatTag(className, element.getMethodName());
61+
}
62+
}
63+
} else if (isInternalPackage(className)) {
64+
continue;
65+
} else {
66+
return formatTag(className, element.getMethodName());
67+
}
68+
}
69+
return null;
70+
}
71+
72+
private static boolean isInternalPackage(final String cls) {
73+
for (String internalPackage : INTERNAL_PACKAGES) {
74+
if (cls.startsWith(internalPackage)) {
75+
return true;
76+
}
77+
}
78+
return false;
79+
}
80+
81+
private static String formatTag(final String cls, final String method) {
82+
int lastDot = cls.lastIndexOf('.');
83+
String simpleClassName;
84+
if (lastDot == -1) {
85+
simpleClassName = cls;
86+
} else {
87+
simpleClassName = cls.substring(lastDot + 1);
88+
}
89+
String tag = simpleClassName + "." + method;
90+
if (tag.length() > MAX_TAG_LENGTH) {
91+
tag = tag.substring(tag.length() - MAX_TAG_LENGTH);
92+
}
93+
return tag;
94+
}
95+
}

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

Lines changed: 79 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,14 @@ default boolean isEnableLocationApi() {
10641071
return false;
10651072
}
10661073

1074+
default boolean isAutoTagTransactionsDisabled() {
1075+
return false;
1076+
}
1077+
1078+
default boolean isAutoTagTransactionsEnabled() {
1079+
return false;
1080+
}
1081+
10671082
@Deprecated
10681083
@ObsoleteApi(
10691084
"This will be removed in an upcoming version without a major version bump. You should use"
@@ -1168,6 +1183,18 @@ public boolean isEnableLocationApi() {
11681183
return Boolean.parseBoolean(System.getenv(EXPERIMENTAL_LOCATION_API_ENV_VAR));
11691184
}
11701185

1186+
@Override
1187+
public boolean isAutoTagTransactionsDisabled() {
1188+
return Boolean.parseBoolean(System.getenv("SPANNER_DISABLE_AUTO_TAGGING"))
1189+
|| Boolean.parseBoolean(System.getProperty("spanner.disable_auto_tagging"));
1190+
}
1191+
1192+
@Override
1193+
public boolean isAutoTagTransactionsEnabled() {
1194+
return Boolean.parseBoolean(System.getenv("SPANNER_ENABLE_AUTO_TAGGING"))
1195+
|| Boolean.getBoolean("spanner.enable_auto_tagging");
1196+
}
1197+
11711198
@Override
11721199
public String getMonitoringHost() {
11731200
return System.getenv(SPANNER_MONITORING_HOST);
@@ -1256,6 +1283,9 @@ public static class Builder
12561283
private boolean usePlainText = false;
12571284
private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance();
12581285
private RequestOptions.ClientContext clientContext;
1286+
private boolean autoTagTransactionsEnabled = false;
1287+
private List<String> autoTagTransactionsPackages = Collections.emptyList();
1288+
private int autoTagTransactionsTracerLimit = 50;
12591289

12601290
private static String createCustomClientLibToken(String token) {
12611291
return token + " " + ServiceOptions.getGoogApiClientLibName();
@@ -1362,6 +1392,9 @@ protected Builder() {
13621392
this.monitoringHost = options.monitoringHost;
13631393
this.defaultTransactionOptions = options.defaultTransactionOptions;
13641394
this.clientContext = options.clientContext;
1395+
this.autoTagTransactionsEnabled = options.autoTagTransactionsEnabled;
1396+
this.autoTagTransactionsPackages = options.autoTagTransactionsPackages;
1397+
this.autoTagTransactionsTracerLimit = options.autoTagTransactionsTracerLimit;
13651398
}
13661399

13671400
@Override
@@ -2120,6 +2153,33 @@ public Builder setDefaultClientContext(RequestOptions.ClientContext clientContex
21202153
return this;
21212154
}
21222155

2156+
public Builder enableAutoTagTransactions() {
2157+
this.autoTagTransactionsEnabled = true;
2158+
return this;
2159+
}
2160+
2161+
public Builder disableAutoTagTransactions() {
2162+
this.autoTagTransactionsEnabled = false;
2163+
return this;
2164+
}
2165+
2166+
public Builder setAutoTagTransactionsPackage(String autoTagTransactionsPackage) {
2167+
this.autoTagTransactionsPackages = Collections.singletonList(autoTagTransactionsPackage);
2168+
return this;
2169+
}
2170+
2171+
public Builder setAutoTagTransactionsPackages(List<String> autoTagTransactionsPackages) {
2172+
this.autoTagTransactionsPackages =
2173+
Collections.unmodifiableList(
2174+
new ArrayList<>(Preconditions.checkNotNull(autoTagTransactionsPackages)));
2175+
return this;
2176+
}
2177+
2178+
public Builder setAutoTagTransactionsTracerLimit(int autoTagTransactionsTracerLimit) {
2179+
this.autoTagTransactionsTracerLimit = autoTagTransactionsTracerLimit;
2180+
return this;
2181+
}
2182+
21232183
@SuppressWarnings("rawtypes")
21242184
@Override
21252185
public SpannerOptions build() {
@@ -2547,6 +2607,25 @@ public TransactionOptions getDefaultTransactionOptions() {
25472607
return defaultTransactionOptions;
25482608
}
25492609

2610+
public boolean isAutoTagTransactionsEnabled() {
2611+
if (environment.isAutoTagTransactionsDisabled()) {
2612+
return false;
2613+
}
2614+
return autoTagTransactionsEnabled || environment.isAutoTagTransactionsEnabled();
2615+
}
2616+
2617+
public List<String> getAutoTagTransactionsPackages() {
2618+
return autoTagTransactionsPackages;
2619+
}
2620+
2621+
public int getAutoTagTransactionsTracerLimit() {
2622+
return autoTagTransactionsTracerLimit;
2623+
}
2624+
2625+
public boolean isAutoTagTransactionsDisabled() {
2626+
return environment.isAutoTagTransactionsDisabled();
2627+
}
2628+
25502629
@BetaApi
25512630
public boolean isUseVirtualThreads() {
25522631
return useVirtualThreads;

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

Lines changed: 10 additions & 0 deletions
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,6 +781,15 @@ String getTransactionTag() {
780781
if (this.options.hasTag()) {
781782
return this.options.tag();
782783
}
784+
if (session.getSpanner().getOptions().isAutoTagTransactionsEnabled()) {
785+
if (this.cachedTransactionTag == null) {
786+
this.cachedTransactionTag = AutoTagHelper.getAutoTag(session.getSpanner().getOptions());
787+
if (this.cachedTransactionTag == null) {
788+
this.cachedTransactionTag = "";
789+
}
790+
}
791+
return this.cachedTransactionTag.isEmpty() ? null : this.cachedTransactionTag;
792+
}
783793
return null;
784794
}
785795

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.example.spanner;
18+
19+
import com.google.api.core.ApiFuture;
20+
import com.google.cloud.spanner.DatabaseClient;
21+
import com.google.cloud.spanner.Options.TransactionOption;
22+
import com.google.cloud.spanner.TransactionRunner;
23+
import com.google.common.util.concurrent.MoreExecutors;
24+
25+
/** Helper class in a non-ignored package to stable-test auto-tagging stack walks. */
26+
public final class TagTestHelper {
27+
28+
private TagTestHelper() {}
29+
30+
public static <T> T run(
31+
DatabaseClient client, TransactionRunner.TransactionCallable<T> callable) {
32+
return client.readWriteTransaction().run(callable);
33+
}
34+
35+
public static <T> T runWithOptions(
36+
DatabaseClient client,
37+
TransactionRunner.TransactionCallable<T> callable,
38+
TransactionOption option) {
39+
return client.readWriteTransaction(option).run(callable);
40+
}
41+
42+
public static <R> ApiFuture<R> runAsync(
43+
DatabaseClient client, com.google.cloud.spanner.AsyncRunner.AsyncWork<R> work) {
44+
return client.runAsync().runAsync(work, MoreExecutors.directExecutor());
45+
}
46+
47+
public static void singleUseConsume(
48+
DatabaseClient client, com.google.cloud.spanner.Statement stmt) {
49+
try (com.google.cloud.spanner.ResultSet resultSet = client.singleUse().executeQuery(stmt)) {
50+
while (resultSet.next()) {}
51+
}
52+
}
53+
54+
public static void readOnlyTxnConsume(
55+
com.google.cloud.spanner.ReadOnlyTransaction txn, com.google.cloud.spanner.Statement stmt) {
56+
try (com.google.cloud.spanner.ResultSet resultSet = txn.executeQuery(stmt)) {
57+
while (resultSet.next()) {}
58+
}
59+
}
60+
61+
/** Subclass with an extremely long name to test tag truncation rules. */
62+
public static final class ExtremelyLongClassNameForTestingTagTruncationSupportUnderCheckstyle {
63+
public static <T> T run(
64+
DatabaseClient client, TransactionRunner.TransactionCallable<T> callable) {
65+
return client.readWriteTransaction().run(callable);
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)