Skip to content

Commit a382a4e

Browse files
committed
chore: Add implementation for firestore resource
1 parent ffd8277 commit a382a4e

File tree

5 files changed

+1043
-0
lines changed

5 files changed

+1043
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.datastore.telemetry;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
/** A {@link MetricsRecorder} that fans out to multiple recorders. */
23+
class CompositeMetricsRecorder implements MetricsRecorder {
24+
25+
private final List<MetricsRecorder> recorders;
26+
27+
CompositeMetricsRecorder(List<MetricsRecorder> recorders) {
28+
this.recorders = recorders;
29+
}
30+
31+
@Override
32+
public void recordTransactionLatency(double latencyMs, Map<String, String> attributes) {
33+
for (MetricsRecorder recorder : recorders) {
34+
recorder.recordTransactionLatency(latencyMs, attributes);
35+
}
36+
}
37+
38+
@Override
39+
public void recordTransactionAttemptCount(long count, Map<String, String> attributes) {
40+
for (MetricsRecorder recorder : recorders) {
41+
recorder.recordTransactionAttemptCount(count, attributes);
42+
}
43+
}
44+
45+
@Override
46+
public void recordAttemptLatency(double latencyMs, Map<String, String> attributes) {
47+
for (MetricsRecorder recorder : recorders) {
48+
recorder.recordAttemptLatency(latencyMs, attributes);
49+
}
50+
}
51+
52+
@Override
53+
public void recordAttemptCount(long count, Map<String, String> attributes) {
54+
for (MetricsRecorder recorder : recorders) {
55+
recorder.recordAttemptCount(count, attributes);
56+
}
57+
}
58+
59+
@Override
60+
public void recordOperationLatency(double latencyMs, Map<String, String> attributes) {
61+
for (MetricsRecorder recorder : recorders) {
62+
recorder.recordOperationLatency(latencyMs, attributes);
63+
}
64+
}
65+
66+
@Override
67+
public void recordOperationCount(long count, Map<String, String> attributes) {
68+
for (MetricsRecorder recorder : recorders) {
69+
recorder.recordOperationCount(count, attributes);
70+
}
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
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.datastore.telemetry;
18+
19+
import com.google.api.gax.core.GaxProperties;
20+
import com.google.auth.Credentials;
21+
import com.google.common.base.Strings;
22+
import io.opentelemetry.api.OpenTelemetry;
23+
import io.opentelemetry.api.common.Attributes;
24+
import io.opentelemetry.api.common.AttributesBuilder;
25+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
26+
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
27+
import io.opentelemetry.sdk.resources.Resource;
28+
import java.io.BufferedReader;
29+
import java.io.IOException;
30+
import java.io.InputStream;
31+
import java.io.InputStreamReader;
32+
import java.lang.management.ManagementFactory;
33+
import java.lang.reflect.Method;
34+
import java.net.HttpURLConnection;
35+
import java.net.URL;
36+
import java.nio.charset.StandardCharsets;
37+
import java.util.HashMap;
38+
import java.util.Map;
39+
import java.util.logging.Level;
40+
import java.util.logging.Logger;
41+
import javax.annotation.Nullable;
42+
43+
/**
44+
* Provides a built-in {@link OpenTelemetry} instance for Datastore client-side metrics.
45+
*
46+
* <p>This class is responsible for configuring a private {@link OpenTelemetrySdk} that exports
47+
* metrics to Google Cloud Monitoring using a {@link DatastoreCloudMonitoringExporter}.
48+
*
49+
* <p>The implementation follows the pattern used in other Google Cloud client libraries, providing
50+
* automated environment detection and resource attribute configuration for the {@code
51+
* firestore.googleapis.com/Database} monitored resource.
52+
*/
53+
public final class DatastoreBuiltInMetricsProvider {
54+
55+
public static final DatastoreBuiltInMetricsProvider INSTANCE =
56+
new DatastoreBuiltInMetricsProvider();
57+
58+
private static final Logger logger =
59+
Logger.getLogger(DatastoreBuiltInMetricsProvider.class.getName());
60+
61+
private static String taskId;
62+
private static String location;
63+
private static final String DEFAULT_LOCATION = "global";
64+
65+
private SdkMeterProvider sdkMeterProvider;
66+
private OpenTelemetry openTelemetry;
67+
68+
private DatastoreBuiltInMetricsProvider() {}
69+
70+
/**
71+
* Returns a singleton {@link OpenTelemetry} instance for built-in metrics.
72+
*
73+
* <p>This method initializes an {@link SdkMeterProvider} with a {@link
74+
* DatastoreCloudMonitoringExporter} and the appropriate resource attributes. It also registers a
75+
* shutdown hook to ensure metrics are flushed when the JVM exits.
76+
*
77+
* @param projectId the GCP project ID.
78+
* @param databaseId the Datastore database ID.
79+
* @param credentials the credentials to use for exporting metrics.
80+
* @param monitoringHost optional monitoring host override.
81+
* @param universeDomain the universe domain to use for monitoring.
82+
* @return the {@link OpenTelemetry} instance, or {@code null} if it could not be created.
83+
*/
84+
public OpenTelemetry getOrCreateOpenTelemetry(
85+
String projectId,
86+
String databaseId,
87+
@Nullable Credentials credentials,
88+
@Nullable String monitoringHost,
89+
String universeDomain) {
90+
try {
91+
if (this.openTelemetry == null) {
92+
SdkMeterProviderBuilder sdkMeterProviderBuilder = SdkMeterProvider.builder();
93+
// Register Datastore-specific views and the PeriodicMetricReader.
94+
DatastoreBuiltInMetricsView.registerBuiltinMetrics(
95+
DatastoreCloudMonitoringExporter.create(
96+
projectId, credentials, monitoringHost, universeDomain),
97+
sdkMeterProviderBuilder);
98+
// Configure the monitored resource attributes.
99+
sdkMeterProviderBuilder.setResource(
100+
Resource.create(createResourceAttributes(projectId, databaseId)));
101+
this.sdkMeterProvider = sdkMeterProviderBuilder.build();
102+
this.openTelemetry = new DatastoreOpenTelemetry(this.sdkMeterProvider);
103+
// Ensure cleanup on shutdown.
104+
Runtime.getRuntime().addShutdownHook(new Thread(this.sdkMeterProvider::close));
105+
}
106+
return this.openTelemetry;
107+
} catch (IOException ex) {
108+
logger.log(
109+
Level.WARNING,
110+
"Unable to get OpenTelemetry object for client side metrics, will skip exporting client"
111+
+ " side metrics",
112+
ex);
113+
return null;
114+
}
115+
}
116+
117+
/**
118+
* Quick check to see if the application is running on GCP by querying the metadata server.
119+
*
120+
* <p>This is used to determine if we should enable built-in metrics by default.
121+
*
122+
* @return {@code true} if running on GCP, {@code false} otherwise.
123+
*/
124+
public static boolean quickCheckIsRunningOnGcp() {
125+
int timeout = 5000;
126+
try {
127+
URL url = new URL("http://metadata.google.internal/computeMetadata/v1/project/project-id");
128+
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
129+
connection.setConnectTimeout(timeout);
130+
connection.setRequestProperty("Metadata-Flavor", "Google");
131+
if (connection.getResponseCode() == 200
132+
&& ("Google").equals(connection.getHeaderField("Metadata-Flavor"))) {
133+
InputStream input = connection.getInputStream();
134+
try (BufferedReader reader =
135+
new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
136+
return !Strings.isNullOrEmpty(reader.readLine());
137+
}
138+
}
139+
} catch (IOException ignore) {
140+
// ignore
141+
}
142+
return false;
143+
}
144+
145+
/**
146+
* Detects the client's GCP location (region).
147+
*
148+
* <p>To avoid dependencies on external resource detection libraries, this implementation
149+
* currently defaults to "global".
150+
*
151+
* @return the detected location, or "global" if detection fails.
152+
*/
153+
public static String detectClientLocation() {
154+
if (location == null) {
155+
location = DEFAULT_LOCATION;
156+
}
157+
return location;
158+
}
159+
160+
/**
161+
* Creates resource attributes for the {@code firestore.googleapis.com/Database} monitored
162+
* resource.
163+
*
164+
* <p>These attributes are attached to the OTel {@link Resource} and used by the exporter to
165+
* populate the resource labels in Cloud Monitoring.
166+
*
167+
* @param projectId the GCP project ID.
168+
* @param databaseId the Datastore database ID.
169+
* @return the resource attributes.
170+
*/
171+
Attributes createResourceAttributes(String projectId, String databaseId) {
172+
AttributesBuilder attributesBuilder =
173+
Attributes.builder()
174+
.put(TelemetryConstants.PROJECT_ID_KEY, projectId)
175+
.put(TelemetryConstants.DATABASE_ID_KEY, databaseId)
176+
.put(TelemetryConstants.LOCATION_ID_KEY, detectClientLocation());
177+
178+
return attributesBuilder.build();
179+
}
180+
181+
/**
182+
* Creates common client attributes for each metric data point.
183+
*
184+
* <p>These attributes (client_name, client_uid) are added to every exported metric to identify
185+
* the specific client instance.
186+
*
187+
* @return a map of client attributes.
188+
*/
189+
public Map<String, String> createClientAttributes() {
190+
Map<String, String> clientAttributes = new HashMap<>();
191+
clientAttributes.put(
192+
TelemetryConstants.CLIENT_NAME_KEY.getKey(),
193+
"datastore-java/" + GaxProperties.getLibraryVersion(getClass()));
194+
clientAttributes.put(TelemetryConstants.CLIENT_UID_KEY.getKey(), getDefaultTaskValue());
195+
clientAttributes.put(TelemetryConstants.SERVICE_KEY.getKey(), TelemetryConstants.SERVICE_VALUE);
196+
return clientAttributes;
197+
}
198+
199+
/**
200+
* Generates a unique identifier for the {@code client_uid} metric field.
201+
*
202+
* <p>The identifier is composed of a UUID, the process ID (PID), and the hostname to ensure
203+
* uniqueness across different instances and restarts.
204+
*
205+
* @return a unique identifier string in the format UUID@PID@hostname.
206+
*/
207+
private static String getDefaultTaskValue() {
208+
if (taskId == null) {
209+
taskId = ManagementFactory.getRuntimeMXBean().getName();
210+
}
211+
return taskId;
212+
}
213+
214+
/**
215+
* Returns the process ID.
216+
*
217+
* <p>Attempts to use {@code ProcessHandle} (Java 9+) and falls back to {@code RuntimeMXBean} for
218+
* Java 8 compatibility.
219+
*
220+
* @return the PID as a string.
221+
*/
222+
private static String getProcessId() {
223+
try {
224+
// Check if Java 9+ and ProcessHandle class is available
225+
Class<?> processHandleClass = Class.forName("java.lang.ProcessHandle");
226+
Method currentMethod = processHandleClass.getMethod("current");
227+
Object processHandleInstance = currentMethod.invoke(null);
228+
Method pidMethod = processHandleClass.getMethod("pid");
229+
long pid = (long) pidMethod.invoke(processHandleInstance);
230+
return Long.toString(pid);
231+
} catch (Exception e) {
232+
// Fallback to Java 8 method
233+
final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
234+
if (jvmName != null && jvmName.contains("@")) {
235+
return jvmName.split("@")[0];
236+
} else {
237+
return "unknown";
238+
}
239+
}
240+
}
241+
242+
/**
243+
* A simple {@link OpenTelemetry} implementation that only provides a {@link
244+
* io.opentelemetry.api.metrics.MeterProvider}.
245+
*
246+
* <p>This avoids a dependency on {@code OpenTelemetrySdk} and {@code SdkTracerProvider}, which
247+
* are not needed for built-in metrics and might not be available on the classpath at runtime.
248+
*/
249+
private static class DatastoreOpenTelemetry implements OpenTelemetry {
250+
private final io.opentelemetry.api.metrics.MeterProvider meterProvider;
251+
252+
DatastoreOpenTelemetry(io.opentelemetry.api.metrics.MeterProvider meterProvider) {
253+
this.meterProvider = meterProvider;
254+
}
255+
256+
@Override
257+
public io.opentelemetry.api.metrics.MeterProvider getMeterProvider() {
258+
return meterProvider;
259+
}
260+
261+
@Override
262+
public io.opentelemetry.api.trace.TracerProvider getTracerProvider() {
263+
return io.opentelemetry.api.trace.TracerProvider.noop();
264+
}
265+
266+
@Override
267+
public io.opentelemetry.context.propagation.ContextPropagators getPropagators() {
268+
return io.opentelemetry.context.propagation.ContextPropagators.noop();
269+
}
270+
}
271+
}

0 commit comments

Comments
 (0)