Skip to content

Commit

Permalink
Add support for createServiceTimeSeries (#318)
Browse files Browse the repository at this point in the history
* Update export logic for createServiceTimeSeries

* Add test case for use with CreateServiceTimeSeries

* Make serviceTimeSeries builder config public

* Update configuration test

* Switch functional interface to Consumer
  • Loading branch information
psx95 authored Apr 3, 2024
1 parent af5e104 commit 4d1da47
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,24 @@ public interface CloudMetricClient {
MetricDescriptor createMetricDescriptor(CreateMetricDescriptorRequest request);

/**
* Send a timeseries to Cloud Monitoring.
* Send a time series to Cloud Monitoring.
*
* @param name The name of the project where we write the timeseries.
* @param timeSeries The list of timeseries to write.
* <p>Note: This can only take one point at per timeseries.
* @param name The name of the project where we write the time series.
* @param timeSeries The list of time series to write.
* <p>Note: This can only take one point at per time series.
*/
void createTimeSeries(ProjectName name, List<TimeSeries> timeSeries);

/**
* Send a service time series to Cloud Monitoring. A service time series is a time series for a
* metric from a Google Cloud service. This method should not be used for sending custom metrics.
*
* @param name The name of the project where we write the time series.
* @param timeSeries The list of time series to write.
* <p>Note: This can only take one point at per time series.
*/
void createServiceTimeSeries(ProjectName name, List<TimeSeries> timeSeries);

/** Shutdown this client, cleaning up any resources. */
void shutdown();
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public void createTimeSeries(ProjectName name, List<TimeSeries> timeSeries) {
this.metricServiceClient.createTimeSeries(name, timeSeries);
}

@Override
public void createServiceTimeSeries(ProjectName name, List<TimeSeries> timeSeries) {
this.metricServiceClient.createServiceTimeSeries(name, timeSeries);
}

@Override
public void shutdown() {
this.metricServiceClient.shutdown();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
Expand All @@ -66,18 +67,21 @@ class InternalMetricExporter implements MetricExporter {
private final String prefix;
private final MetricDescriptorStrategy metricDescriptorStrategy;
private final Predicate<AttributeKey<?>> resourceAttributesFilter;
private final boolean useCreateServiceTimeSeries;

InternalMetricExporter(
String projectId,
String prefix,
CloudMetricClient client,
MetricDescriptorStrategy descriptorStrategy,
Predicate<AttributeKey<?>> resourceAttributesFilter) {
Predicate<AttributeKey<?>> resourceAttributesFilter,
boolean useCreateServiceTimeSeries) {
this.projectId = projectId;
this.prefix = prefix;
this.metricServiceClient = client;
this.metricDescriptorStrategy = descriptorStrategy;
this.resourceAttributesFilter = resourceAttributesFilter;
this.useCreateServiceTimeSeries = useCreateServiceTimeSeries;
}

static InternalMetricExporter createWithConfiguration(MetricConfiguration configuration)
Expand Down Expand Up @@ -115,7 +119,8 @@ static InternalMetricExporter createWithConfiguration(MetricConfiguration config
prefix,
new CloudMetricClientImpl(MetricServiceClient.create(builder.build())),
configuration.getDescriptorStrategy(),
configuration.getResourceAttributesFilter());
configuration.getResourceAttributesFilter(),
configuration.getUseServiceTimeSeries());
}

@VisibleForTesting
Expand All @@ -124,9 +129,15 @@ static InternalMetricExporter createWithClient(
String prefix,
CloudMetricClient metricServiceClient,
MetricDescriptorStrategy descriptorStrategy,
Predicate<AttributeKey<?>> resourceAttributesFilter) {
Predicate<AttributeKey<?>> resourceAttributesFilter,
boolean useCreateServiceTimeSeries) {
return new InternalMetricExporter(
projectId, prefix, metricServiceClient, descriptorStrategy, resourceAttributesFilter);
projectId,
prefix,
metricServiceClient,
descriptorStrategy,
resourceAttributesFilter,
useCreateServiceTimeSeries);
}

private void exportDescriptor(MetricDescriptor descriptor) {
Expand Down Expand Up @@ -197,32 +208,46 @@ public CompletableResultCode export(Collection<MetricData> metrics) {
// }
}
// Update metric descriptors based on configured strategy.
try {
Collection<MetricDescriptor> descriptors = builder.getDescriptors();
if (!descriptors.isEmpty()) {
metricDescriptorStrategy.exportDescriptors(descriptors, this::exportDescriptor);
}
} catch (Exception e) {
logger.warn("Failed to create metric descriptors", e);
}
exportDescriptors(builder);

List<TimeSeries> series = builder.getTimeSeries();
createTimeSeriesBatch(metricServiceClient, ProjectName.of(projectId), series);
Consumer<List<TimeSeries>> timeSeriesGenerator =
timeSeries -> {
if (useCreateServiceTimeSeries) {
metricServiceClient.createServiceTimeSeries(ProjectName.of(projectId), timeSeries);
} else {
metricServiceClient.createTimeSeries(ProjectName.of(projectId), timeSeries);
}
};
createTimeSeriesBatch(series, timeSeriesGenerator);
// TODO: better error reporting.
if (series.size() < metrics.size()) {
return CompletableResultCode.ofFailure();
}
return CompletableResultCode.ofSuccess();
}

private void exportDescriptors(MetricTimeSeriesBuilder timeSeriesBuilder) {
if (useCreateServiceTimeSeries) {
// do not export metric descriptors when using createServiceTimeSeries
return;
}
try {
Collection<MetricDescriptor> descriptors = timeSeriesBuilder.getDescriptors();
if (!descriptors.isEmpty()) {
metricDescriptorStrategy.exportDescriptors(descriptors, this::exportDescriptor);
}
} catch (Exception e) {
logger.warn("Failed to create metric descriptors", e);
}
}

// Fragment metrics into batches and send to GCM.
private static void createTimeSeriesBatch(
CloudMetricClient metricServiceClient,
ProjectName projectName,
List<TimeSeries> allTimesSeries) {
private void createTimeSeriesBatch(
List<TimeSeries> allTimesSeries, Consumer<List<TimeSeries>> timeSeriesGenerator) {
List<List<TimeSeries>> batches = Lists.partition(allTimesSeries, MAX_BATCH_SIZE);
for (List<TimeSeries> timeSeries : batches) {
metricServiceClient.createTimeSeries(projectName, new ArrayList<>(timeSeries));
timeSeriesGenerator.accept(new ArrayList<>(timeSeries));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.semconv.ResourceAttributes;
import java.time.Duration;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -141,6 +142,15 @@ public final String getProjectId() {
*/
public abstract Predicate<AttributeKey<?>> getResourceAttributesFilter();

/**
* Returns a boolean indicating if the {@link MetricConfiguration} is configured to write to a
* metric generated from a Google Cloud Service.
*
* @return true if the {@link MetricConfiguration} is configured to write to a metric generated
* from a Google Cloud Service, false otherwise.
*/
public abstract boolean getUseServiceTimeSeries();

@VisibleForTesting
abstract boolean getInsecureEndpoint();

Expand All @@ -164,6 +174,7 @@ public static Builder builder() {
.setDeadline(DEFAULT_DEADLINE)
.setDescriptorStrategy(MetricDescriptorStrategy.SEND_ONCE)
.setInsecureEndpoint(false)
.setUseServiceTimeSeries(false)
.setResourceAttributesFilter(DEFAULT_RESOURCE_ATTRIBUTES_FILTER)
.setMetricServiceEndpoint(MetricServiceStubSettings.getDefaultEndpoint());
}
Expand Down Expand Up @@ -215,6 +226,18 @@ public final Builder setProjectId(String projectId) {
/** Sets the endpoint where to write Metrics. Defaults to monitoring.googleapis.com:443. */
public abstract Builder setMetricServiceEndpoint(String endpoint);

/**
* Sets the {@link MetricConfiguration} to configure the exporter to write metrics via {@link
* com.google.cloud.monitoring.v3.MetricServiceClient#createServiceTimeSeries(String, List)}
* method. By default, this is false.
*
* @param useServiceTimeSeries a boolean indicating whether to use {@link
* com.google.cloud.monitoring.v3.MetricServiceClient#createServiceTimeSeries(String, List)}
* method for writing metrics to Google Cloud Monitoring.
* @return this
*/
public abstract Builder setUseServiceTimeSeries(boolean useServiceTimeSeries);

/**
* Set a filter to determine which resource attributes to add to metrics as metric labels. By
* default, it adds service.name, service.namespace, and service.instance.id. This is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ public class FakeData {
ImmutableSumData.create(
true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint)));

static final MetricData googleComputeServiceMetricData =
ImmutableMetricData.createLongSum(
aGceResource,
anInstrumentationLibraryInfo,
"guest/disk/io_time",
"description",
"ns",
ImmutableSumData.create(
true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint)));

static final String aTraceId = "00000000000000000000000000000001";
static final String aSpanId = "0000000000000002";
static final SpanContext aSpanContext =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import static com.google.cloud.opentelemetry.metric.FakeData.aSpanId;
import static com.google.cloud.opentelemetry.metric.FakeData.aTraceId;
import static com.google.cloud.opentelemetry.metric.FakeData.anInstrumentationLibraryInfo;
import static com.google.cloud.opentelemetry.metric.FakeData.googleComputeServiceMetricData;
import static com.google.cloud.opentelemetry.metric.MetricConfiguration.DEFAULT_PREFIX;
import static com.google.cloud.opentelemetry.metric.MetricConfiguration.DEFAULT_RESOURCE_ATTRIBUTES_FILTER;
import static com.google.cloud.opentelemetry.metric.MetricConfiguration.NO_RESOURCE_ATTRIBUTES;
Expand Down Expand Up @@ -132,7 +133,8 @@ public void testExportSendsAllDescriptorsOnce() {
DEFAULT_PREFIX,
mockClient,
MetricDescriptorStrategy.SEND_ONCE,
DEFAULT_RESOURCE_ATTRIBUTES_FILTER);
DEFAULT_RESOURCE_ATTRIBUTES_FILTER,
false);
CompletableResultCode result = exporter.export(ImmutableList.of(aMetricData, aHistogram));
assertTrue(result.isSuccess());
CompletableResultCode result2 = exporter.export(ImmutableList.of(aMetricData, aHistogram));
Expand Down Expand Up @@ -241,7 +243,8 @@ public void testExportSucceeds() {
DEFAULT_PREFIX,
mockClient,
MetricDescriptorStrategy.ALWAYS_SEND,
DEFAULT_RESOURCE_ATTRIBUTES_FILTER);
DEFAULT_RESOURCE_ATTRIBUTES_FILTER,
false);

CompletableResultCode result = exporter.export(ImmutableList.of(aMetricData));
verify(mockClient, times(1)).createMetricDescriptor(metricDescriptorCaptor.capture());
Expand Down Expand Up @@ -362,7 +365,8 @@ public void testExportWithHistogram_Succeeds() {
DEFAULT_PREFIX,
mockClient,
MetricDescriptorStrategy.ALWAYS_SEND,
DEFAULT_RESOURCE_ATTRIBUTES_FILTER);
DEFAULT_RESOURCE_ATTRIBUTES_FILTER,
false);
CompletableResultCode result = exporter.export(ImmutableList.of(aHistogram));
verify(mockClient, times(1)).createMetricDescriptor(metricDescriptorCaptor.capture());
verify(mockClient, times(1))
Expand All @@ -382,7 +386,8 @@ public void testExportWithNonSupportedMetricTypeReturnsFailure() {
DEFAULT_PREFIX,
mockClient,
MetricDescriptorStrategy.ALWAYS_SEND,
NO_RESOURCE_ATTRIBUTES);
NO_RESOURCE_ATTRIBUTES,
false);

MetricData metricData =
ImmutableMetricData.createDoubleSummary(
Expand Down Expand Up @@ -452,6 +457,26 @@ public void verifyExporterCreationErrorDoesNotBreakMetricExporter() {
}
}

@Test
public void verifyExporterExportGoogleServiceMetrics() {
MetricExporter exporter =
InternalMetricExporter.createWithClient(
aProjectId,
"compute.googleapis.com",
mockClient,
MetricDescriptorStrategy.ALWAYS_SEND,
NO_RESOURCE_ATTRIBUTES,
true);

CompletableResultCode result =
exporter.export(ImmutableList.of(googleComputeServiceMetricData));
verify(mockClient, times(0)).createMetricDescriptor(any());
verify(mockClient, times(0)).createTimeSeries(any(ProjectName.class), any());
verify(mockClient, times(1)).createServiceTimeSeries(any(ProjectName.class), any());

assertTrue(result.isSuccess());
}

private void generateOpenTelemetryUsingGoogleCloudMetricExporter(MetricExporter metricExporter) {
SdkMeterProvider meterProvider =
SdkMeterProvider.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
package com.google.cloud.opentelemetry.metric;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import com.google.auth.Credentials;
import com.google.auth.oauth2.AccessToken;
Expand Down Expand Up @@ -48,6 +50,7 @@ public void testDefaultConfigurationSucceeds() {

assertNull(configuration.getCredentials());
assertEquals(PROJECT_ID, configuration.getProjectId());
assertFalse(configuration.getUseServiceTimeSeries());
}

@Test
Expand All @@ -58,11 +61,13 @@ public void testSetAllConfigurationFieldsSucceeds() {
.setProjectId(PROJECT_ID)
.setCredentials(FAKE_CREDENTIALS)
.setResourceAttributesFilter(allowAllPredicate)
.setUseServiceTimeSeries(true)
.build();

assertEquals(FAKE_CREDENTIALS, configuration.getCredentials());
assertEquals(PROJECT_ID, configuration.getProjectId());
assertEquals(allowAllPredicate, configuration.getResourceAttributesFilter());
assertTrue(configuration.getUseServiceTimeSeries());
}

@Test
Expand Down

0 comments on commit 4d1da47

Please sign in to comment.