Skip to content

Commit d0d400f

Browse files
feat(spanner): add option for auto-tagging transactions
1 parent ca594e4 commit d0d400f

7 files changed

Lines changed: 652 additions & 3 deletions

File tree

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

Lines changed: 17 additions & 1 deletion
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;
@@ -841,7 +842,22 @@ RequestOptions buildRequestOptions(Options options) {
841842
builder.setClientContext(clientContextBuilder.build());
842843
}
843844
if (getTransactionTag() != null) {
844-
builder.setTransactionTag(getTransactionTag());
845+
// Read-write transactions support transaction-level tags only. We populate the
846+
// transaction tag on the builder if it is non-empty.
847+
if (!getTransactionTag().isEmpty()) {
848+
builder.setTransactionTag(getTransactionTag());
849+
}
850+
} else if (session.getSpanner().getOptions().isAutoTaggingEnabled()
851+
&& builder.getRequestTag().isEmpty()) {
852+
// Read-only contexts (both single-use and multi-use) do not support transaction-level tags.
853+
// We lazily resolve and populate the request tag instead.
854+
if (this.cachedRequestTag == null) {
855+
String autoTag = AutoTagHelper.getAutoTag(session.getSpanner().getOptions());
856+
this.cachedRequestTag = autoTag == null ? "" : autoTag;
857+
}
858+
if (!this.cachedRequestTag.isEmpty()) {
859+
builder.setRequestTag(this.cachedRequestTag);
860+
}
845861
}
846862
return builder.build();
847863
}
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.getAutoTaggingTracerLimit();
50+
int limit = Math.min(stackTrace.length, tracerLimit);
51+
List<String> targetPackages = options.getAutoTaggingPackages();
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: 82 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 autoTaggingEnabled;
313+
private final List<String> autoTaggingPackages;
314+
private final int autoTaggingTracerLimit;
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+
autoTaggingEnabled = builder.autoTaggingEnabled;
1001+
autoTaggingPackages = builder.autoTaggingPackages;
1002+
autoTaggingTracerLimit = builder.autoTaggingTracerLimit;
9961003
}
9971004

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

1074+
default boolean isAutoTaggingDisabled() {
1075+
return false;
1076+
}
1077+
1078+
default boolean isAutoTaggingEnabled() {
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 isAutoTaggingDisabled() {
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 isAutoTaggingEnabled() {
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 autoTaggingEnabled = false;
1287+
private List<String> autoTaggingPackages = Collections.emptyList();
1288+
private int autoTaggingTracerLimit = 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.autoTaggingEnabled = options.autoTaggingEnabled;
1396+
this.autoTaggingPackages = options.autoTaggingPackages;
1397+
this.autoTaggingTracerLimit = options.autoTaggingTracerLimit;
13651398
}
13661399

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

2156+
public Builder enableAutoTagging() {
2157+
this.autoTaggingEnabled = true;
2158+
return this;
2159+
}
2160+
2161+
public Builder disableAutoTagging() {
2162+
this.autoTaggingEnabled = false;
2163+
return this;
2164+
}
2165+
2166+
public Builder setAutoTaggingPackages(String... autoTaggingPackages) {
2167+
this.autoTaggingPackages =
2168+
Collections.unmodifiableList(
2169+
new ArrayList<>(
2170+
java.util.Arrays.asList(Preconditions.checkNotNull(autoTaggingPackages))));
2171+
return this;
2172+
}
2173+
2174+
public Builder setAutoTaggingPackages(List<String> autoTaggingPackages) {
2175+
this.autoTaggingPackages =
2176+
Collections.unmodifiableList(
2177+
new ArrayList<>(Preconditions.checkNotNull(autoTaggingPackages)));
2178+
return this;
2179+
}
2180+
2181+
public Builder setAutoTaggingTracerLimit(int autoTaggingTracerLimit) {
2182+
this.autoTaggingTracerLimit = autoTaggingTracerLimit;
2183+
return this;
2184+
}
2185+
21232186
@SuppressWarnings("rawtypes")
21242187
@Override
21252188
public SpannerOptions build() {
@@ -2547,6 +2610,25 @@ public TransactionOptions getDefaultTransactionOptions() {
25472610
return defaultTransactionOptions;
25482611
}
25492612

2613+
public boolean isAutoTaggingEnabled() {
2614+
if (environment.isAutoTaggingDisabled()) {
2615+
return false;
2616+
}
2617+
return autoTaggingEnabled || environment.isAutoTaggingEnabled();
2618+
}
2619+
2620+
public List<String> getAutoTaggingPackages() {
2621+
return autoTaggingPackages;
2622+
}
2623+
2624+
public int getAutoTaggingTracerLimit() {
2625+
return autoTaggingTracerLimit;
2626+
}
2627+
2628+
public boolean isAutoTaggingDisabled() {
2629+
return environment.isAutoTaggingDisabled();
2630+
}
2631+
25502632
@BetaApi
25512633
public boolean isUseVirtualThreads() {
25522634
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().isAutoTaggingEnabled()) {
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;
792+
}
783793
return null;
784794
}
785795

0 commit comments

Comments
 (0)