diff --git a/java/google/registry/monitoring/metrics/stackdriver/BUILD b/java/google/registry/monitoring/metrics/stackdriver/BUILD new file mode 100644 index 000000000..dca3fbe57 --- /dev/null +++ b/java/google/registry/monitoring/metrics/stackdriver/BUILD @@ -0,0 +1,24 @@ +package( + default_visibility = ["//visibility:public"], +) + +licenses(["notice"]) # Apache 2.0 + +java_library( + name = "stackdriver", + srcs = glob(["*.java"]), + deps = [ + "//java/google/registry/monitoring/metrics", + "@com_google_api_client", + "@com_google_apis_google_api_services_monitoring", + "@com_google_appengine_api_1_0_sdk", + "@com_google_auto_value", + "@com_google_code_findbugs_jsr305", + "@com_google_dagger", + "@com_google_errorprone_error_prone_annotations", + "@com_google_guava", + "@com_google_http_client", + "@com_google_re2j", + "@joda_time", + ], +) diff --git a/java/google/registry/monitoring/metrics/stackdriver/StackdriverWriter.java b/java/google/registry/monitoring/metrics/stackdriver/StackdriverWriter.java new file mode 100644 index 000000000..b8908cf04 --- /dev/null +++ b/java/google/registry/monitoring/metrics/stackdriver/StackdriverWriter.java @@ -0,0 +1,408 @@ +// Copyright 2016 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.monitoring.metrics.stackdriver; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.services.monitoring.v3.Monitoring; +import com.google.api.services.monitoring.v3.model.BucketOptions; +import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest; +import com.google.api.services.monitoring.v3.model.Distribution; +import com.google.api.services.monitoring.v3.model.Explicit; +import com.google.api.services.monitoring.v3.model.Exponential; +import com.google.api.services.monitoring.v3.model.LabelDescriptor; +import com.google.api.services.monitoring.v3.model.Linear; +import com.google.api.services.monitoring.v3.model.Metric; +import com.google.api.services.monitoring.v3.model.MetricDescriptor; +import com.google.api.services.monitoring.v3.model.MonitoredResource; +import com.google.api.services.monitoring.v3.model.Point; +import com.google.api.services.monitoring.v3.model.TimeInterval; +import com.google.api.services.monitoring.v3.model.TimeSeries; +import com.google.api.services.monitoring.v3.model.TypedValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.RateLimiter; +import google.registry.monitoring.metrics.CustomFitter; +import google.registry.monitoring.metrics.DistributionFitter; +import google.registry.monitoring.metrics.ExponentialFitter; +import google.registry.monitoring.metrics.IncrementableMetric; +import google.registry.monitoring.metrics.LinearFitter; +import google.registry.monitoring.metrics.MetricPoint; +import google.registry.monitoring.metrics.MetricRegistryImpl; +import google.registry.monitoring.metrics.MetricSchema.Kind; +import google.registry.monitoring.metrics.MetricWriter; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Queue; +import java.util.logging.Logger; +import javax.annotation.concurrent.NotThreadSafe; +import javax.inject.Inject; +import javax.inject.Named; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +/** + * Metrics writer for Google Cloud Monitoring V3 + * + *

This class communicates with the API via HTTP. In order to increase throughput and minimize + * CPU, it buffers points to be written until it has {@code maxPointsPerRequest} points buffered or + * until {@link #flush()} is called. + * + * @see Introduction to the Stackdriver + * Monitoring API + */ +// TODO(shikhman): add retry logic +@NotThreadSafe +public class StackdriverWriter implements MetricWriter { + + /** + * A counter representing the total number of points pushed. Has {@link Kind} and metric value + * types as labels. + */ + private static final IncrementableMetric pushedPoints = + MetricRegistryImpl.getDefault() + .newIncrementableMetric( + "/metrics/stackdriver/points_pushed", + "Count of points pushed to Stackdriver Monitoring API.", + "Points Pushed", + ImmutableSet.of( + google.registry.monitoring.metrics.LabelDescriptor.create("kind", "Metric Kind"), + google.registry.monitoring.metrics.LabelDescriptor.create( + "valueType", "Metric Value Type"))); + private static final String METRIC_DOMAIN = "custom.googleapis.com"; + private static final String LABEL_VALUE_TYPE = "STRING"; + private static final DateTimeFormatter DATETIME_FORMATTER = ISODateTimeFormat.dateTime(); + private static final Logger logger = Logger.getLogger(StackdriverWriter.class.getName()); + // A map of native type to the equivalent Stackdriver metric type. + private static final ImmutableMap, String> ENCODED_METRIC_TYPES = + new ImmutableMap.Builder, String>() + .put(Long.class, "INT64") + .put(Double.class, "DOUBLE") + .put(Boolean.class, "BOOL") + .put(String.class, "STRING") + .put(google.registry.monitoring.metrics.Distribution.class, "DISTRIBUTION") + .build(); + // A map of native kind to the equivalent Stackdriver metric kind. + private static final ImmutableMap ENCODED_METRIC_KINDS = + new ImmutableMap.Builder() + .put(Kind.GAUGE.name(), "GAUGE") + .put(Kind.CUMULATIVE.name(), "CUMULATIVE") + .build(); + private static final String FLUSH_OVERFLOW_ERROR = "Cannot flush more than 200 points at a time"; + private static final String METRIC_KIND_ERROR = + "Unrecognized metric kind, must be one of " + + Joiner.on(",").join(ENCODED_METRIC_KINDS.keySet()); + private static final String METRIC_TYPE_ERROR = + "Unrecognized metric type, must be one of " + + Joiner.on(" ").join(ENCODED_METRIC_TYPES.keySet()); + private static final String METRIC_LABEL_COUNT_ERROR = + "Metric label value count does not match its MetricDescriptor's label count"; + + private final MonitoredResource monitoredResource; + private final Queue timeSeriesBuffer; + /** + * A local cache of MetricDescriptors. A metric's metadata (name, kind, type, label definitions) + * must be registered before points for the metric can be pushed. + */ + private final HashMap, MetricDescriptor> + registeredDescriptors = new HashMap<>(); + private final String projectResource; + private final Monitoring monitoringClient; + private final int maxPointsPerRequest; + private final RateLimiter rateLimiter; + + /** + * Constructs a {@link StackdriverWriter}. + * + *

The monitoringClient must have read and write permissions to the Cloud Monitoring API v3 on + * the provided project. + */ + @Inject + public StackdriverWriter( + Monitoring monitoringClient, + @Named("stackdriverGcpProject") String project, + MonitoredResource monitoredResource, + @Named("stackdriverMaxQps") int maxQps, + @Named("stackdriverMaxPointsPerRequest") int maxPointsPerRequest) { + this.monitoringClient = checkNotNull(monitoringClient); + this.projectResource = "projects/" + checkNotNull(project); + this.monitoredResource = monitoredResource; + this.maxPointsPerRequest = maxPointsPerRequest; + this.timeSeriesBuffer = new ArrayDeque<>(maxPointsPerRequest); + this.rateLimiter = RateLimiter.create(maxQps); + } + + @VisibleForTesting + static ImmutableList encodeLabelDescriptors( + ImmutableSet labelDescriptors) { + List stackDriverLabelDescriptors = new ArrayList<>(labelDescriptors.size()); + + for (google.registry.monitoring.metrics.LabelDescriptor labelDescriptor : labelDescriptors) { + stackDriverLabelDescriptors.add( + new LabelDescriptor() + .setKey(labelDescriptor.name()) + .setDescription(labelDescriptor.description()) + .setValueType(LABEL_VALUE_TYPE)); + } + + return ImmutableList.copyOf(stackDriverLabelDescriptors); + } + + @VisibleForTesting + static MetricDescriptor encodeMetricDescriptor( + google.registry.monitoring.metrics.Metric metric) { + return new MetricDescriptor() + .setType(METRIC_DOMAIN + metric.getMetricSchema().name()) + .setDescription(metric.getMetricSchema().description()) + .setDisplayName(metric.getMetricSchema().valueDisplayName()) + .setValueType(ENCODED_METRIC_TYPES.get(metric.getValueClass())) + .setLabels(encodeLabelDescriptors(metric.getMetricSchema().labels())) + .setMetricKind(ENCODED_METRIC_KINDS.get(metric.getMetricSchema().kind().name())); + } + + /** Encodes and writes a metric point to Stackdriver. The point may be buffered. */ + @Override + public void write(google.registry.monitoring.metrics.MetricPoint point) + throws IOException { + checkNotNull(point); + + TimeSeries timeSeries = getEncodedTimeSeries(point); + timeSeriesBuffer.add(timeSeries); + + logger.fine(String.format("Enqueued metric %s for writing", timeSeries.getMetric().getType())); + if (timeSeriesBuffer.size() == maxPointsPerRequest) { + flush(); + } + } + + /** Flushes all buffered metric points to Stackdriver. This call is blocking. */ + @Override + public void flush() throws IOException { + checkState(timeSeriesBuffer.size() <= 200, FLUSH_OVERFLOW_ERROR); + + // Return early; Stackdriver throws errors if we attempt to send empty requests. + if (timeSeriesBuffer.isEmpty()) { + logger.fine("Attempted to flush with no pending points, doing nothing"); + return; + } + + ImmutableList timeSeriesList = ImmutableList.copyOf(timeSeriesBuffer); + timeSeriesBuffer.clear(); + + CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(timeSeriesList); + + rateLimiter.acquire(); + monitoringClient.projects().timeSeries().create(projectResource, request).execute(); + + for (TimeSeries timeSeries : timeSeriesList) { + pushedPoints.increment(timeSeries.getMetricKind(), timeSeries.getValueType()); + } + logger.info(String.format("Flushed %d metrics to Stackdriver", timeSeriesList.size())); + } + + /** + * Registers a metric's {@link MetricDescriptor} with the Monitoring API. + * + * @param metric the metric to be registered. + * @see Stackdriver MetricDescriptor API + */ + @VisibleForTesting + MetricDescriptor registerMetric(final google.registry.monitoring.metrics.Metric metric) + throws IOException { + if (registeredDescriptors.containsKey(metric)) { + logger.fine( + String.format("Using existing metric descriptor %s", metric.getMetricSchema().name())); + return registeredDescriptors.get(metric); + } + + MetricDescriptor descriptor = encodeMetricDescriptor(metric); + + rateLimiter.acquire(); + // We try to create a descriptor, but it may have been created already, so we re-fetch it on + // failure + try { + descriptor = + monitoringClient + .projects() + .metricDescriptors() + .create(projectResource, descriptor) + .execute(); + logger.info(String.format("Registered new metric descriptor %s", descriptor.getType())); + } catch (GoogleJsonResponseException jsonException) { + // Not the error we were expecting, just give up + if (!"ALREADY_EXISTS".equals(jsonException.getStatusMessage())) { + throw jsonException; + } + + descriptor = + monitoringClient + .projects() + .metricDescriptors() + .get(projectResource + "/metricDescriptors/" + descriptor.getType()) + .execute(); + + logger.info( + String.format("Fetched existing metric descriptor %s", metric.getMetricSchema().name())); + } + + registeredDescriptors.put(metric, descriptor); + + return descriptor; + } + + private static TimeInterval encodeTimeInterval(Interval nativeInterval, Kind metricKind) { + + TimeInterval encodedInterval = + new TimeInterval().setStartTime(DATETIME_FORMATTER.print(nativeInterval.getStart())); + + DateTime endTimestamp = + nativeInterval.toDurationMillis() == 0 && metricKind != Kind.GAUGE + ? nativeInterval.getEnd().plusMillis(1) + : nativeInterval.getEnd(); + + return encodedInterval.setEndTime(DATETIME_FORMATTER.print(endTimestamp)); + } + + private static BucketOptions encodeBucketOptions(DistributionFitter fitter) { + BucketOptions bucketOptions = new BucketOptions(); + + if (fitter instanceof LinearFitter) { + LinearFitter linearFitter = (LinearFitter) fitter; + + bucketOptions.setLinearBuckets( + new Linear() + .setNumFiniteBuckets(linearFitter.boundaries().size() - 1) + .setWidth(linearFitter.width()) + .setOffset(linearFitter.offset())); + } else if (fitter instanceof ExponentialFitter) { + ExponentialFitter exponentialFitter = (ExponentialFitter) fitter; + + bucketOptions.setExponentialBuckets( + new Exponential() + .setNumFiniteBuckets(exponentialFitter.boundaries().size() - 1) + .setGrowthFactor(exponentialFitter.base()) + .setScale(exponentialFitter.scale())); + } else if (fitter instanceof CustomFitter) { + bucketOptions.setExplicitBuckets(new Explicit().setBounds(fitter.boundaries().asList())); + } else { + throw new IllegalArgumentException("Illegal DistributionFitter type"); + } + + return bucketOptions; + } + + private static List encodeDistributionPoints( + google.registry.monitoring.metrics.Distribution distribution) { + return distribution.intervalCounts().asMapOfRanges().values().asList(); + } + + private static Distribution encodeDistribution( + google.registry.monitoring.metrics.Distribution nativeDistribution) { + return new Distribution() + .setMean(nativeDistribution.mean()) + .setCount(nativeDistribution.count()) + .setSumOfSquaredDeviation(nativeDistribution.sumOfSquaredDeviation()) + .setBucketOptions(encodeBucketOptions(nativeDistribution.distributionFitter())) + .setBucketCounts(encodeDistributionPoints(nativeDistribution)); + } + + /** + * Encodes a {@link MetricPoint} into a Stackdriver {@link TimeSeries}. + * + *

This method will register the underlying {@link google.registry.monitoring.metrics.Metric} + * as a Stackdriver {@link MetricDescriptor}. + * + * @see + * Stackdriver TimeSeries API + */ + @VisibleForTesting + TimeSeries getEncodedTimeSeries(google.registry.monitoring.metrics.MetricPoint point) + throws IOException { + google.registry.monitoring.metrics.Metric metric = point.metric(); + try { + checkArgument( + ENCODED_METRIC_KINDS.containsKey(metric.getMetricSchema().kind().name()), + METRIC_KIND_ERROR); + checkArgument(ENCODED_METRIC_TYPES.containsKey(metric.getValueClass()), METRIC_TYPE_ERROR); + } catch (IllegalArgumentException e) { + throw new IOException(e.getMessage()); + } + + MetricDescriptor descriptor = registerMetric(metric); + + if (point.labelValues().size() != point.metric().getMetricSchema().labels().size()) { + throw new IOException(METRIC_LABEL_COUNT_ERROR); + } + + V value = point.value(); + TypedValue encodedValue = new TypedValue(); + Class valueClass = metric.getValueClass(); + + if (valueClass == Long.class) { + encodedValue.setInt64Value((Long) value); + } else if (valueClass == Double.class) { + encodedValue.setDoubleValue((Double) value); + } else if (valueClass == Boolean.class) { + encodedValue.setBoolValue((Boolean) value); + } else if (valueClass == String.class) { + encodedValue.setStringValue((String) value); + } else if (valueClass == google.registry.monitoring.metrics.Distribution.class) { + encodedValue.setDistributionValue( + encodeDistribution((google.registry.monitoring.metrics.Distribution) value)); + } else { + // This is unreachable because the precondition checks will catch all NotSerializable + // exceptions. + throw new IllegalArgumentException("Invalid metric valueClass: " + metric.getValueClass()); + } + + Point encodedPoint = + new Point() + .setInterval(encodeTimeInterval(point.interval(), metric.getMetricSchema().kind())) + .setValue(encodedValue); + + List encodedLabels = descriptor.getLabels(); + // The MetricDescriptors returned by the GCM API have null fields rather than empty lists + encodedLabels = encodedLabels == null ? ImmutableList.of() : encodedLabels; + + ImmutableMap.Builder labelValues = new ImmutableMap.Builder<>(); + int i = 0; + for (LabelDescriptor labelDescriptor : encodedLabels) { + labelValues.put(labelDescriptor.getKey(), point.labelValues().get(i++)); + } + + Metric encodedMetric = + new Metric().setType(descriptor.getType()).setLabels(labelValues.build()); + + return new TimeSeries() + .setMetric(encodedMetric) + .setPoints(ImmutableList.of(encodedPoint)) + .setResource(monitoredResource) + // these two attributes are ignored by the API, we set them here to use elsewhere + // for internal metrics. + .setMetricKind(descriptor.getMetricKind()) + .setValueType(descriptor.getValueType()); + } +} diff --git a/javatests/google/registry/monitoring/metrics/example/BUILD b/javatests/google/registry/monitoring/metrics/example/BUILD new file mode 100644 index 000000000..ff6127479 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/example/BUILD @@ -0,0 +1,19 @@ +package( + default_visibility = ["//java/google/registry:registry_project"], +) + +licenses(["notice"]) # Apache 2.0 + +java_binary( + name = "SheepCounterExample", + srcs = ["SheepCounterExample.java"], + deps = [ + "//java/google/registry/monitoring/metrics", + "//java/google/registry/monitoring/metrics/stackdriver", + "@com_google_api_client", + "@com_google_apis_google_api_services_monitoring", + "@com_google_guava", + "@com_google_http_client", + "@com_google_http_client_jackson2", + ], +) diff --git a/javatests/google/registry/monitoring/metrics/example/SheepCounterExample.java b/javatests/google/registry/monitoring/metrics/example/SheepCounterExample.java new file mode 100644 index 000000000..c9b58fd15 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/example/SheepCounterExample.java @@ -0,0 +1,268 @@ +// Copyright 2016 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.monitoring.metrics.example; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.monitoring.v3.Monitoring; +import com.google.api.services.monitoring.v3.MonitoringScopes; +import com.google.api.services.monitoring.v3.model.MonitoredResource; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import google.registry.monitoring.metrics.EventMetric; +import google.registry.monitoring.metrics.IncrementableMetric; +import google.registry.monitoring.metrics.LabelDescriptor; +import google.registry.monitoring.metrics.LinearFitter; +import google.registry.monitoring.metrics.Metric; +import google.registry.monitoring.metrics.MetricRegistryImpl; +import google.registry.monitoring.metrics.MetricReporter; +import google.registry.monitoring.metrics.MetricWriter; +import google.registry.monitoring.metrics.SettableMetric; +import google.registry.monitoring.metrics.stackdriver.StackdriverWriter; +import java.io.IOException; +import java.util.Random; +import java.util.concurrent.Executors; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** A sample application which uses the Metrics API to count sheep while sleeping. */ +public final class SheepCounterExample { + + /** + * The code below for using a custom {@link LogManager} is only necessary to enable logging at JVM + * shutdown to show the shutdown logs of {@link MetricReporter} in this small standalone + * application. + * + *

It is NOT necessary for normal use of the Metrics library. + */ + static { + // must be called before any Logger method is used. + System.setProperty("java.util.logging.manager", DelayedShutdownLogManager.class.getName()); + } + + private static final Logger logger = Logger.getLogger(SheepCounterExample.class.getName()); + + /** + * The time interval, in seconds, between when the {@link MetricReporter} will read {@link Metric} + * instances and enqueue them to the {@link MetricWriter}. + * + * @see MetricReporter + */ + private static final long METRICS_REPORTING_INTERVAL = 30L; + + /** + * The maximum queries per second to the Stackdriver API. Contact Cloud Support to raise this from + * the default value if necessary. + */ + private static final int STACKDRIVER_MAX_QPS = 30; + + /** + * The maximum number of {@link com.google.api.services.monitoring.v3.model.TimeSeries} that can + * be bundled into a single {@link + * com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest}. This must be at most 200. + * Setting this lower will cause the {@link StackdriverWriter} to {@link + * StackdriverWriter#flush()} more frequently. + */ + private static final int STACKDRIVER_MAX_POINTS_PER_REQUEST = 200; + + // Create some metrics to track your ZZZs. + private static final ImmutableList SHEEP_COLORS = + ImmutableList.of("Green", "Yellow", "Red", "Blue"); + private static final ImmutableList SHEEP_SPECIES = + ImmutableList.of("Domestic", "Bighorn"); + private static final ImmutableSet SHEEP_ATTRIBUTES = + ImmutableSet.of( + LabelDescriptor.create("color", "Sheep Color"), + LabelDescriptor.create("species", "Sheep Species")); + + /** + * Counters are good for tracking monotonically increasing values, like request counts or error + * counts. Or, in this case, sheep. + */ + private static final IncrementableMetric sheepCounter = + MetricRegistryImpl.getDefault() + .newIncrementableMetric( + "/sheep", "Counts sheep over time.", "Number of Sheep", SHEEP_ATTRIBUTES); + + /** + * Settable metrics are good for state indicators. For example, you could use one to track the + * lifecycle of a {@link com.google.common.util.concurrent.Service}. In this case, we are just + * using it to track the sleep state of this application. + */ + private static final SettableMetric isSleeping = + MetricRegistryImpl.getDefault() + .newSettableMetric( + "/is_sleeping", + "Tracks sleep state.", + "Sleeping?", + ImmutableSet.of(), + Boolean.class); + + /** + * Gauge metrics never need to be accessed, so the assignment here is unnecessary. You only need + * it if you plan on calling {@link Metric#getTimestampedValues()} to read the values of the + * metric in the code yourself. + */ + @SuppressWarnings("unused") + private static final Metric sleepQuality = + MetricRegistryImpl.getDefault() + .newGauge( + "/sleep_quality", + "Quality of the sleep.", + "Quality", + ImmutableSet.of(), + new Supplier, Double>>() { + @Override + public ImmutableMap, Double> get() { + return ImmutableMap.of(ImmutableList.of(), new Random().nextDouble()); + } + }, + Double.class); + + /** + * Event metrics track aspects of an "event." Here, we track the fluffiness of the sheep we've + * seen. + */ + private static final EventMetric sheepFluffiness = + MetricRegistryImpl.getDefault() + .newEventMetric( + "/sheep_fluffiness", + "Measures the fluffiness of seen sheep.", + "Fill Power", + SHEEP_ATTRIBUTES, + LinearFitter.create(5, 20.0, 20.0)); + + private static Monitoring createAuthorizedMonitoringClient() throws IOException { + // Grab the Application Default Credentials from the environment. + // Generate these with 'gcloud beta auth application-default login' + GoogleCredential credential = + GoogleCredential.getApplicationDefault().createScoped(MonitoringScopes.all()); + + // Create and return the CloudMonitoring service object + HttpTransport httpTransport = new NetHttpTransport(); + JsonFactory jsonFactory = new JacksonFactory(); + return new Monitoring.Builder(httpTransport, jsonFactory, credential) + .setApplicationName("Monitoring Sample") + .build(); + } + + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.err.println("Missing required project argument"); + System.err.printf( + "Usage: java %s gcp-project-id [verbose]\n", SheepCounterExample.class.getName()); + return; + } + String project = args[0]; + + // Turn up the logging verbosity + if (args.length > 1) { + Logger log = LogManager.getLogManager().getLogger(""); + log.setLevel(Level.ALL); + for (Handler h : log.getHandlers()) { + h.setLevel(Level.ALL); + } + } + + // Create a sample resource. In this case, a GCE Instance. + // See https://cloud.google.com/monitoring/api/resources for a list of resource types. + MonitoredResource monitoredResource = + new MonitoredResource() + .setType("gce_instance") + .setLabels( + ImmutableMap.of( + "instance_id", "test-instance", + "zone", "us-central1-f")); + + // Set up the Metrics infrastructure. + MetricWriter stackdriverWriter = + new StackdriverWriter( + createAuthorizedMonitoringClient(), + project, + monitoredResource, + STACKDRIVER_MAX_QPS, + STACKDRIVER_MAX_POINTS_PER_REQUEST); + final MetricReporter reporter = + new MetricReporter( + stackdriverWriter, METRICS_REPORTING_INTERVAL, Executors.defaultThreadFactory()); + reporter.startAsync().awaitRunning(); + + // Set up a handler to stop sleeping on SIGINT. + Runtime.getRuntime() + .addShutdownHook( + new Thread( + new Runnable() { + @Override + public void run() { + reporter.stopAsync().awaitTerminated(); + // Allow the LogManager to cleanup the loggers. + DelayedShutdownLogManager.resetFinally(); + } + })); + + System.err.println("Send SIGINT (Ctrl+C) to stop sleeping."); + while (true) { + // Count some Googley sheep. + int colorIndex = new Random().nextInt(SHEEP_COLORS.size()); + int speciesIndex = new Random().nextInt(SHEEP_SPECIES.size()); + sheepCounter.incrementBy(1, SHEEP_COLORS.get(colorIndex), SHEEP_SPECIES.get(speciesIndex)); + sheepFluffiness.record( + new Random().nextDouble() * 200, + SHEEP_COLORS.get(colorIndex), + SHEEP_SPECIES.get(speciesIndex)); + isSleeping.set(true); + + logger.info("zzz..."); + Thread.sleep(5000); + } + } + + /** + * Special {@link LogManager} with a no-op {@link LogManager#reset()} so that logging can proceed + * as usual until stopped in in another runtime shutdown hook. + * + *

The class is marked public because it is loaded by the JVM classloader at runtime. + */ + @SuppressWarnings("WeakerAccess") + public static class DelayedShutdownLogManager extends LogManager { + + private static DelayedShutdownLogManager instance; + + public DelayedShutdownLogManager() { + instance = this; + } + + /** A no-op implementation. */ + @Override + public void reset() { + /* don't reset yet. */ + } + + static void resetFinally() { + instance.delayedReset(); + } + + private void delayedReset() { + super.reset(); + } + } +} diff --git a/javatests/google/registry/monitoring/metrics/stackdriver/BUILD b/javatests/google/registry/monitoring/metrics/stackdriver/BUILD new file mode 100644 index 000000000..76a0b3b8d --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/stackdriver/BUILD @@ -0,0 +1,34 @@ +package( + default_testonly = 1, + default_visibility = ["//java/google/registry:registry_project"], +) + +licenses(["notice"]) # Apache 2.0 + +load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "stackdriver", + srcs = glob(["*.java"]), + deps = [ + "//java/google/registry/monitoring/metrics", + "//java/google/registry/monitoring/metrics/stackdriver", + "@com_google_api_client", + "@com_google_apis_google_api_services_monitoring", + "@com_google_guava", + "@com_google_http_client", + "@com_google_http_client_jackson2", + "@com_google_truth", + "@joda_time", + "@junit", + "@org_mockito_all", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["*Test.java"]), + deps = [ + ":stackdriver", + ], +) diff --git a/javatests/google/registry/monitoring/metrics/stackdriver/GoogleJsonResponseExceptionHelper.java b/javatests/google/registry/monitoring/metrics/stackdriver/GoogleJsonResponseExceptionHelper.java new file mode 100644 index 000000000..b3f918ece --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/stackdriver/GoogleJsonResponseExceptionHelper.java @@ -0,0 +1,171 @@ +// Copyright 2016 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.monitoring.metrics.stackdriver; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.json.jackson2.JacksonFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** A helper to create instances of {@link GoogleJsonResponseException}. */ +public class GoogleJsonResponseExceptionHelper { + /** + * @param statusCode the status code that should be in the returned {@link + * GoogleJsonResponseException} + * @return a {@link GoogleJsonResponseException} with the status code {@code statusCode} + * @throws IOException shouldn't occur + */ + public static GoogleJsonResponseException create(int statusCode) throws IOException { + HttpResponse response = createHttpResponse(statusCode, null); + return GoogleJsonResponseException.from(new JacksonFactory(), response); + } + + public static HttpResponse createHttpResponse(int statusCode, InputStream content) + throws IOException { + FakeHttpTransport transport = new FakeHttpTransport(statusCode, content); + HttpRequestFactory factory = transport.createRequestFactory(); + HttpRequest request = + factory.buildRequest( + "foo", new GenericUrl("http://example.com/bar"), new EmptyHttpContent()); + request.setThrowExceptionOnExecuteError(false); + return request.execute(); + } + + private static class FakeHttpTransport extends HttpTransport { + private final int statusCode; + private final InputStream content; + + FakeHttpTransport(int statusCode, InputStream content) { + this.statusCode = statusCode; + this.content = content; + } + + @Override + protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new FakeLowLevelHttpRequest(statusCode, content); + } + } + + private static class FakeLowLevelHttpRequest extends LowLevelHttpRequest { + private final int statusCode; + private final InputStream content; + + FakeLowLevelHttpRequest(int statusCode, InputStream content) { + this.statusCode = statusCode; + this.content = content; + } + + @Override + public void addHeader(String name, String value) throws IOException { + // Nothing! + } + + @Override + public LowLevelHttpResponse execute() throws IOException { + return new FakeLowLevelHttpResponse(statusCode, content); + } + } + + private static class FakeLowLevelHttpResponse extends LowLevelHttpResponse { + private final int statusCode; + private final InputStream content; + + FakeLowLevelHttpResponse(int statusCode, InputStream content) { + this.statusCode = statusCode; + this.content = content; + } + + @Override + public InputStream getContent() throws IOException { + return content; + } + + @Override + public String getContentEncoding() throws IOException { + return null; + } + + @Override + public long getContentLength() throws IOException { + return 0; + } + + @Override + public String getContentType() throws IOException { + return "text/json"; + } + + @Override + public String getStatusLine() throws IOException { + return null; + } + + @Override + public int getStatusCode() throws IOException { + return statusCode; + } + + @Override + public String getReasonPhrase() throws IOException { + return null; + } + + @Override + public int getHeaderCount() throws IOException { + return 0; + } + + @Override + public String getHeaderName(int index) throws IOException { + return null; + } + + @Override + public String getHeaderValue(int index) throws IOException { + return null; + } + } + + private static class EmptyHttpContent implements HttpContent { + @Override + public long getLength() throws IOException { + return 0; + } + + @Override + public String getType() { + return "text/json"; + } + + @Override + public boolean retrySupported() { + return false; + } + + @Override + public void writeTo(OutputStream out) throws IOException { + // Nothing! + } + } +} diff --git a/javatests/google/registry/monitoring/metrics/stackdriver/StackdriverWriterTest.java b/javatests/google/registry/monitoring/metrics/stackdriver/StackdriverWriterTest.java new file mode 100644 index 000000000..4dea28353 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/stackdriver/StackdriverWriterTest.java @@ -0,0 +1,563 @@ +// Copyright 2016 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.monitoring.metrics.stackdriver; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.services.monitoring.v3.Monitoring; +import com.google.api.services.monitoring.v3.model.BucketOptions; +import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest; +import com.google.api.services.monitoring.v3.model.Explicit; +import com.google.api.services.monitoring.v3.model.Exponential; +import com.google.api.services.monitoring.v3.model.Linear; +import com.google.api.services.monitoring.v3.model.MetricDescriptor; +import com.google.api.services.monitoring.v3.model.MonitoredResource; +import com.google.api.services.monitoring.v3.model.Point; +import com.google.api.services.monitoring.v3.model.TimeSeries; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import google.registry.monitoring.metrics.CustomFitter; +import google.registry.monitoring.metrics.Distribution; +import google.registry.monitoring.metrics.ExponentialFitter; +import google.registry.monitoring.metrics.LabelDescriptor; +import google.registry.monitoring.metrics.LinearFitter; +import google.registry.monitoring.metrics.Metric; +import google.registry.monitoring.metrics.MetricPoint; +import google.registry.monitoring.metrics.MetricSchema; +import google.registry.monitoring.metrics.MetricSchema.Kind; +import google.registry.monitoring.metrics.MutableDistribution; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; +import org.joda.time.Instant; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +/** Unit tests for {@link StackdriverWriter}. */ +@RunWith(MockitoJUnitRunner.class) +public class StackdriverWriterTest { + + @Mock private Monitoring client; + @Mock private Monitoring.Projects projects; + @Mock private Monitoring.Projects.MetricDescriptors metricDescriptors; + @Mock private Monitoring.Projects.MetricDescriptors.Get metricDescriptorGet; + @Mock private Monitoring.Projects.TimeSeries timeSeries; + @Mock private Monitoring.Projects.MetricDescriptors.Create metricDescriptorCreate; + @Mock private Monitoring.Projects.TimeSeries.Create timeSeriesCreate; + @Mock private Metric metric; + @Mock private Metric boolMetric; + @Mock private Metric distributionMetric; + private static final String PROJECT = "PROJECT"; + private static final int MAX_QPS = 10; + private static final int MAX_POINTS_PER_REQUEST = 10; + private static final MonitoredResource MONITORED_RESOURCE = new MonitoredResource(); + + @Before + public void setUp() throws Exception { + when(metric.getValueClass()).thenReturn(Long.class); + when(metric.getCardinality()).thenReturn(1); + when(metric.getMetricSchema()) + .thenReturn( + MetricSchema.create( + "/name", + "desc", + "vdn", + Kind.CUMULATIVE, + ImmutableSet.of(LabelDescriptor.create("label1", "desc1")))); + // Store in an intermediate value, because Mockito hates when mocks are evaluated inside of + // thenReturn() methods. + MetricPoint longPoint = + MetricPoint.create( + metric, ImmutableList.of("value1"), new Instant(1337), new Instant(1338), 5L); + when(metric.getTimestampedValues()).thenReturn(ImmutableList.of(longPoint)); + + when(boolMetric.getValueClass()).thenReturn(Boolean.class); + when(boolMetric.getMetricSchema()) + .thenReturn( + MetricSchema.create( + "/name", + "desc", + "vdn", + Kind.GAUGE, + ImmutableSet.of(LabelDescriptor.create("label1", "desc1")))); + // Store in an intermediate value, because Mockito hates when mocks are evaluated inside of + // thenReturn() methods. + MetricPoint boolPoint = + MetricPoint.create(boolMetric, ImmutableList.of("foo"), new Instant(1337), true); + when(boolMetric.getTimestampedValues()).thenReturn(ImmutableList.of(boolPoint)); + + when(distributionMetric.getMetricSchema()) + .thenReturn( + MetricSchema.create( + "/name", + "desc", + "vdn", + Kind.GAUGE, + ImmutableSet.of(LabelDescriptor.create("label1", "desc1")))); + when(distributionMetric.getValueClass()).thenReturn(Distribution.class); + + MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(metric); + when(client.projects()).thenReturn(projects); + when(projects.metricDescriptors()).thenReturn(metricDescriptors); + when(projects.timeSeries()).thenReturn(timeSeries); + when(metricDescriptors.create(anyString(), any(MetricDescriptor.class))) + .thenReturn(metricDescriptorCreate); + when(metricDescriptorCreate.execute()).thenReturn(descriptor); + when(metricDescriptors.get(anyString())).thenReturn(metricDescriptorGet); + when(metricDescriptorGet.execute()).thenReturn(descriptor); + when(timeSeries.create(anyString(), any(CreateTimeSeriesRequest.class))) + .thenReturn(timeSeriesCreate); + } + + @Test + public void testWrite_maxPoints_flushes() throws Exception { + StackdriverWriter writer = + spy( + new StackdriverWriter( + client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST)); + + + for (int i = 0; i < MAX_POINTS_PER_REQUEST; i++) { + for (MetricPoint point : metric.getTimestampedValues()) { + writer.write(point); + } + } + + verify(writer).flush(); + } + + @Test + public void testWrite_lessThanMaxPoints_doesNotFlush() throws Exception { + StackdriverWriter writer = + spy( + new StackdriverWriter( + client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST)); + for (int i = 0; i < MAX_POINTS_PER_REQUEST - 1; i++) { + for (MetricPoint point : metric.getTimestampedValues()) { + writer.write(point); + } + } + + verify(writer, never()).flush(); + } + + @Test + public void testWrite_invalidMetricType_throwsException() throws Exception { + when(metric.getValueClass()) + .thenAnswer( + new Answer>() { + @Override + public Class answer(InvocationOnMock invocation) throws Throwable { + return Object.class; + } + }); + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + for (MetricPoint point : metric.getTimestampedValues()) { + try { + writer.write(point); + fail("expected IllegalArgumentException"); + } catch (IOException expected) {} + } + } + + @Test + public void testWrite_ManyPoints_flushesTwice() throws Exception { + StackdriverWriter writer = + spy( + new StackdriverWriter( + client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST)); + + for (int i = 0; i < MAX_POINTS_PER_REQUEST * 2; i++) { + for (MetricPoint point : metric.getTimestampedValues()) { + writer.write(point); + } + } + + verify(writer, times(2)).flush(); + } + + @Test + public void testRegisterMetric_registersWithStackdriver() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + writer.registerMetric(metric); + + verify( + client + .projects() + .metricDescriptors() + .create(PROJECT, StackdriverWriter.encodeMetricDescriptor(metric))) + .execute(); + } + + @Test + public void registerMetric_doesNotReregisterDupe() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + writer.registerMetric(metric); + writer.registerMetric(metric); + + verify( + client + .projects() + .metricDescriptors() + .create(PROJECT, StackdriverWriter.encodeMetricDescriptor(metric))) + .execute(); + } + + @Test + public void registerMetric_fetchesStackdriverDefinition() throws Exception { + // Stackdriver throws an Exception with the status message "ALREADY_EXISTS" when you try to + // register a metric that's already been registered, so we fake one here. + ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes(UTF_8)); + HttpResponse response = GoogleJsonResponseExceptionHelper.createHttpResponse(400, inputStream); + HttpResponseException.Builder httpResponseExceptionBuilder = + new HttpResponseException.Builder(response); + httpResponseExceptionBuilder.setStatusCode(400); + httpResponseExceptionBuilder.setStatusMessage("ALREADY_EXISTS"); + GoogleJsonResponseException exception = + new GoogleJsonResponseException(httpResponseExceptionBuilder, null); + when(metricDescriptorCreate.execute()).thenThrow(exception); + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + writer.registerMetric(metric); + + verify(client.projects().metricDescriptors().get("metric")).execute(); + } + + @Test + public void registerMetric_rethrowsException() throws Exception { + ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes(UTF_8)); + HttpResponse response = GoogleJsonResponseExceptionHelper.createHttpResponse(400, inputStream); + HttpResponseException.Builder httpResponseExceptionBuilder = + new HttpResponseException.Builder(response); + httpResponseExceptionBuilder.setStatusCode(404); + GoogleJsonResponseException exception = + new GoogleJsonResponseException(httpResponseExceptionBuilder, null); + when(metricDescriptorCreate.execute()).thenThrow(exception); + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + try { + writer.registerMetric(metric); + fail("Expected GoogleJsonResponseException"); + } catch (GoogleJsonResponseException expected) { + assertThat(exception.getStatusCode()).isEqualTo(404); + } + } + + @Test + public void getEncodedTimeSeries_nullLabels_encodes() throws Exception { + ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes(UTF_8)); + HttpResponse response = GoogleJsonResponseExceptionHelper.createHttpResponse(400, inputStream); + HttpResponseException.Builder httpResponseExceptionBuilder = + new HttpResponseException.Builder(response); + httpResponseExceptionBuilder.setStatusCode(400); + httpResponseExceptionBuilder.setStatusMessage("ALREADY_EXISTS"); + GoogleJsonResponseException exception = + new GoogleJsonResponseException(httpResponseExceptionBuilder, null); + when(metricDescriptorCreate.execute()).thenThrow(exception); + when(metricDescriptorGet.execute()) + .thenReturn(new MetricDescriptor().setName("foo").setLabels(null)); + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + writer.registerMetric(metric); + + TimeSeries timeSeries = + writer.getEncodedTimeSeries( + MetricPoint.create(metric, ImmutableList.of("foo"), new Instant(1337), 10L)); + + assertThat(timeSeries.getMetric().getLabels()).isEmpty(); + } + + @Test + public void encodeMetricDescriptor_simpleMetric_encodes() { + MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(metric); + + assertThat(descriptor.getType()).isEqualTo("custom.googleapis.com/name"); + assertThat(descriptor.getValueType()).isEqualTo("INT64"); + assertThat(descriptor.getDescription()).isEqualTo("desc"); + assertThat(descriptor.getDisplayName()).isEqualTo("vdn"); + assertThat(descriptor.getLabels()) + .containsExactly( + new com.google.api.services.monitoring.v3.model.LabelDescriptor() + .setValueType("STRING") + .setKey("label1") + .setDescription("desc1")); + } + + @Test + public void encodeLabelDescriptors_simpleLabels_encodes() { + ImmutableSet descriptors = + ImmutableSet.of( + LabelDescriptor.create("label1", "description1"), + LabelDescriptor.create("label2", "description2")); + + ImmutableList encodedDescritors = + StackdriverWriter.encodeLabelDescriptors(descriptors); + + assertThat(encodedDescritors) + .containsExactly( + new com.google.api.services.monitoring.v3.model.LabelDescriptor() + .setValueType("STRING") + .setKey("label1") + .setDescription("description1"), + new com.google.api.services.monitoring.v3.model.LabelDescriptor() + .setValueType("STRING") + .setKey("label2") + .setDescription("description2")); + } + + @Test + public void getEncodedTimeSeries_cumulativeMetricPoint_ZeroInterval_encodesGreaterEndTime() + throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + MetricPoint nativePoint = + MetricPoint.create( + metric, ImmutableList.of("foo"), new Instant(1337), new Instant(1337), 10L); + + TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint); + + assertThat(timeSeries.getValueType()).isEqualTo("INT64"); + assertThat(timeSeries.getMetricKind()).isEqualTo("CUMULATIVE"); + List points = timeSeries.getPoints(); + assertThat(points).hasSize(1); + Point point = points.get(0); + assertThat(point.getValue().getInt64Value()).isEqualTo(10L); + assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.338Z"); + } + + @Test + public void getEncodedTimeSeries_cumulativeMetricPoint_nonZeroInterval_encodesSameInterval() + throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + MetricPoint nativePoint = + MetricPoint.create( + metric, ImmutableList.of("foo"), new Instant(1337), new Instant(1339), 10L); + + TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint); + + assertThat(timeSeries.getValueType()).isEqualTo("INT64"); + assertThat(timeSeries.getMetricKind()).isEqualTo("CUMULATIVE"); + List points = timeSeries.getPoints(); + assertThat(points).hasSize(1); + Point point = points.get(0); + assertThat(point.getValue().getInt64Value()).isEqualTo(10L); + assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.339Z"); + } + + @Test + public void getEncodedTimeSeries_gaugeMetricPoint_zeroInterval_encodesSameInterval() + throws Exception { + when(metric.getMetricSchema()) + .thenReturn( + MetricSchema.create( + "/name", + "desc", + "vdn", + Kind.GAUGE, + ImmutableSet.of(LabelDescriptor.create("label1", "desc1")))); + // Store in an intermediate value, because Mockito hates when mocks are evaluated inside of + // thenReturn() methods. + MetricPoint testPoint = + MetricPoint.create(metric, ImmutableList.of("foo"), new Instant(1337), 10L); + when(metric.getTimestampedValues()).thenReturn(ImmutableList.of(testPoint)); + // Store in an intermediate value, because Mockito hates when mocks are evaluated inside of + // thenReturn() methods. + MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(metric); + when(metricDescriptorCreate.execute()).thenReturn(descriptor); + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + MetricPoint nativePoint = + MetricPoint.create( + metric, ImmutableList.of("foo"), new Instant(1337), new Instant(1337), 10L); + + TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint); + + assertThat(timeSeries.getValueType()).isEqualTo("INT64"); + assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE"); + List points = timeSeries.getPoints(); + assertThat(points).hasSize(1); + Point point = points.get(0); + assertThat(point.getValue().getInt64Value()).isEqualTo(10L); + assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + } + + @Test + public void getEncodedTimeSeries_booleanMetric_encodes() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + MetricDescriptor boolDescriptor = StackdriverWriter.encodeMetricDescriptor(boolMetric); + when(metricDescriptorCreate.execute()).thenReturn(boolDescriptor); + MetricPoint nativePoint = + MetricPoint.create(boolMetric, ImmutableList.of("foo"), new Instant(1337), true); + + TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint); + + assertThat(timeSeries.getValueType()).isEqualTo("BOOL"); + assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE"); + List points = timeSeries.getPoints(); + assertThat(points).hasSize(1); + Point point = points.get(0); + assertThat(point.getValue().getBoolValue()).isEqualTo(true); + assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + } + + @Test + public void getEncodedTimeSeries_distributionMetricCustomFitter_encodes() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(distributionMetric); + when(metricDescriptorCreate.execute()).thenReturn(descriptor); + MutableDistribution distribution = + new MutableDistribution(CustomFitter.create(ImmutableSet.of(5.0))); + distribution.add(10.0, 5L); + distribution.add(0.0, 5L); + MetricPoint nativePoint = + MetricPoint.create( + distributionMetric, ImmutableList.of("foo"), new Instant(1337), distribution); + + TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint); + + assertThat(timeSeries.getValueType()).isEqualTo("DISTRIBUTION"); + assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE"); + List points = timeSeries.getPoints(); + assertThat(points).hasSize(1); + Point point = points.get(0); + assertThat(point.getValue().getDistributionValue()) + .isEqualTo( + new com.google.api.services.monitoring.v3.model.Distribution() + .setMean(5.0) + .setSumOfSquaredDeviation(250.0) + .setCount(10L) + .setBucketCounts(ImmutableList.of(5L, 5L)) + .setBucketOptions( + new BucketOptions() + .setExplicitBuckets(new Explicit().setBounds(ImmutableList.of(5.0))))); + assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + } + + @Test + public void getEncodedTimeSeries_distributionMetricLinearFitter_encodes() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(distributionMetric); + when(metricDescriptorCreate.execute()).thenReturn(descriptor); + MutableDistribution distribution = new MutableDistribution(LinearFitter.create(2, 5.0, 3.0)); + distribution.add(0.0, 1L); + distribution.add(3.0, 2L); + distribution.add(10.0, 5L); + distribution.add(20.0, 5L); + MetricPoint nativePoint = + MetricPoint.create( + distributionMetric, ImmutableList.of("foo"), new Instant(1337), distribution); + + + TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint); + + assertThat(timeSeries.getValueType()).isEqualTo("DISTRIBUTION"); + assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE"); + List points = timeSeries.getPoints(); + assertThat(points).hasSize(1); + Point point = points.get(0); + assertThat(point.getValue().getDistributionValue()) + .isEqualTo( + new com.google.api.services.monitoring.v3.model.Distribution() + .setMean(12.0) + .setSumOfSquaredDeviation(646.0) + .setCount(13L) + .setBucketCounts(ImmutableList.of(1L, 2L, 5L, 5L)) + .setBucketOptions( + new BucketOptions() + .setLinearBuckets( + new Linear().setNumFiniteBuckets(2).setWidth(5.0).setOffset(3.0)))); + assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + } + + @Test + public void getEncodedTimeSeries_distributionMetricExponentialFitter_encodes() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(distributionMetric); + when(metricDescriptorCreate.execute()).thenReturn(descriptor); + MutableDistribution distribution = + new MutableDistribution(ExponentialFitter.create(2, 3.0, 0.5)); + distribution.add(0.0, 1L); + distribution.add(3.0, 2L); + distribution.add(10.0, 5L); + distribution.add(20.0, 5L); + MetricPoint nativePoint = + MetricPoint.create( + distributionMetric, ImmutableList.of("foo"), new Instant(1337), distribution); + + TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint); + + assertThat(timeSeries.getValueType()).isEqualTo("DISTRIBUTION"); + assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE"); + List points = timeSeries.getPoints(); + assertThat(points).hasSize(1); + Point point = points.get(0); + assertThat(point.getValue().getDistributionValue()) + .isEqualTo( + new com.google.api.services.monitoring.v3.model.Distribution() + .setMean(12.0) + .setSumOfSquaredDeviation(646.0) + .setCount(13L) + .setBucketCounts(ImmutableList.of(1L, 0L, 2L, 10L)) + .setBucketOptions( + new BucketOptions() + .setExponentialBuckets( + new Exponential() + .setNumFiniteBuckets(2) + .setGrowthFactor(3.0) + .setScale(0.5)))); + assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + } +}