Add MetricRecorder implementation (#11128)

* added MetricRecorderImpl and unit tests for MetricInstrumentRegistry

* updated MetricInstrumentRegistry to use array instead of ArrayList

* renamed record<>Counter APIs to add<>Counter. Added check for mismatched label values

* added lock for instruments array
This commit is contained in:
Vindhya Ningegowda 2024-04-26 13:47:55 -07:00 committed by GitHub
parent da619e2bde
commit 795ee0f6e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 781 additions and 85 deletions

View File

@ -23,7 +23,7 @@ import java.util.List;
*/
@Internal
public final class DoubleCounterMetricInstrument extends PartialMetricInstrument {
DoubleCounterMetricInstrument(long index, String name, String description, String unit,
DoubleCounterMetricInstrument(int index, String name, String description, String unit,
List<String> requiredLabelKeys, List<String> optionalLabelKeys, boolean enableByDefault) {
super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault);
}

View File

@ -25,7 +25,7 @@ import java.util.List;
public final class DoubleHistogramMetricInstrument extends PartialMetricInstrument {
private final List<Double> bucketBoundaries;
DoubleHistogramMetricInstrument(long index, String name, String description, String unit,
DoubleHistogramMetricInstrument(int index, String name, String description, String unit,
List<Double> bucketBoundaries, List<String> requiredLabelKeys, List<String> optionalLabelKeys,
boolean enableByDefault) {
super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault);

View File

@ -23,7 +23,7 @@ import java.util.List;
*/
@Internal
public final class LongCounterMetricInstrument extends PartialMetricInstrument {
LongCounterMetricInstrument(long index, String name, String description, String unit,
LongCounterMetricInstrument(int index, String name, String description, String unit,
List<String> requiredLabelKeys, List<String> optionalLabelKeys, boolean enableByDefault) {
super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault);
}

View File

@ -23,7 +23,7 @@ import java.util.List;
*/
@Internal
public final class LongGaugeMetricInstrument extends PartialMetricInstrument {
LongGaugeMetricInstrument(long index, String name, String description, String unit,
LongGaugeMetricInstrument(int index, String name, String description, String unit,
List<String> requiredLabelKeys, List<String> optionalLabelKeys, boolean enableByDefault) {
super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault);
}

View File

@ -25,7 +25,7 @@ import java.util.List;
public final class LongHistogramMetricInstrument extends PartialMetricInstrument {
private final List<Long> bucketBoundaries;
LongHistogramMetricInstrument(long index, String name, String description, String unit,
LongHistogramMetricInstrument(int index, String name, String description, String unit,
List<Long> bucketBoundaries, List<String> requiredLabelKeys, List<String> optionalLabelKeys,
boolean enableByDefault) {
super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault);

View File

@ -28,7 +28,7 @@ public interface MetricInstrument {
*
* @return the index of the metric instrument.
*/
public long getIndex();
public int getIndex();
/**
* Returns the name of the metric.

View File

@ -16,25 +16,36 @@
package io.grpc;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.concurrent.GuardedBy;
/**
* A registry for globally registered metric instruments.
*/
@Internal
public final class MetricInstrumentRegistry {
static final int INITIAL_INSTRUMENT_CAPACITY = 5;
private static MetricInstrumentRegistry instance;
private final List<MetricInstrument> metricInstruments;
private final Set<String> registeredMetricNames;
private final Object lock = new Object();
@GuardedBy("lock")
private final Set<String> registeredMetricNames = new HashSet<>();
@GuardedBy("lock")
private MetricInstrument[] metricInstruments =
new MetricInstrument[INITIAL_INSTRUMENT_CAPACITY];
@GuardedBy("lock")
private int nextAvailableMetricIndex;
private MetricInstrumentRegistry() {
this.metricInstruments = new CopyOnWriteArrayList<>();
this.registeredMetricNames = new CopyOnWriteArraySet<>();
}
@VisibleForTesting
MetricInstrumentRegistry() {}
/**
* Returns the default metric instrument registry.
@ -50,7 +61,10 @@ public final class MetricInstrumentRegistry {
* Returns a list of registered metric instruments.
*/
public List<MetricInstrument> getMetricInstruments() {
return Collections.unmodifiableList(metricInstruments);
synchronized (lock) {
return Collections.unmodifiableList(
Arrays.asList(Arrays.copyOfRange(metricInstruments, 0, nextAvailableMetricIndex)));
}
}
/**
@ -65,20 +79,30 @@ public final class MetricInstrumentRegistry {
* @return the newly created DoubleCounterMetricInstrument
* @throws IllegalStateException if a metric with the same name already exists
*/
// TODO(dnvindhya): Evaluate locks over synchronized methods and update if needed
public synchronized DoubleCounterMetricInstrument registerDoubleCounter(String name,
public DoubleCounterMetricInstrument registerDoubleCounter(String name,
String description, String unit, List<String> requiredLabelKeys,
List<String> optionalLabelKeys, boolean enableByDefault) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
checkArgument(!Strings.isNullOrEmpty(name), "missing metric name");
checkNotNull(description, "description");
checkNotNull(unit, "unit");
checkNotNull(requiredLabelKeys, "requiredLabelKeys");
checkNotNull(optionalLabelKeys, "optionalLabelKeys");
synchronized (lock) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
}
int index = nextAvailableMetricIndex;
if (index + 1 == metricInstruments.length) {
resizeMetricInstruments();
}
DoubleCounterMetricInstrument instrument = new DoubleCounterMetricInstrument(
index, name, description, unit, requiredLabelKeys, optionalLabelKeys,
enableByDefault);
metricInstruments[index] = instrument;
registeredMetricNames.add(name);
nextAvailableMetricIndex += 1;
return instrument;
}
long instrumentIndex = metricInstruments.size();
DoubleCounterMetricInstrument instrument = new DoubleCounterMetricInstrument(
instrumentIndex, name, description, unit, requiredLabelKeys, optionalLabelKeys,
enableByDefault);
metricInstruments.add(instrument);
registeredMetricNames.add(name);
return instrument;
}
/**
@ -93,20 +117,30 @@ public final class MetricInstrumentRegistry {
* @return the newly created LongCounterMetricInstrument
* @throws IllegalStateException if a metric with the same name already exists
*/
public synchronized LongCounterMetricInstrument registerLongCounter(String name,
public LongCounterMetricInstrument registerLongCounter(String name,
String description, String unit, List<String> requiredLabelKeys,
List<String> optionalLabelKeys, boolean enableByDefault) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
checkArgument(!Strings.isNullOrEmpty(name), "missing metric name");
checkNotNull(description, "description");
checkNotNull(unit, "unit");
checkNotNull(requiredLabelKeys, "requiredLabelKeys");
checkNotNull(optionalLabelKeys, "optionalLabelKeys");
synchronized (lock) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
}
int index = nextAvailableMetricIndex;
if (index + 1 == metricInstruments.length) {
resizeMetricInstruments();
}
LongCounterMetricInstrument instrument = new LongCounterMetricInstrument(
index, name, description, unit, requiredLabelKeys, optionalLabelKeys,
enableByDefault);
metricInstruments[index] = instrument;
registeredMetricNames.add(name);
nextAvailableMetricIndex += 1;
return instrument;
}
// Acquire lock?
long instrumentIndex = metricInstruments.size();
LongCounterMetricInstrument instrument = new LongCounterMetricInstrument(
instrumentIndex, name, description, unit, requiredLabelKeys, optionalLabelKeys,
enableByDefault);
metricInstruments.add(instrument);
registeredMetricNames.add(name);
return instrument;
}
/**
@ -122,20 +156,32 @@ public final class MetricInstrumentRegistry {
* @return the newly created DoubleHistogramMetricInstrument
* @throws IllegalStateException if a metric with the same name already exists
*/
public synchronized DoubleHistogramMetricInstrument registerDoubleHistogram(String name,
public DoubleHistogramMetricInstrument registerDoubleHistogram(String name,
String description, String unit, List<Double> bucketBoundaries,
List<String> requiredLabelKeys, List<String> optionalLabelKeys, boolean enableByDefault) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
checkArgument(!Strings.isNullOrEmpty(name), "missing metric name");
checkNotNull(description, "description");
checkNotNull(unit, "unit");
checkNotNull(bucketBoundaries, "bucketBoundaries");
checkNotNull(requiredLabelKeys, "requiredLabelKeys");
checkNotNull(optionalLabelKeys, "optionalLabelKeys");
synchronized (lock) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
}
int index = nextAvailableMetricIndex;
if (index + 1 == metricInstruments.length) {
resizeMetricInstruments();
}
DoubleHistogramMetricInstrument instrument = new DoubleHistogramMetricInstrument(
index, name, description, unit, bucketBoundaries, requiredLabelKeys,
optionalLabelKeys,
enableByDefault);
metricInstruments[index] = instrument;
registeredMetricNames.add(name);
nextAvailableMetricIndex += 1;
return instrument;
}
long indexToInsertInstrument = metricInstruments.size();
DoubleHistogramMetricInstrument instrument = new DoubleHistogramMetricInstrument(
indexToInsertInstrument, name, description, unit, bucketBoundaries, requiredLabelKeys,
optionalLabelKeys,
enableByDefault);
metricInstruments.add(instrument);
registeredMetricNames.add(name);
return instrument;
}
/**
@ -151,20 +197,32 @@ public final class MetricInstrumentRegistry {
* @return the newly created LongHistogramMetricInstrument
* @throws IllegalStateException if a metric with the same name already exists
*/
public synchronized LongHistogramMetricInstrument registerLongHistogram(String name,
public LongHistogramMetricInstrument registerLongHistogram(String name,
String description, String unit, List<Long> bucketBoundaries, List<String> requiredLabelKeys,
List<String> optionalLabelKeys, boolean enableByDefault) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
checkArgument(!Strings.isNullOrEmpty(name), "missing metric name");
checkNotNull(description, "description");
checkNotNull(unit, "unit");
checkNotNull(bucketBoundaries, "bucketBoundaries");
checkNotNull(requiredLabelKeys, "requiredLabelKeys");
checkNotNull(optionalLabelKeys, "optionalLabelKeys");
synchronized (lock) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
}
int index = nextAvailableMetricIndex;
if (index + 1 == metricInstruments.length) {
resizeMetricInstruments();
}
LongHistogramMetricInstrument instrument = new LongHistogramMetricInstrument(
index, name, description, unit, bucketBoundaries, requiredLabelKeys,
optionalLabelKeys,
enableByDefault);
metricInstruments[index] = instrument;
registeredMetricNames.add(name);
nextAvailableMetricIndex += 1;
return instrument;
}
long indexToInsertInstrument = metricInstruments.size();
LongHistogramMetricInstrument instrument = new LongHistogramMetricInstrument(
indexToInsertInstrument, name, description, unit, bucketBoundaries, requiredLabelKeys,
optionalLabelKeys,
enableByDefault);
metricInstruments.add(instrument);
registeredMetricNames.add(name);
return instrument;
}
@ -180,18 +238,38 @@ public final class MetricInstrumentRegistry {
* @return the newly created LongGaugeMetricInstrument
* @throws IllegalStateException if a metric with the same name already exists
*/
public synchronized LongGaugeMetricInstrument registerLongGauge(String name, String description,
public LongGaugeMetricInstrument registerLongGauge(String name, String description,
String unit, List<String> requiredLabelKeys, List<String> optionalLabelKeys, boolean
enableByDefault) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
checkArgument(!Strings.isNullOrEmpty(name), "missing metric name");
checkNotNull(description, "description");
checkNotNull(unit, "unit");
checkNotNull(requiredLabelKeys, "requiredLabelKeys");
checkNotNull(optionalLabelKeys, "optionalLabelKeys");
synchronized (lock) {
if (registeredMetricNames.contains(name)) {
throw new IllegalStateException("Metric with name " + name + " already exists");
}
int index = nextAvailableMetricIndex;
if (index + 1 == metricInstruments.length) {
resizeMetricInstruments();
}
LongGaugeMetricInstrument instrument = new LongGaugeMetricInstrument(
index, name, description, unit, requiredLabelKeys, optionalLabelKeys,
enableByDefault);
metricInstruments[index] = instrument;
registeredMetricNames.add(name);
nextAvailableMetricIndex += 1;
return instrument;
}
long indexToInsertInstrument = metricInstruments.size();
LongGaugeMetricInstrument instrument = new LongGaugeMetricInstrument(
indexToInsertInstrument, name, description, unit, requiredLabelKeys, optionalLabelKeys,
enableByDefault);
metricInstruments.add(instrument);
registeredMetricNames.add(name);
return instrument;
}
@GuardedBy("lock")
private void resizeMetricInstruments() {
// Increase the capacity of the metricInstruments array by INITIAL_INSTRUMENT_CAPACITY
int newInstrumentsCapacity = metricInstruments.length + INITIAL_INSTRUMENT_CAPACITY;
MetricInstrument[] resizedMetricInstruments = Arrays.copyOf(metricInstruments,
newInstrumentsCapacity);
metricInstruments = resizedMetricInstruments;
}
}

View File

@ -16,11 +16,6 @@
package io.grpc;
import io.grpc.DoubleCounterMetricInstrument;
import io.grpc.DoubleHistogramMetricInstrument;
import io.grpc.Internal;
import io.grpc.LongCounterMetricInstrument;
import io.grpc.LongHistogramMetricInstrument;
import java.util.List;
/**
@ -30,25 +25,25 @@ import java.util.List;
@Internal
public interface MetricRecorder {
/**
* Records a value for a double-precision counter metric instrument.
* Adds a value for a double-precision counter metric instrument.
*
* @param metricInstrument The counter metric instrument to record the value against.
* @param value The value to record.
* @param metricInstrument The counter metric instrument to add the value against.
* @param value The value to add.
* @param requiredLabelValues A list of required label values for the metric.
* @param optionalLabelValues A list of additional, optional label values for the metric.
*/
default void recordDoubleCounter(DoubleCounterMetricInstrument metricInstrument, double value,
default void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, double value,
List<String> requiredLabelValues, List<String> optionalLabelValues) {}
/**
* Records a value for a long valued counter metric instrument.
* Adds a value for a long valued counter metric instrument.
*
* @param metricInstrument The counter metric instrument to record the value against.
* @param value The value to record.
* @param metricInstrument The counter metric instrument to add the value against.
* @param value The value to add.
* @param requiredLabelValues A list of required label values for the metric.
* @param optionalLabelValues A list of additional, optional label values for the metric.
*/
default void recordLongCounter(LongCounterMetricInstrument metricInstrument, long value,
default void addLongCounter(LongCounterMetricInstrument metricInstrument, long value,
List<String> requiredLabelValues, List<String> optionalLabelValues) {}
/**

View File

@ -98,4 +98,6 @@ public interface MetricSink {
default void recordLongHistogram(LongHistogramMetricInstrument metricInstrument, long value,
List<String> requiredLabelValues, List<String> optionalLabelValues) {
}
default void updateMeasures(List<MetricInstrument> instruments) {}
}

View File

@ -25,7 +25,7 @@ import java.util.List;
*/
@Internal
abstract class PartialMetricInstrument implements MetricInstrument {
protected final long index;
protected final int index;
protected final String name;
protected final String description;
protected final String unit;
@ -44,7 +44,7 @@ abstract class PartialMetricInstrument implements MetricInstrument {
* @param optionalLabelKeys a list of optional label keys for the metric
* @param enableByDefault whether the metric should be enabled by default
*/
protected PartialMetricInstrument(long index, String name, String description, String unit,
protected PartialMetricInstrument(int index, String name, String description, String unit,
List<String> requiredLabelKeys, List<String> optionalLabelKeys, boolean enableByDefault) {
this.index = index;
this.name = name;
@ -56,7 +56,7 @@ abstract class PartialMetricInstrument implements MetricInstrument {
}
@Override
public long getIndex() {
public int getIndex() {
return index;
}

View File

@ -0,0 +1,193 @@
/*
* Copyright 2024 The gRPC Authors
*
* 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 io.grpc;
import static com.google.common.truth.Truth.assertThat;
import static io.grpc.MetricInstrumentRegistry.INITIAL_INSTRUMENT_CAPACITY;
import com.google.common.collect.ImmutableList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit test for {@link MetricInstrumentRegistry}.
*/
@RunWith(JUnit4.class)
public class MetricInstrumentRegistryTest {
private static final ImmutableList<String> REQUIRED_LABEL_KEYS = ImmutableList.of("KEY1", "KEY2");
private static final ImmutableList<String> OPTIONAL_LABEL_KEYS = ImmutableList.of(
"OPTIONAL_KEY_1");
private static final ImmutableList<Double> DOUBLE_HISTOGRAM_BUCKETS = ImmutableList.of(0.01, 0.1);
private static final ImmutableList<Long> LONG_HISTOGRAM_BUCKETS = ImmutableList.of(1L, 10L);
private static final String METRIC_NAME_1 = "testMetric1";
private static final String DESCRIPTION_1 = "description1";
private static final String DESCRIPTION_2 = "description2";
private static final String UNIT_1 = "unit1";
private static final String UNIT_2 = "unit2";
private static final boolean ENABLED = true;
private static final boolean DISABLED = false;
private MetricInstrumentRegistry registry = new MetricInstrumentRegistry();
@Test
public void registerDoubleCounterSuccess() {
DoubleCounterMetricInstrument instrument = registry.registerDoubleCounter(
METRIC_NAME_1, DESCRIPTION_1, UNIT_1, REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED);
assertThat(registry.getMetricInstruments().contains(instrument)).isTrue();
assertThat(registry.getMetricInstruments().size()).isEqualTo(1);
assertThat(instrument.getName()).isEqualTo(METRIC_NAME_1);
assertThat(instrument.getDescription()).isEqualTo(DESCRIPTION_1);
assertThat(instrument.getUnit()).isEqualTo(UNIT_1);
assertThat(instrument.getRequiredLabelKeys()).isEqualTo(REQUIRED_LABEL_KEYS);
assertThat(instrument.getOptionalLabelKeys()).isEqualTo(OPTIONAL_LABEL_KEYS);
assertThat(instrument.isEnableByDefault()).isTrue();
}
@Test
public void registerLongCounterSuccess() {
LongCounterMetricInstrument instrument2 = registry.registerLongCounter(
METRIC_NAME_1, DESCRIPTION_1, UNIT_1, REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED);
assertThat(registry.getMetricInstruments().contains(instrument2)).isTrue();
assertThat(registry.getMetricInstruments().size()).isEqualTo(1);
assertThat(instrument2.getName()).isEqualTo(METRIC_NAME_1);
assertThat(instrument2.getDescription()).isEqualTo(DESCRIPTION_1);
assertThat(instrument2.getUnit()).isEqualTo(UNIT_1);
assertThat(instrument2.getRequiredLabelKeys()).isEqualTo(REQUIRED_LABEL_KEYS);
assertThat(instrument2.getOptionalLabelKeys()).isEqualTo(OPTIONAL_LABEL_KEYS);
assertThat(instrument2.isEnableByDefault()).isTrue();
}
@Test
public void registerDoubleHistogramSuccess() {
DoubleHistogramMetricInstrument instrument3 = registry.registerDoubleHistogram(
METRIC_NAME_1, DESCRIPTION_1, UNIT_1, DOUBLE_HISTOGRAM_BUCKETS, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, ENABLED);
assertThat(registry.getMetricInstruments().contains(instrument3)).isTrue();
assertThat(registry.getMetricInstruments().size()).isEqualTo(1);
assertThat(instrument3.getName()).isEqualTo(METRIC_NAME_1);
assertThat(instrument3.getDescription()).isEqualTo(DESCRIPTION_1);
assertThat(instrument3.getUnit()).isEqualTo(UNIT_1);
assertThat(instrument3.getBucketBoundaries()).isEqualTo(DOUBLE_HISTOGRAM_BUCKETS);
assertThat(instrument3.getRequiredLabelKeys()).isEqualTo(REQUIRED_LABEL_KEYS);
assertThat(instrument3.getOptionalLabelKeys()).isEqualTo(OPTIONAL_LABEL_KEYS);
assertThat(instrument3.isEnableByDefault()).isTrue();
}
@Test
public void registerLongHistogramSuccess() {
LongHistogramMetricInstrument instrument4 = registry.registerLongHistogram(
METRIC_NAME_1, DESCRIPTION_1, UNIT_1, LONG_HISTOGRAM_BUCKETS, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, ENABLED);
assertThat(registry.getMetricInstruments().contains(instrument4)).isTrue();
assertThat(registry.getMetricInstruments().size()).isEqualTo(1);
assertThat(instrument4.getName()).isEqualTo(METRIC_NAME_1);
assertThat(instrument4.getDescription()).isEqualTo(DESCRIPTION_1);
assertThat(instrument4.getUnit()).isEqualTo(UNIT_1);
assertThat(instrument4.getBucketBoundaries()).isEqualTo(LONG_HISTOGRAM_BUCKETS);
assertThat(instrument4.getRequiredLabelKeys()).isEqualTo(REQUIRED_LABEL_KEYS);
assertThat(instrument4.getOptionalLabelKeys()).isEqualTo(OPTIONAL_LABEL_KEYS);
assertThat(instrument4.isEnableByDefault()).isTrue();
}
@Test
public void registerLongGaugeSuccess() {
LongGaugeMetricInstrument instrument4 = registry.registerLongGauge(
METRIC_NAME_1, DESCRIPTION_1, UNIT_1, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, ENABLED);
assertThat(registry.getMetricInstruments().contains(instrument4)).isTrue();
assertThat(registry.getMetricInstruments().size()).isEqualTo(1);
assertThat(instrument4.getName()).isEqualTo(METRIC_NAME_1);
assertThat(instrument4.getDescription()).isEqualTo(DESCRIPTION_1);
assertThat(instrument4.getUnit()).isEqualTo(UNIT_1);
assertThat(instrument4.getRequiredLabelKeys()).isEqualTo(REQUIRED_LABEL_KEYS);
assertThat(instrument4.getOptionalLabelKeys()).isEqualTo(OPTIONAL_LABEL_KEYS);
assertThat(instrument4.isEnableByDefault()).isTrue();
}
@Test(expected = IllegalStateException.class)
public void registerDoubleCounterDuplicateName() {
registry.registerDoubleCounter(METRIC_NAME_1, DESCRIPTION_1, UNIT_1, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, ENABLED);
registry.registerDoubleCounter(METRIC_NAME_1, DESCRIPTION_2, UNIT_2, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, DISABLED);
}
@Test(expected = IllegalStateException.class)
public void registerLongCounterDuplicateName() {
registry.registerDoubleCounter(METRIC_NAME_1, DESCRIPTION_1, UNIT_1, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, ENABLED);
registry.registerLongCounter(METRIC_NAME_1, DESCRIPTION_2, UNIT_2, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, DISABLED);
}
@Test(expected = IllegalStateException.class)
public void registerDoubleHistogramDuplicateName() {
registry.registerLongHistogram(METRIC_NAME_1, DESCRIPTION_1, UNIT_1, LONG_HISTOGRAM_BUCKETS,
REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED);
registry.registerDoubleHistogram(METRIC_NAME_1, DESCRIPTION_2, UNIT_2, DOUBLE_HISTOGRAM_BUCKETS,
REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, DISABLED);
}
@Test(expected = IllegalStateException.class)
public void registerLongHistogramDuplicateName() {
registry.registerLongCounter(METRIC_NAME_1, DESCRIPTION_1, UNIT_1, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, ENABLED);
registry.registerLongHistogram(METRIC_NAME_1, DESCRIPTION_2, UNIT_2, LONG_HISTOGRAM_BUCKETS,
REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, DISABLED);
}
@Test(expected = IllegalStateException.class)
public void registerLongGaugeDuplicateName() {
registry.registerDoubleHistogram(METRIC_NAME_1, DESCRIPTION_1, UNIT_1, DOUBLE_HISTOGRAM_BUCKETS,
REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED);
registry.registerLongGauge(METRIC_NAME_1, DESCRIPTION_2, UNIT_2, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, DISABLED);
}
@Test
public void getMetricInstrumentsMultipleRegistered() {
DoubleCounterMetricInstrument instrument1 = registry.registerDoubleCounter(
"testMetric1", DESCRIPTION_1, UNIT_1, REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED);
LongCounterMetricInstrument instrument2 = registry.registerLongCounter(
"testMetric2", DESCRIPTION_2, UNIT_2, REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, DISABLED);
DoubleHistogramMetricInstrument instrument3 = registry.registerDoubleHistogram(
"testMetric3", DESCRIPTION_2, UNIT_2, DOUBLE_HISTOGRAM_BUCKETS, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, DISABLED);
List<MetricInstrument> instruments = registry.getMetricInstruments();
assertThat(instruments.size()).isEqualTo(3);
assertThat(instruments.contains(instrument1)).isTrue();
assertThat(instruments.contains(instrument2)).isTrue();
assertThat(instruments.contains(instrument3)).isTrue();
}
@Test
public void resizeMetricInstrumentsCapacityIncrease() {
int initialCapacity = INITIAL_INSTRUMENT_CAPACITY;
MetricInstrumentRegistry testRegistry = new MetricInstrumentRegistry();
// Registering enough instruments to trigger resize
for (int i = 0; i < initialCapacity + 1; i++) {
testRegistry.registerLongHistogram("name" + i, "desc", "unit", ImmutableList.of(),
ImmutableList.of(), ImmutableList.of(), true);
}
assertThat(testRegistry.getMetricInstruments().size()).isGreaterThan(initialCapacity);
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2024 The gRPC Authors
*
* 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 io.grpc;
/**
* Accesses test-only methods of {@link MetricInstrumentRegistry}.
*/
public final class MetricInstrumentRegistryAccessor {
private MetricInstrumentRegistryAccessor() {
}
public static MetricInstrumentRegistry createMetricInstrumentRegistry() {
return new MetricInstrumentRegistry();
}
}

View File

@ -0,0 +1,174 @@
/*
* Copyright 2024 The gRPC Authors
*
* 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 io.grpc.internal;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.annotations.VisibleForTesting;
import io.grpc.DoubleCounterMetricInstrument;
import io.grpc.DoubleHistogramMetricInstrument;
import io.grpc.LongCounterMetricInstrument;
import io.grpc.LongHistogramMetricInstrument;
import io.grpc.MetricInstrument;
import io.grpc.MetricInstrumentRegistry;
import io.grpc.MetricRecorder;
import io.grpc.MetricSink;
import java.util.List;
/**
* Provides a central point for gRPC components to record metric values. Metrics can be exported to
* monitoring systems by configuring one or more {@link MetricSink}s.
*
* <p>This class encapsulates the interaction with metric sinks, including updating them with
* the latest set of {@link MetricInstrument}s provided by the {@link MetricInstrumentRegistry}.
*/
final class MetricRecorderImpl implements MetricRecorder {
private final List<MetricSink> metricSinks;
private final MetricInstrumentRegistry registry;
@VisibleForTesting
MetricRecorderImpl(List<MetricSink> metricSinks, MetricInstrumentRegistry registry) {
this.metricSinks = metricSinks;
this.registry = registry;
}
/**
* Records a double counter value.
*
* @param metricInstrument the {@link DoubleCounterMetricInstrument} to record.
* @param value the value to record.
* @param requiredLabelValues the required label values for the metric.
* @param optionalLabelValues the optional label values for the metric.
*/
@Override
public void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, double value,
List<String> requiredLabelValues, List<String> optionalLabelValues) {
checkArgument(requiredLabelValues != null
&& requiredLabelValues.size() == metricInstrument.getRequiredLabelKeys().size(),
"Incorrect number of required labels provided. Expected: "
+ metricInstrument.getRequiredLabelKeys().size());
checkArgument(optionalLabelValues != null
&& optionalLabelValues.size() == metricInstrument.getOptionalLabelKeys().size(),
"Incorrect number of optional labels provided. Expected: "
+ metricInstrument.getOptionalLabelKeys().size());
for (MetricSink sink : metricSinks) {
// TODO(dnvindhya): Move updating measures logic from sink to here
List<Object> measures = sink.getMetricsMeasures();
if (measures.size() <= metricInstrument.getIndex()) {
// Measures may need updating in two cases:
// 1. When the sink is initially created with an empty list of measures.
// 2. When new metric instruments are registered, requiring the sink to accommodate them.
sink.updateMeasures(registry.getMetricInstruments());
}
sink.recordDoubleCounter(metricInstrument, value, requiredLabelValues, optionalLabelValues);
}
}
/**
* Records a long counter value.
*
* @param metricInstrument the {@link LongCounterMetricInstrument} to record.
* @param value the value to record.
* @param requiredLabelValues the required label values for the metric.
* @param optionalLabelValues the optional label values for the metric.
*/
@Override
public void addLongCounter(LongCounterMetricInstrument metricInstrument, long value,
List<String> requiredLabelValues, List<String> optionalLabelValues) {
checkArgument(requiredLabelValues != null
&& requiredLabelValues.size() == metricInstrument.getRequiredLabelKeys().size(),
"Incorrect number of required labels provided. Expected: "
+ metricInstrument.getRequiredLabelKeys().size());
checkArgument(optionalLabelValues != null
&& optionalLabelValues.size() == metricInstrument.getOptionalLabelKeys().size(),
"Incorrect number of optional labels provided. Expected: "
+ metricInstrument.getOptionalLabelKeys().size());
for (MetricSink sink : metricSinks) {
List<Object> measures = sink.getMetricsMeasures();
if (measures.size() <= metricInstrument.getIndex()) {
// Measures may need updating in two cases:
// 1. When the sink is initially created with an empty list of measures.
// 2. When new metric instruments are registered, requiring the sink to accommodate them.
sink.updateMeasures(registry.getMetricInstruments());
}
sink.recordLongCounter(metricInstrument, value, requiredLabelValues, optionalLabelValues);
}
}
/**
* Records a double histogram value.
*
* @param metricInstrument the {@link DoubleHistogramMetricInstrument} to record.
* @param value the value to record.
* @param requiredLabelValues the required label values for the metric.
* @param optionalLabelValues the optional label values for the metric.
*/
@Override
public void recordDoubleHistogram(DoubleHistogramMetricInstrument metricInstrument, double value,
List<String> requiredLabelValues, List<String> optionalLabelValues) {
checkArgument(requiredLabelValues != null
&& requiredLabelValues.size() == metricInstrument.getRequiredLabelKeys().size(),
"Incorrect number of required labels provided. Expected: "
+ metricInstrument.getRequiredLabelKeys().size());
checkArgument(optionalLabelValues != null
&& optionalLabelValues.size() == metricInstrument.getOptionalLabelKeys().size(),
"Incorrect number of optional labels provided. Expected: "
+ metricInstrument.getOptionalLabelKeys().size());
for (MetricSink sink : metricSinks) {
List<Object> measures = sink.getMetricsMeasures();
if (measures.size() <= metricInstrument.getIndex()) {
// Measures may need updating in two cases:
// 1. When the sink is initially created with an empty list of measures.
// 2. When new metric instruments are registered, requiring the sink to accommodate them.
sink.updateMeasures(registry.getMetricInstruments());
}
sink.recordDoubleHistogram(metricInstrument, value, requiredLabelValues, optionalLabelValues);
}
}
/**
* Records a long histogram value.
*
* @param metricInstrument the {@link LongHistogramMetricInstrument} to record.
* @param value the value to record.
* @param requiredLabelValues the required label values for the metric.
* @param optionalLabelValues the optional label values for the metric.
*/
@Override
public void recordLongHistogram(LongHistogramMetricInstrument metricInstrument, long value,
List<String> requiredLabelValues, List<String> optionalLabelValues) {
checkArgument(requiredLabelValues != null
&& requiredLabelValues.size() == metricInstrument.getRequiredLabelKeys().size(),
"Incorrect number of required labels provided. Expected: "
+ metricInstrument.getRequiredLabelKeys().size());
checkArgument(optionalLabelValues != null
&& optionalLabelValues.size() == metricInstrument.getOptionalLabelKeys().size(),
"Incorrect number of optional labels provided. Expected: "
+ metricInstrument.getOptionalLabelKeys().size());
for (MetricSink sink : metricSinks) {
List<Object> measures = sink.getMetricsMeasures();
if (measures.size() <= metricInstrument.getIndex()) {
// Measures may need updating in two cases:
// 1. When the sink is initially created with an empty list of measures.
// 2. When new metric instruments are registered, requiring the sink to accommodate them.
sink.updateMeasures(registry.getMetricInstruments());
}
sink.recordLongHistogram(metricInstrument, value, requiredLabelValues, optionalLabelValues);
}
}
}

View File

@ -0,0 +1,224 @@
/*
* Copyright 2024 The gRPC Authors
*
* 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 io.grpc.internal;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import io.grpc.DoubleCounterMetricInstrument;
import io.grpc.DoubleHistogramMetricInstrument;
import io.grpc.LongCounterMetricInstrument;
import io.grpc.LongHistogramMetricInstrument;
import io.grpc.MetricInstrumentRegistry;
import io.grpc.MetricInstrumentRegistryAccessor;
import io.grpc.MetricRecorder;
import io.grpc.MetricSink;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit test for {@link MetricRecorderImpl}.
*/
@RunWith(JUnit4.class)
public class MetricRecorderImplTest {
private static final String DESCRIPTION = "description";
private static final String UNIT = "unit";
private static final boolean ENABLED = true;
private static final ImmutableList<String> REQUIRED_LABEL_KEYS = ImmutableList.of("KEY1", "KEY2");
private static final ImmutableList<String> OPTIONAL_LABEL_KEYS = ImmutableList.of(
"OPTIONAL_KEY_1");
private static final ImmutableList<String> REQUIRED_LABEL_VALUES = ImmutableList.of("VALUE1",
"VALUE2");
private static final ImmutableList<String> OPTIONAL_LABEL_VALUES = ImmutableList.of(
"OPTIONAL_VALUE_1");
private MetricSink mockSink = mock(MetricSink.class);
private List<MetricSink> sinks = Arrays.asList(mockSink, mockSink);
private MetricInstrumentRegistry registry =
MetricInstrumentRegistryAccessor.createMetricInstrumentRegistry();
private final DoubleCounterMetricInstrument doubleCounterInstrument =
registry.registerDoubleCounter("counter0", DESCRIPTION, UNIT, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, ENABLED);
private final LongCounterMetricInstrument longCounterInstrument =
registry.registerLongCounter("counter1", DESCRIPTION, UNIT, REQUIRED_LABEL_KEYS,
OPTIONAL_LABEL_KEYS, ENABLED);
private final DoubleHistogramMetricInstrument doubleHistogramInstrument =
registry.registerDoubleHistogram("histogram1", DESCRIPTION, UNIT,
Collections.emptyList(), REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED);
private final LongHistogramMetricInstrument longHistogramInstrument =
registry.registerLongHistogram("histogram2", DESCRIPTION, UNIT,
Collections.emptyList(), REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED);
private MetricRecorder recorder;
@Before
public void setUp() {
recorder = new MetricRecorderImpl(sinks, registry);
}
@Test
public void recordCounter() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.addDoubleCounter(doubleCounterInstrument, 1.0, REQUIRED_LABEL_VALUES,
OPTIONAL_LABEL_VALUES);
verify(mockSink, times(2)).recordDoubleCounter(eq(doubleCounterInstrument), eq(1D),
eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES));
recorder.addLongCounter(longCounterInstrument, 1, REQUIRED_LABEL_VALUES,
OPTIONAL_LABEL_VALUES);
verify(mockSink, times(2)).recordLongCounter(eq(longCounterInstrument), eq(1L),
eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES));
verify(mockSink, never()).updateMeasures(registry.getMetricInstruments());
}
@Test
public void recordHistogram() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.recordDoubleHistogram(doubleHistogramInstrument, 99.0, REQUIRED_LABEL_VALUES,
OPTIONAL_LABEL_VALUES);
verify(mockSink, times(2)).recordDoubleHistogram(eq(doubleHistogramInstrument),
eq(99D), eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES));
recorder.recordLongHistogram(longHistogramInstrument, 99, REQUIRED_LABEL_VALUES,
OPTIONAL_LABEL_VALUES);
verify(mockSink, times(2)).recordLongHistogram(eq(longHistogramInstrument), eq(99L),
eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES));
verify(mockSink, never()).updateMeasures(registry.getMetricInstruments());
}
@Test
public void newRegisteredMetricUpdateMeasures() {
// Sink is initialized with zero measures, should trigger updateMeasures() on sinks
when(mockSink.getMetricsMeasures()).thenReturn(new ArrayList<>());
// Double Counter
recorder.addDoubleCounter(doubleCounterInstrument, 1.0, REQUIRED_LABEL_VALUES,
OPTIONAL_LABEL_VALUES);
verify(mockSink, times(2)).updateMeasures(anyList());
verify(mockSink, times(2)).recordDoubleCounter(eq(doubleCounterInstrument), eq(1D),
eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES));
// Long Counter
recorder.addLongCounter(longCounterInstrument, 1, REQUIRED_LABEL_VALUES,
OPTIONAL_LABEL_VALUES);
verify(mockSink, times(4)).updateMeasures(anyList());
verify(mockSink, times(2)).recordLongCounter(eq(longCounterInstrument), eq(1L),
eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES));
// Double Histogram
recorder.recordDoubleHistogram(doubleHistogramInstrument, 99.0, REQUIRED_LABEL_VALUES,
OPTIONAL_LABEL_VALUES);
verify(mockSink, times(6)).updateMeasures(anyList());
verify(mockSink, times(2)).recordDoubleHistogram(eq(doubleHistogramInstrument),
eq(99D), eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES));
// Long Histogram
recorder.recordLongHistogram(longHistogramInstrument, 99, REQUIRED_LABEL_VALUES,
OPTIONAL_LABEL_VALUES);
verify(mockSink, times(8)).updateMeasures(registry.getMetricInstruments());
verify(mockSink, times(2)).recordLongHistogram(eq(longHistogramInstrument), eq(99L),
eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES));
}
@Test(expected = IllegalArgumentException.class)
public void recordDoubleCounterMismatchedRequiredLabelValues() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.addDoubleCounter(doubleCounterInstrument, 1.0, ImmutableList.of(),
OPTIONAL_LABEL_VALUES);
}
@Test(expected = IllegalArgumentException.class)
public void recordLongCounterMismatchedRequiredLabelValues() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.addLongCounter(longCounterInstrument, 1, ImmutableList.of(),
OPTIONAL_LABEL_VALUES);
}
@Test(expected = IllegalArgumentException.class)
public void recordDoubleHistogramMismatchedRequiredLabelValues() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.recordDoubleHistogram(doubleHistogramInstrument, 99.0, ImmutableList.of(),
OPTIONAL_LABEL_VALUES);
}
@Test(expected = IllegalArgumentException.class)
public void recordLongHistogramMismatchedRequiredLabelValues() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.recordLongHistogram(longHistogramInstrument, 99, ImmutableList.of(),
OPTIONAL_LABEL_VALUES);
}
@Test(expected = IllegalArgumentException.class)
public void recordDoubleCounterMismatchedOptionalLabelValues() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.addDoubleCounter(doubleCounterInstrument, 1.0, REQUIRED_LABEL_VALUES,
ImmutableList.of());
}
@Test(expected = IllegalArgumentException.class)
public void recordLongCounterMismatchedOptionalLabelValues() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.addLongCounter(longCounterInstrument, 1, REQUIRED_LABEL_VALUES,
ImmutableList.of());
}
@Test(expected = IllegalArgumentException.class)
public void recordDoubleHistogramMismatchedOptionalLabelValues() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.recordDoubleHistogram(doubleHistogramInstrument, 99.0, REQUIRED_LABEL_VALUES,
ImmutableList.of());
}
@Test(expected = IllegalArgumentException.class)
public void recordLongHistogramMismatchedOptionalLabelValues() {
when(mockSink.getMetricsMeasures()).thenReturn(
Arrays.asList(new Object(), new Object(), new Object(), new Object()));
recorder.recordLongHistogram(longHistogramInstrument, 99, REQUIRED_LABEL_VALUES,
ImmutableList.of());
}
}