Skip to content

Commit

Permalink
chore: client metrics (#3125)
Browse files Browse the repository at this point in the history
* feat: client metrics

* Review comments

* Few issues and code optimisations

* review comments

* directpath_used attribute

* removed directpath enabled attribute

* removed directpath enabled attribute

* lint

* bucket boundaries
  • Loading branch information
surbhigarg92 authored and lqiu96 committed Sep 19, 2024
1 parent 163782b commit 9c47ea4
Show file tree
Hide file tree
Showing 11 changed files with 844 additions and 22 deletions.
9 changes: 8 additions & 1 deletion google-cloud-spanner/clirr-ignored-differences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,13 @@
<method>boolean isEnableApiTracing()</method>
</difference>

<!-- Added Built In Metrics option -->
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/SpannerOptions$SpannerEnvironment</className>
<method>boolean isEnableBuiltInMetrics()</method>
</difference>

<!-- Added ExcludeTxnFromChangeStreams -->
<difference>
<differenceType>7012</differenceType>
Expand Down Expand Up @@ -725,7 +732,7 @@
<className>com/google/cloud/spanner/SessionPoolOptions$Builder</className>
<method>com.google.cloud.spanner.SessionPoolOptions$Builder setUseMultiplexedSession(boolean)</method>
</difference>

<!-- Added reset() method -->
<difference>
<differenceType>7012</differenceType>
Expand Down
15 changes: 9 additions & 6 deletions google-cloud-spanner/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-context</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-common</artifactId>
Expand All @@ -254,6 +258,10 @@
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-metrics</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud.opentelemetry</groupId>
<artifactId>detector-resources-support</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-monitoring</artifactId>
Expand Down Expand Up @@ -437,11 +445,6 @@
<scope>test</scope>
</dependency>
<!-- OpenTelemetry test dependencies -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-trace</artifactId>
Expand Down Expand Up @@ -610,4 +613,4 @@
</dependencies>
</profile>
</profiles>
</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@

package com.google.cloud.spanner;

import com.google.api.core.InternalApi;
import com.google.api.gax.tracing.OpenTelemetryMetricsRecorder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.sdk.metrics.Aggregation;
import io.opentelemetry.sdk.metrics.InstrumentSelector;
import io.opentelemetry.sdk.metrics.InstrumentType;
import io.opentelemetry.sdk.metrics.View;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@InternalApi
public class BuiltInMetricsConstant {

public static final String METER_NAME = "spanner.googleapis.com/internal/client";

public static final String GAX_METER_NAME = "gax-java";
public static final String GAX_METER_NAME = OpenTelemetryMetricsRecorder.GAX_METER_NAME;

static final String OPERATION_LATENCIES_NAME = "operation_latencies";
static final String ATTEMPT_LATENCIES_NAME = "attempt_latencies";
Expand Down Expand Up @@ -66,6 +76,10 @@ public class BuiltInMetricsConstant {
public static final AttributeKey<String> DIRECT_PATH_USED_KEY =
AttributeKey.stringKey("directpath_used");

// IP address prefixes allocated for DirectPath backends.
public static final String DP_IPV6_PREFIX = "2001:4860:8040";
public static final String DP_IPV4_PREFIX = "34.126";

public static final Set<AttributeKey> COMMON_ATTRIBUTES =
ImmutableSet.of(
PROJECT_ID_KEY,
Expand All @@ -79,4 +93,73 @@ public class BuiltInMetricsConstant {
CLIENT_NAME_KEY,
DIRECT_PATH_ENABLED_KEY,
DIRECT_PATH_USED_KEY);

static Aggregation AGGREGATION_WITH_MILLIS_HISTOGRAM =
Aggregation.explicitBucketHistogram(
ImmutableList.of(
0.0, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0,
15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 25.0, 30.0, 40.0, 50.0, 65.0, 80.0, 100.0, 130.0,
160.0, 200.0, 250.0, 300.0, 400.0, 500.0, 650.0, 800.0, 1000.0, 2000.0, 5000.0,
10000.0, 20000.0, 50000.0, 100000.0, 200000.0, 400000.0, 800000.0, 1600000.0,
3200000.0));

static Map<InstrumentSelector, View> getAllViews() {
ImmutableMap.Builder<InstrumentSelector, View> views = ImmutableMap.builder();
defineView(
views,
BuiltInMetricsConstant.OPERATION_LATENCY_NAME,
BuiltInMetricsConstant.OPERATION_LATENCIES_NAME,
BuiltInMetricsConstant.AGGREGATION_WITH_MILLIS_HISTOGRAM,
InstrumentType.HISTOGRAM,
"ms");
defineView(
views,
BuiltInMetricsConstant.ATTEMPT_LATENCY_NAME,
BuiltInMetricsConstant.ATTEMPT_LATENCIES_NAME,
BuiltInMetricsConstant.AGGREGATION_WITH_MILLIS_HISTOGRAM,
InstrumentType.HISTOGRAM,
"ms");
defineView(
views,
BuiltInMetricsConstant.OPERATION_COUNT_NAME,
BuiltInMetricsConstant.OPERATION_COUNT_NAME,
Aggregation.sum(),
InstrumentType.COUNTER,
"1");
defineView(
views,
BuiltInMetricsConstant.ATTEMPT_COUNT_NAME,
BuiltInMetricsConstant.ATTEMPT_COUNT_NAME,
Aggregation.sum(),
InstrumentType.COUNTER,
"1");
return views.build();
}

private static void defineView(
ImmutableMap.Builder<InstrumentSelector, View> viewMap,
String metricName,
String metricViewName,
Aggregation aggregation,
InstrumentType type,
String unit) {
InstrumentSelector selector =
InstrumentSelector.builder()
.setName(BuiltInMetricsConstant.METER_NAME + '/' + metricName)
.setMeterName(BuiltInMetricsConstant.GAX_METER_NAME)
.setType(type)
.setUnit(unit)
.build();
Set<String> attributesFilter =
BuiltInMetricsConstant.COMMON_ATTRIBUTES.stream()
.map(AttributeKey::getKey)
.collect(Collectors.toSet());
View view =
View.builder()
.setName(BuiltInMetricsConstant.METER_NAME + '/' + metricViewName)
.setAggregation(aggregation)
.setAttributeFilter(attributesFilter)
.build();
viewMap.put(selector, view);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright 2024 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.spanner;

import static com.google.cloud.opentelemetry.detection.GCPPlatformDetector.SupportedPlatform.GOOGLE_KUBERNETES_ENGINE;
import static com.google.cloud.spanner.BuiltInMetricsConstant.CLIENT_NAME_KEY;
import static com.google.cloud.spanner.BuiltInMetricsConstant.CLIENT_UID_KEY;
import static com.google.cloud.spanner.BuiltInMetricsConstant.INSTANCE_CONFIG_ID_KEY;
import static com.google.cloud.spanner.BuiltInMetricsConstant.LOCATION_ID_KEY;
import static com.google.cloud.spanner.BuiltInMetricsConstant.PROJECT_ID_KEY;

import com.google.auth.Credentials;
import com.google.cloud.opentelemetry.detection.AttributeKeys;
import com.google.cloud.opentelemetry.detection.DetectedPlatform;
import com.google.cloud.opentelemetry.detection.GCPPlatformDetector;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;

final class BuiltInOpenTelemetryMetricsProvider {

static BuiltInOpenTelemetryMetricsProvider INSTANCE = new BuiltInOpenTelemetryMetricsProvider();

private static final Logger logger =
Logger.getLogger(BuiltInOpenTelemetryMetricsProvider.class.getName());

private static String taskId;

private OpenTelemetry openTelemetry;

private BuiltInOpenTelemetryMetricsProvider() {}

OpenTelemetry getOrCreateOpenTelemetry(String projectId, @Nullable Credentials credentials) {
try {
if (this.openTelemetry == null) {
SdkMeterProviderBuilder sdkMeterProviderBuilder = SdkMeterProvider.builder();
BuiltInOpenTelemetryMetricsView.registerBuiltinMetrics(
SpannerCloudMonitoringExporter.create(projectId, credentials), sdkMeterProviderBuilder);
this.openTelemetry =
OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProviderBuilder.build()).build();
}
return this.openTelemetry;
} catch (IOException ex) {
logger.log(
Level.WARNING,
"Unable to get OpenTelemetry object for client side metrics, will skip exporting client side metrics",
ex);
return null;
}
}

Map<String, String> createClientAttributes(String projectId, String client_name) {
Map<String, String> clientAttributes = new HashMap<>();
clientAttributes.put(LOCATION_ID_KEY.getKey(), detectClientLocation());
clientAttributes.put(PROJECT_ID_KEY.getKey(), projectId);
// TODO: Replace this with real value.
clientAttributes.put(INSTANCE_CONFIG_ID_KEY.getKey(), "unknown");
clientAttributes.put(CLIENT_NAME_KEY.getKey(), client_name);
clientAttributes.put(CLIENT_UID_KEY.getKey(), getDefaultTaskValue());
return clientAttributes;
}

static String detectClientLocation() {
GCPPlatformDetector detector = GCPPlatformDetector.DEFAULT_INSTANCE;
DetectedPlatform detectedPlatform = detector.detectPlatform();
// All platform except GKE uses "cloud_region" for region attribute.
String region = detectedPlatform.getAttributes().get("cloud_region");
if (detectedPlatform.getSupportedPlatform() == GOOGLE_KUBERNETES_ENGINE) {
region = detectedPlatform.getAttributes().get(AttributeKeys.GKE_LOCATION_TYPE_REGION);
}
return region == null ? "global" : region;
}

/**
* Generates a unique identifier for the Client_uid metric field. The identifier is composed of a
* UUID, the process ID (PID), and the hostname.
*
* <p>For Java 9 and later, the PID is obtained using the ProcessHandle API. For Java 8, the PID
* is extracted from ManagementFactory.getRuntimeMXBean().getName().
*
* @return A unique identifier string in the format UUID@PID@hostname
*/
private static String getDefaultTaskValue() {
if (taskId == null) {
String identifier = UUID.randomUUID().toString();
String pid = getProcessId();

try {
String hostname = InetAddress.getLocalHost().getHostName();
taskId = identifier + "@" + pid + "@" + hostname;
} catch (UnknownHostException e) {
logger.log(Level.INFO, "Unable to get the hostname.", e);
taskId = identifier + "@" + pid + "@localhost";
}
}
return taskId;
}

private static String getProcessId() {
try {
// Check if Java 9+ and ProcessHandle class is available
Class<?> processHandleClass = Class.forName("java.lang.ProcessHandle");
Method currentMethod = processHandleClass.getMethod("current");
Object processHandleInstance = currentMethod.invoke(null);
Method pidMethod = processHandleClass.getMethod("pid");
long pid = (long) pidMethod.invoke(processHandleInstance);
return Long.toString(pid);
} catch (Exception e) {
// Fallback to Java 8 method
final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
if (jvmName != null && jvmName.contains("@")) {
return jvmName.split("@")[0];
} else {
return "unknown";
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2024 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.spanner;

import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
import io.opentelemetry.sdk.metrics.export.MetricExporter;
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;

class BuiltInOpenTelemetryMetricsView {

private BuiltInOpenTelemetryMetricsView() {}

/** Register built-in metrics on the {@link SdkMeterProviderBuilder} with credentials. */
static void registerBuiltinMetrics(
MetricExporter metricExporter, SdkMeterProviderBuilder builder) {
BuiltInMetricsConstant.getAllViews().forEach(builder::registerView);
builder.registerMetricReader(PeriodicMetricReader.create(metricExporter));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.threeten.bp.Duration;

@InternalApi
Expand Down Expand Up @@ -177,5 +178,14 @@ public void addAttributes(String key, String value) {
metricsTracer.addAttributes(key, value);
}
}
};
}

public void addAttributes(Map<String, String> attributes) {
for (ApiTracer child : children) {
if (child instanceof MetricsTracer) {
MetricsTracer metricsTracer = (MetricsTracer) child;
attributes.forEach((key, value) -> metricsTracer.addAttributes(key, value));
}
}
}
}
Loading

0 comments on commit 9c47ea4

Please sign in to comment.