Skip to content

Commit 4329896

Browse files
authored
feat(storage): enable App-Centric Observability (ACO) support in Otel (#13248)
Adding support for App-Centric Observability (ACO) by ensuring GCS spans include the required id and location resource attributes: `gcp.resource.destination.id` and `gcp.resource.destination.location` - Implemented AcoSpanBuilder and AcoSpan to intercept setAttribute("gsutil.uri", ...), startSpan and endSpan calls - Added a package-private BucketMetadataCache using a bounded LinkedHashMap (10,000 entries) to manage bucket-to-location mappings with low memory overhead - Triggered asynchronous GetBucket metadata resolution on cache misses using a dedicated ThreadPoolExecutor - Managing cache eviction or fallback attributes in case of 404/403 Validated via OtelStorageDecoratorAcoUnitTest and integration tests (ITOpenTelemetryTest, ITOpenTelemetryMPUTest) ---------
1 parent 2f915ab commit 4329896

10 files changed

Lines changed: 1256 additions & 9 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.storage;
18+
19+
import io.opentelemetry.api.trace.SpanBuilder;
20+
import java.util.concurrent.ExecutorService;
21+
import java.util.concurrent.TimeUnit;
22+
23+
abstract class AcoContext implements AutoCloseable {
24+
25+
abstract SpanBuilder wrap(SpanBuilder spanBuilder, OtelStorageDecorator parent);
26+
27+
abstract BucketMetadataCache getCache();
28+
29+
abstract ExecutorService getCacheExecutor();
30+
31+
@Override
32+
public abstract void close();
33+
34+
static AcoContext create(boolean enabled) {
35+
return enabled ? new EnabledAcoContext() : new DisabledAcoContext();
36+
}
37+
38+
private static final class EnabledAcoContext extends AcoContext {
39+
private final BucketMetadataCache bucketMetadataCache =
40+
BucketMetadataCache.getBucketMetadataCache();
41+
private volatile ExecutorService cacheExecutor;
42+
43+
@Override
44+
SpanBuilder wrap(SpanBuilder spanBuilder, OtelStorageDecorator parent) {
45+
return new AcoSpanBuilder(spanBuilder, parent);
46+
}
47+
48+
@Override
49+
BucketMetadataCache getCache() {
50+
return bucketMetadataCache;
51+
}
52+
53+
@Override
54+
ExecutorService getCacheExecutor() {
55+
ExecutorService result = cacheExecutor;
56+
if (result == null) {
57+
synchronized (this) {
58+
result = cacheExecutor;
59+
if (result == null) {
60+
cacheExecutor = result = AcoSpanBuilder.newCacheExecutor();
61+
}
62+
}
63+
}
64+
return result;
65+
}
66+
67+
@Override
68+
public void close() {
69+
bucketMetadataCache.clear();
70+
synchronized (this) {
71+
if (cacheExecutor != null) {
72+
cacheExecutor.shutdownNow();
73+
try {
74+
cacheExecutor.awaitTermination(5, TimeUnit.SECONDS);
75+
} catch (InterruptedException e) {
76+
Thread.currentThread().interrupt();
77+
}
78+
}
79+
}
80+
}
81+
}
82+
83+
private static final class DisabledAcoContext extends AcoContext {
84+
@Override
85+
SpanBuilder wrap(SpanBuilder spanBuilder, OtelStorageDecorator parent) {
86+
return spanBuilder;
87+
}
88+
89+
@Override
90+
BucketMetadataCache getCache() {
91+
return null;
92+
}
93+
94+
@Override
95+
ExecutorService getCacheExecutor() {
96+
return null;
97+
}
98+
99+
@Override
100+
public void close() {}
101+
}
102+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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.storage;
18+
19+
import io.opentelemetry.api.common.AttributeKey;
20+
import io.opentelemetry.api.common.Attributes;
21+
import io.opentelemetry.api.trace.Span;
22+
import io.opentelemetry.api.trace.SpanContext;
23+
import io.opentelemetry.api.trace.StatusCode;
24+
import java.util.concurrent.TimeUnit;
25+
26+
final class AcoSpan implements Span {
27+
private final Span delegate;
28+
private final String bucketName;
29+
private final OtelStorageDecorator parent;
30+
31+
AcoSpan(Span delegate, String bucketName, OtelStorageDecorator parent) {
32+
this.delegate = delegate;
33+
this.bucketName = bucketName;
34+
this.parent = parent;
35+
}
36+
37+
private void applyCacheAttributes() {
38+
if (bucketName != null && parent != null) {
39+
BucketMetadataCache cache = parent.acoContext.getCache();
40+
if (cache != null) {
41+
BucketMetadataCache.BucketMetadata md = cache.get(bucketName);
42+
if (md != null && !md.fetchPending) {
43+
delegate.setAttribute("gcp.resource.destination.id", md.resource);
44+
delegate.setAttribute("gcp.resource.destination.location", md.location);
45+
}
46+
}
47+
}
48+
}
49+
50+
@Override
51+
public void end() {
52+
applyCacheAttributes();
53+
delegate.end();
54+
}
55+
56+
@Override
57+
public void end(long timestamp, TimeUnit unit) {
58+
applyCacheAttributes();
59+
delegate.end(timestamp, unit);
60+
}
61+
62+
@Override
63+
public Span recordException(Throwable exception) {
64+
delegate.recordException(exception);
65+
handleException(exception);
66+
return this;
67+
}
68+
69+
@Override
70+
public Span recordException(Throwable exception, Attributes attributes) {
71+
delegate.recordException(exception, attributes);
72+
handleException(exception);
73+
return this;
74+
}
75+
76+
private void handleException(Throwable exception) {
77+
if (exception instanceof StorageException && parent != null) {
78+
StorageException se = (StorageException) exception;
79+
if (se.getCode() == 404 && se.getMessage() != null) {
80+
String msg = se.getMessage().toLowerCase(java.util.Locale.US);
81+
if (msg.contains("bucket not found") || msg.contains("bucket does not exist")) {
82+
BucketMetadataCache cache = parent.acoContext.getCache();
83+
if (cache != null) {
84+
cache.remove(bucketName);
85+
}
86+
}
87+
}
88+
}
89+
}
90+
91+
@Override
92+
public Span setAttribute(String k, String v) {
93+
delegate.setAttribute(k, v);
94+
return this;
95+
}
96+
97+
@Override
98+
public Span setAttribute(String k, long v) {
99+
delegate.setAttribute(k, v);
100+
return this;
101+
}
102+
103+
@Override
104+
public Span setAttribute(String k, double v) {
105+
delegate.setAttribute(k, v);
106+
return this;
107+
}
108+
109+
@Override
110+
public Span setAttribute(String k, boolean v) {
111+
delegate.setAttribute(k, v);
112+
return this;
113+
}
114+
115+
@Override
116+
public <T> Span setAttribute(AttributeKey<T> k, T v) {
117+
delegate.setAttribute(k, v);
118+
return this;
119+
}
120+
121+
@Override
122+
public Span addEvent(String n) {
123+
delegate.addEvent(n);
124+
return this;
125+
}
126+
127+
@Override
128+
public Span addEvent(String n, Attributes a) {
129+
delegate.addEvent(n, a);
130+
return this;
131+
}
132+
133+
@Override
134+
public Span addEvent(String n, long t, TimeUnit u) {
135+
delegate.addEvent(n, t, u);
136+
return this;
137+
}
138+
139+
@Override
140+
public Span addEvent(String n, Attributes a, long t, TimeUnit u) {
141+
delegate.addEvent(n, a, t, u);
142+
return this;
143+
}
144+
145+
@Override
146+
public Span setStatus(StatusCode c) {
147+
delegate.setStatus(c);
148+
return this;
149+
}
150+
151+
@Override
152+
public Span setStatus(StatusCode c, String d) {
153+
delegate.setStatus(c, d);
154+
return this;
155+
}
156+
157+
@Override
158+
public Span updateName(String name) {
159+
delegate.updateName(name);
160+
return this;
161+
}
162+
163+
@Override
164+
public SpanContext getSpanContext() {
165+
return delegate.getSpanContext();
166+
}
167+
168+
@Override
169+
public boolean isRecording() {
170+
return delegate.isRecording();
171+
}
172+
}

0 commit comments

Comments
 (0)