Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@

package com.google.cloud.datastore;

import static com.google.cloud.datastore.telemetry.TraceUtil.ATTRIBUTES_KEY_DEFERRED;
import static com.google.cloud.datastore.telemetry.TraceUtil.ATTRIBUTES_KEY_DOCUMENT_COUNT;
import static com.google.cloud.datastore.telemetry.TraceUtil.ATTRIBUTES_KEY_MISSING;
import static com.google.cloud.datastore.telemetry.TraceUtil.ATTRIBUTES_KEY_MORE_RESULTS;
import static com.google.cloud.datastore.telemetry.TraceUtil.ATTRIBUTES_KEY_READ_CONSISTENCY;
import static com.google.cloud.datastore.telemetry.TraceUtil.ATTRIBUTES_KEY_RECEIVED;
import static com.google.cloud.datastore.telemetry.TraceUtil.ATTRIBUTES_KEY_TRANSACTIONAL;
import static com.google.cloud.datastore.telemetry.TraceUtil.ATTRIBUTES_KEY_TRANSACTION_ID;
import static com.google.cloud.datastore.telemetry.TelemetryConstants.ATTRIBUTES_KEY_DEFERRED;
import static com.google.cloud.datastore.telemetry.TelemetryConstants.ATTRIBUTES_KEY_DOCUMENT_COUNT;
import static com.google.cloud.datastore.telemetry.TelemetryConstants.ATTRIBUTES_KEY_MISSING;
import static com.google.cloud.datastore.telemetry.TelemetryConstants.ATTRIBUTES_KEY_MORE_RESULTS;
import static com.google.cloud.datastore.telemetry.TelemetryConstants.ATTRIBUTES_KEY_READ_CONSISTENCY;
import static com.google.cloud.datastore.telemetry.TelemetryConstants.ATTRIBUTES_KEY_RECEIVED;
import static com.google.cloud.datastore.telemetry.TelemetryConstants.ATTRIBUTES_KEY_TRANSACTIONAL;
import static com.google.cloud.datastore.telemetry.TelemetryConstants.ATTRIBUTES_KEY_TRANSACTION_ID;
import static com.google.cloud.datastore.telemetry.TraceUtil.SPAN_NAME_ALLOCATE_IDS;
import static com.google.cloud.datastore.telemetry.TraceUtil.SPAN_NAME_BEGIN_TRANSACTION;
import static com.google.cloud.datastore.telemetry.TraceUtil.SPAN_NAME_COMMIT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public DatastoreOpenTelemetryOptions.Builder setTracingEnabled(boolean enabled)
* @return the builder instance.
*/
@Nonnull
public DatastoreOpenTelemetryOptions.Builder setMetricsEnabled(boolean enabled) {
DatastoreOpenTelemetryOptions.Builder setMetricsEnabled(boolean enabled) {
this.metricsEnabled = enabled;
return this;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.datastore.telemetry;

import com.google.cloud.datastore.DatastoreOpenTelemetryOptions;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import java.util.Map;
import javax.annotation.Nonnull;

/** Interface to record specific metric operations. */
public interface MetricsRecorder {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two questions:

  1. Do we need another layer for built in metrics? If we don't plan to use a different framework for built in metrics, and/or let customers record these metrics by themselves, maybe we can have a OpenTemeletryMetricsRecorder directly?
  2. If we do want another layer, maybe this interface can be package private as well?

Copy link
Copy Markdown
Member Author

@lqiu96 lqiu96 Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, oops. I didn't realize this was public. I will create a PR to remove this visibility before a new version is cut.

I was thinking there would be something like (names not set in stone):

MetricsRecorder
-> CompositeMetricsRecorder (will take any number of MetricRecorder impls)
-> OpenTelemetryMetricsRecorder (takes in otel object so this can be configured to be built-in or customer supplied)

I'm still thinking through the built-in aspect and I'll reduce visibility to allow for change in the future.

/** Records the total latency of a transaction in milliseconds. */
void recordTransactionLatency(double latencyMs, Map<String, String> attributes);

/** Records the number of attempts a transaction took. */
void recordTransactionAttemptCount(long count, Map<String, String> attributes);

/**
* Returns a {@link MetricsRecorder} instance based on the provided OpenTelemetry options.
*
* @param options The {@link com.google.cloud.datastore.DatastoreOpenTelemetryOptions} configuring
* telemetry.
* @return An {@link OpenTelemetryMetricsRecorder} if metrics are enabled, otherwise a {@link
* NoOpMetricsRecorder}.
*/
static MetricsRecorder getInstance(@Nonnull DatastoreOpenTelemetryOptions options) {
boolean isMetricsEnabled = options.isMetricsEnabled();

if (isMetricsEnabled) {
OpenTelemetry openTelemetry = options.getOpenTelemetry();
if (openTelemetry == null) {
return new OpenTelemetryMetricsRecorder(GlobalOpenTelemetry.get());
}
return new OpenTelemetryMetricsRecorder(openTelemetry);
} else {
return new NoOpMetricsRecorder();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.datastore.telemetry;

import java.util.Map;

/**
* Metrics recorder implementation, used to stub out metrics instrumentation when metrics are
* disabled.
*/
class NoOpMetricsRecorder implements MetricsRecorder {

@Override
public void recordTransactionLatency(double latencyMs, Map<String, String> attributes) {
/* No-Op OTel Operation */
}

@Override
public void recordTransactionAttemptCount(long count, Map<String, String> attributes) {
/* No-Op OTel Operation */
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.datastore.telemetry;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
import java.util.Map;
import javax.annotation.Nonnull;

/**
* OpenTelemetry metrics recorder implementation, used to record metrics when metrics are enabled.
*/
class OpenTelemetryMetricsRecorder implements MetricsRecorder {
private final OpenTelemetry openTelemetry;

private final DoubleHistogram transactionLatency;
private final LongCounter transactionAttemptCount;

OpenTelemetryMetricsRecorder(@Nonnull OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;

Meter meter = openTelemetry.getMeter(TelemetryConstants.METER_NAME);

this.transactionLatency =
meter
.histogramBuilder(TelemetryConstants.SERVICE_NAME + "/transaction_latency")
Comment thread
jinseopkim0 marked this conversation as resolved.
.setDescription("Total latency for successful transaction operations")
.setUnit("ms")
.build();

this.transactionAttemptCount =
meter
.counterBuilder(TelemetryConstants.SERVICE_NAME + "/transaction_attempt_count")
.setDescription("Number of attempts to commit a transaction")
.build();
}

OpenTelemetry getOpenTelemetry() {
return openTelemetry;
}

@Override
public void recordTransactionLatency(double latencyMs, Map<String, String> attributes) {
transactionLatency.record(latencyMs, toOtelAttributes(attributes));
}

@Override
public void recordTransactionAttemptCount(long count, Map<String, String> attributes) {
transactionAttemptCount.add(count, toOtelAttributes(attributes));
}

private static Attributes toOtelAttributes(Map<String, String> attributes) {
AttributesBuilder builder = Attributes.builder();
if (attributes != null) {
attributes.forEach(builder::put);
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.datastore.telemetry;

import com.google.api.core.InternalApi;

/** Internal telemetry constants shared between OpenTelemetry tracing and metrics. */
@InternalApi
public class TelemetryConstants {
static final String SERVICE_NAME = "datastore.googleapis.com";
static final String METER_NAME = "com.google.cloud.datastore";

public static final String ATTRIBUTES_KEY_DOCUMENT_COUNT = "doc_count";
public static final String ATTRIBUTES_KEY_TRANSACTIONAL = "transactional";
public static final String ATTRIBUTES_KEY_TRANSACTION_ID = "transaction_id";
public static final String ATTRIBUTES_KEY_READ_CONSISTENCY = "read_consistency";
public static final String ATTRIBUTES_KEY_RECEIVED = "Received";
public static final String ATTRIBUTES_KEY_MISSING = "Missing";
public static final String ATTRIBUTES_KEY_DEFERRED = "Deferred";
public static final String ATTRIBUTES_KEY_MORE_RESULTS = "more_results";

/* TODO(lawrenceqiu): For now, these are a duplicate of method names in TraceUtil. Those will use these eventually */
// Format is not SnakeCase to keep backward compatibility with the existing values TraceUtil spans
public static final String METHOD_ALLOCATE_IDS = "AllocateIds";
Comment thread
lqiu96 marked this conversation as resolved.
public static final String METHOD_BEGIN_TRANSACTION = "Transaction.Begin";
public static final String METHOD_COMMIT = "Commit";
public static final String METHOD_LOOKUP = "Lookup";
public static final String METHOD_RESERVE_IDS = "ReserveIds";
public static final String METHOD_RUN_QUERY = "RunQuery";
public static final String METHOD_TRANSACTION_COMMIT = "Transaction.Commit";
public static final String METHOD_TRANSACTION_LOOKUP = "Transaction.Lookup";
public static final String METHOD_TRANSACTION_RUN = "Transaction.Run";
public static final String METHOD_TRANSACTION_RUN_QUERY = "Transaction.RunQuery";
public static final String METHOD_TRANSACTION_ROLLBACK = "Transaction.Rollback";
public static final String METHOD_TRANSACTION_RUN_AGGREGATION_QUERY =
"Transaction.RunAggregationQuery";
public static final String METHOD_ADD = "add";
public static final String METHOD_PUT = "put";
public static final String METHOD_UPDATE = "update";
public static final String METHOD_DELETE = "delete";
public static final String METHOD_SUBMIT = "submit";

private TelemetryConstants() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,6 @@ public interface TraceUtil {
String SPAN_NAME_TRANSACTION_RUN_QUERY = "Transaction.RunQuery";
String SPAN_NAME_ROLLBACK = "Transaction.Rollback";
String SPAN_NAME_TRANSACTION_RUN_AGGREGATION_QUERY = "Transaction.RunAggregationQuery";
String ATTRIBUTES_KEY_DOCUMENT_COUNT = "doc_count";
String ATTRIBUTES_KEY_TRANSACTIONAL = "transactional";
String ATTRIBUTES_KEY_TRANSACTION_ID = "transaction_id";
String ATTRIBUTES_KEY_READ_CONSISTENCY = "read_consistency";
String ATTRIBUTES_KEY_RECEIVED = "Received";
String ATTRIBUTES_KEY_MISSING = "Missing";
String ATTRIBUTES_KEY_DEFERRED = "Deferred";
String ATTRIBUTES_KEY_MORE_RESULTS = "more_results";

/**
* Creates and returns an instance of the TraceUtil class.
Expand Down
Loading
Loading