Skip to content

Commit 64d437c

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 64d437c

5 files changed

Lines changed: 502 additions & 2 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,14 @@ QueryOptions buildQueryOptions(QueryOptions requestOptions) {
831831

832832
RequestOptions buildRequestOptions(Options options) {
833833
RequestOptions.Builder builder = options.toRequestOptionsProto(false).toBuilder();
834+
if (session.getSpanner().getOptions().isAutoTagTransactionsEnabled()
835+
&& getTransactionTag() == null
836+
&& builder.getRequestTag().isEmpty()) {
837+
String autoTag = AutoTagHelper.getAutoTag(session.getSpanner().getOptions());
838+
if (autoTag != null) {
839+
builder.setRequestTag(autoTag);
840+
}
841+
}
834842
RequestOptions.ClientContext defaultClientContext =
835843
session.getSpanner().getOptions().getClientContext();
836844
if (defaultClientContext != null) {
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ static class Builder extends AbstractReadContext.Builder<Builder, TransactionCon
9595

9696
private Clock clock = new Clock();
9797
private ByteString transactionId;
98-
// This field is set only when the transaction is created during a retry and uses a
99-
// multiplexed session.
10098
private ByteString previousTransactionId;
10199
private Options options;
102100
private boolean trackTransactionStarter;
@@ -198,6 +196,7 @@ public void removeListener(Runnable listener) {
198196
private boolean aborted;
199197

200198
private final Options options;
199+
private volatile String cachedTransactionTag;
201200

202201
/** Default to -1 to indicate not available. */
203202
@GuardedBy("lock")
@@ -780,6 +779,15 @@ String getTransactionTag() {
780779
if (this.options.hasTag()) {
781780
return this.options.tag();
782781
}
782+
if (session.getSpanner().getOptions().isAutoTagTransactionsEnabled()) {
783+
if (this.cachedTransactionTag == null) {
784+
this.cachedTransactionTag = AutoTagHelper.getAutoTag(session.getSpanner().getOptions());
785+
if (this.cachedTransactionTag == null) {
786+
this.cachedTransactionTag = "";
787+
}
788+
}
789+
return this.cachedTransactionTag.isEmpty() ? null : this.cachedTransactionTag;
790+
}
783791
return null;
784792
}
785793

0 commit comments

Comments
 (0)