diff --git a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/CloudMetricClient.java b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/CloudMetricClient.java index ecaa0e5b..4073c422 100644 --- a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/CloudMetricClient.java +++ b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/CloudMetricClient.java @@ -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. - *

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. + *

Note: This can only take one point at per time series. */ void createTimeSeries(ProjectName name, List 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. + *

Note: This can only take one point at per time series. + */ + void createServiceTimeSeries(ProjectName name, List timeSeries); + /** Shutdown this client, cleaning up any resources. */ void shutdown(); } diff --git a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/CloudMetricClientImpl.java b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/CloudMetricClientImpl.java index ef5c26f7..7f510409 100644 --- a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/CloudMetricClientImpl.java +++ b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/CloudMetricClientImpl.java @@ -40,6 +40,11 @@ public void createTimeSeries(ProjectName name, List timeSeries) { this.metricServiceClient.createTimeSeries(name, timeSeries); } + @Override + public void createServiceTimeSeries(ProjectName name, List timeSeries) { + this.metricServiceClient.createServiceTimeSeries(name, timeSeries); + } + @Override public void shutdown() { this.metricServiceClient.shutdown(); diff --git a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/InternalMetricExporter.java b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/InternalMetricExporter.java index 8c0c90a7..bbd65029 100644 --- a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/InternalMetricExporter.java +++ b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/InternalMetricExporter.java @@ -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; @@ -66,18 +67,21 @@ class InternalMetricExporter implements MetricExporter { private final String prefix; private final MetricDescriptorStrategy metricDescriptorStrategy; private final Predicate> resourceAttributesFilter; + private final boolean useCreateServiceTimeSeries; InternalMetricExporter( String projectId, String prefix, CloudMetricClient client, MetricDescriptorStrategy descriptorStrategy, - Predicate> resourceAttributesFilter) { + Predicate> 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) @@ -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 @@ -124,9 +129,15 @@ static InternalMetricExporter createWithClient( String prefix, CloudMetricClient metricServiceClient, MetricDescriptorStrategy descriptorStrategy, - Predicate> resourceAttributesFilter) { + Predicate> resourceAttributesFilter, + boolean useCreateServiceTimeSeries) { return new InternalMetricExporter( - projectId, prefix, metricServiceClient, descriptorStrategy, resourceAttributesFilter); + projectId, + prefix, + metricServiceClient, + descriptorStrategy, + resourceAttributesFilter, + useCreateServiceTimeSeries); } private void exportDescriptor(MetricDescriptor descriptor) { @@ -197,17 +208,18 @@ public CompletableResultCode export(Collection metrics) { // } } // Update metric descriptors based on configured strategy. - try { - Collection 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 series = builder.getTimeSeries(); - createTimeSeriesBatch(metricServiceClient, ProjectName.of(projectId), series); + Consumer> 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(); @@ -215,14 +227,27 @@ public CompletableResultCode export(Collection metrics) { return CompletableResultCode.ofSuccess(); } + private void exportDescriptors(MetricTimeSeriesBuilder timeSeriesBuilder) { + if (useCreateServiceTimeSeries) { + // do not export metric descriptors when using createServiceTimeSeries + return; + } + try { + Collection 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 allTimesSeries) { + private void createTimeSeriesBatch( + List allTimesSeries, Consumer> timeSeriesGenerator) { List> batches = Lists.partition(allTimesSeries, MAX_BATCH_SIZE); for (List timeSeries : batches) { - metricServiceClient.createTimeSeries(projectName, new ArrayList<>(timeSeries)); + timeSeriesGenerator.accept(new ArrayList<>(timeSeries)); } } diff --git a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MetricConfiguration.java b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MetricConfiguration.java index f073253f..fd525c16 100644 --- a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MetricConfiguration.java +++ b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MetricConfiguration.java @@ -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; @@ -141,6 +142,15 @@ public final String getProjectId() { */ public abstract Predicate> 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(); @@ -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()); } @@ -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 diff --git a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/FakeData.java b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/FakeData.java index dbdbd9ff..44b99600 100644 --- a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/FakeData.java +++ b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/FakeData.java @@ -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 = diff --git a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/GoogleCloudMetricExporterTest.java b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/GoogleCloudMetricExporterTest.java index cb5a30e1..9484d4c0 100644 --- a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/GoogleCloudMetricExporterTest.java +++ b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/GoogleCloudMetricExporterTest.java @@ -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; @@ -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)); @@ -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()); @@ -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)) @@ -382,7 +386,8 @@ public void testExportWithNonSupportedMetricTypeReturnsFailure() { DEFAULT_PREFIX, mockClient, MetricDescriptorStrategy.ALWAYS_SEND, - NO_RESOURCE_ATTRIBUTES); + NO_RESOURCE_ATTRIBUTES, + false); MetricData metricData = ImmutableMetricData.createDoubleSummary( @@ -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() diff --git a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/MetricConfigurationTest.java b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/MetricConfigurationTest.java index dcef7eca..ab8119f2 100644 --- a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/MetricConfigurationTest.java +++ b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/MetricConfigurationTest.java @@ -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; @@ -48,6 +50,7 @@ public void testDefaultConfigurationSucceeds() { assertNull(configuration.getCredentials()); assertEquals(PROJECT_ID, configuration.getProjectId()); + assertFalse(configuration.getUseServiceTimeSeries()); } @Test @@ -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