diff --git a/api/src/main/java/io/grpc/Configurator.java b/api/src/main/java/io/grpc/Configurator.java new file mode 100644 index 0000000000..a904af13d7 --- /dev/null +++ b/api/src/main/java/io/grpc/Configurator.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * Provides hooks for modifying gRPC channels and servers during their construction. + */ +@Internal +public interface Configurator { + /** + * Allows implementations to modify the channel builder. + * + * @param channelBuilder the channel builder being constructed + */ + default void configureChannelBuilder(ManagedChannelBuilder channelBuilder) {} + + /** + * Allows implementations to modify the server builder. + * + * @param serverBuilder the server builder being constructed + */ + default void configureServerBuilder(ServerBuilder serverBuilder) {} +} diff --git a/api/src/main/java/io/grpc/ConfiguratorRegistry.java b/api/src/main/java/io/grpc/ConfiguratorRegistry.java new file mode 100644 index 0000000000..3d6a7aadd6 --- /dev/null +++ b/api/src/main/java/io/grpc/ConfiguratorRegistry.java @@ -0,0 +1,67 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A registry for {@link Configurator} instances. + * + *

This class is responsible for maintaining a list of configurators and providing access to + * them. The default registry can be obtained using {@link #getDefaultRegistry()}. + */ +@Internal +public final class ConfiguratorRegistry { + private static ConfiguratorRegistry instance; + private static boolean isConfiguratorsSet; + private List configurators = Collections.emptyList(); + + ConfiguratorRegistry() {} + + /** + * Returns the default global instance of the configurator registry. + */ + public static synchronized ConfiguratorRegistry getDefaultRegistry() { + if (instance == null) { + instance = new ConfiguratorRegistry(); + } + return instance; + } + + /** + * Sets the configurators in this registry. This method can only be called once. + * + * @param configurators the configurators to set + * @throws IllegalStateException if this method is called more than once + */ + public synchronized void setConfigurators(List configurators) { + if (isConfiguratorsSet) { + throw new IllegalStateException("Configurators are already set"); + } + configurators = Collections.unmodifiableList(new ArrayList<>(configurators)); + isConfiguratorsSet = true; + } + + /** + * Returns a list of the configurators in this registry. + */ + public synchronized List getConfigurators() { + return configurators; + } +} diff --git a/api/src/main/java/io/grpc/DoubleCounterMetricInstrument.java b/api/src/main/java/io/grpc/DoubleCounterMetricInstrument.java index 6657a6d8d4..3f07d83d58 100644 --- a/api/src/main/java/io/grpc/DoubleCounterMetricInstrument.java +++ b/api/src/main/java/io/grpc/DoubleCounterMetricInstrument.java @@ -23,7 +23,7 @@ import java.util.List; */ @Internal public final class DoubleCounterMetricInstrument extends PartialMetricInstrument { - DoubleCounterMetricInstrument(int index, String name, String description, String unit, + public DoubleCounterMetricInstrument(int index, String name, String description, String unit, List requiredLabelKeys, List optionalLabelKeys, boolean enableByDefault) { super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault); } diff --git a/api/src/main/java/io/grpc/DoubleHistogramMetricInstrument.java b/api/src/main/java/io/grpc/DoubleHistogramMetricInstrument.java index 76d1b284da..9039a8c62c 100644 --- a/api/src/main/java/io/grpc/DoubleHistogramMetricInstrument.java +++ b/api/src/main/java/io/grpc/DoubleHistogramMetricInstrument.java @@ -25,7 +25,7 @@ import java.util.List; public final class DoubleHistogramMetricInstrument extends PartialMetricInstrument { private final List bucketBoundaries; - DoubleHistogramMetricInstrument(int index, String name, String description, String unit, + public DoubleHistogramMetricInstrument(int index, String name, String description, String unit, List bucketBoundaries, List requiredLabelKeys, List optionalLabelKeys, boolean enableByDefault) { super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault); diff --git a/api/src/main/java/io/grpc/LongCounterMetricInstrument.java b/api/src/main/java/io/grpc/LongCounterMetricInstrument.java index ab29ba5926..73516dfb9e 100644 --- a/api/src/main/java/io/grpc/LongCounterMetricInstrument.java +++ b/api/src/main/java/io/grpc/LongCounterMetricInstrument.java @@ -23,7 +23,7 @@ import java.util.List; */ @Internal public final class LongCounterMetricInstrument extends PartialMetricInstrument { - LongCounterMetricInstrument(int index, String name, String description, String unit, + public LongCounterMetricInstrument(int index, String name, String description, String unit, List requiredLabelKeys, List optionalLabelKeys, boolean enableByDefault) { super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault); } diff --git a/api/src/main/java/io/grpc/LongGaugeMetricInstrument.java b/api/src/main/java/io/grpc/LongGaugeMetricInstrument.java index c8804e9883..8e24dd715e 100644 --- a/api/src/main/java/io/grpc/LongGaugeMetricInstrument.java +++ b/api/src/main/java/io/grpc/LongGaugeMetricInstrument.java @@ -23,7 +23,7 @@ import java.util.List; */ @Internal public final class LongGaugeMetricInstrument extends PartialMetricInstrument { - LongGaugeMetricInstrument(int index, String name, String description, String unit, + public LongGaugeMetricInstrument(int index, String name, String description, String unit, List requiredLabelKeys, List optionalLabelKeys, boolean enableByDefault) { super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault); } diff --git a/api/src/main/java/io/grpc/LongHistogramMetricInstrument.java b/api/src/main/java/io/grpc/LongHistogramMetricInstrument.java index 87006021f4..2a4e56ffd5 100644 --- a/api/src/main/java/io/grpc/LongHistogramMetricInstrument.java +++ b/api/src/main/java/io/grpc/LongHistogramMetricInstrument.java @@ -25,7 +25,7 @@ import java.util.List; public final class LongHistogramMetricInstrument extends PartialMetricInstrument { private final List bucketBoundaries; - LongHistogramMetricInstrument(int index, String name, String description, String unit, + public LongHistogramMetricInstrument(int index, String name, String description, String unit, List bucketBoundaries, List requiredLabelKeys, List optionalLabelKeys, boolean enableByDefault) { super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault); diff --git a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java index fd74eba281..a61ac058a6 100644 --- a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java +++ b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java @@ -95,6 +95,7 @@ public final class MetricInstrumentRegistry { if (index + 1 == metricInstruments.length) { resizeMetricInstruments(); } + // TODO(dnvindhya): add limit for number of optional labels allowed DoubleCounterMetricInstrument instrument = new DoubleCounterMetricInstrument( index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault); diff --git a/api/src/main/java/io/grpc/MetricSink.java b/api/src/main/java/io/grpc/MetricSink.java index c4b7b19259..8ca7f7978d 100644 --- a/api/src/main/java/io/grpc/MetricSink.java +++ b/api/src/main/java/io/grpc/MetricSink.java @@ -17,6 +17,7 @@ package io.grpc; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -26,50 +27,49 @@ import java.util.Set; public interface MetricSink { /** - * Returns a set of names for the metrics that are currently enabled for this `MetricSink`. + * Returns a set of names for the metrics that are currently enabled or disabled. * * @return A set of enabled metric names. */ - Set getEnabledMetrics(); + Map getEnabledMetrics(); /** - * Returns a list of label names that are considered optional for metrics collected by this - * `MetricSink`. + * Returns a set of optional label names for metrics that the sink actually wants. * - * @return A list of optional label names. + * @return A set of optional label names. */ - List getOptionalLabels(); + Set getOptionalLabels(); /** - * Returns a list of metric measures used to record metric values. These measures are created + * Returns size of metric measures used to record metric values. These measures are created * based on registered metrics (via MetricInstrumentRegistry) and are ordered according to their * registration sequence. * - * @return A list of metric measures. + * @return Size of metric measures. */ - List getMetricsMeasures(); + int getMeasuresSize(); /** - * Records a value for a double-precision counter associated with specified metric instrument. + * Adds a value for a double-precision counter associated with specified metric instrument. * - * @param metricInstrument The counter metric instrument identifies metric measure to record. + * @param metricInstrument The counter metric instrument identifies metric measure to add. * @param value The value to record. * @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 requiredLabelValues, List optionalLabelValues) { } /** - * Records a value for a long valued counter metric associated with specified metric instrument. + * Adds a value for a long valued counter metric associated with specified metric instrument. * - * @param metricInstrument The counter metric instrument identifies metric measure to record. + * @param metricInstrument The counter metric instrument identifies metric measure to add. * @param value The value to record. * @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 requiredLabelValues, List optionalLabelValues) { } @@ -99,5 +99,5 @@ public interface MetricSink { List requiredLabelValues, List optionalLabelValues) { } - default void updateMeasures(List instruments) {} + void updateMeasures(List instruments); } diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 2ba9e34999..eeb769291d 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -72,6 +72,8 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.MetricInstrumentRegistry; +import io.grpc.MetricRecorder; import io.grpc.NameResolver; import io.grpc.NameResolver.ConfigOrError; import io.grpc.NameResolver.ResolutionResult; @@ -588,6 +590,7 @@ final class ManagedChannelImpl extends ManagedChannel implements private final ChannelStreamProvider transportProvider = new ChannelStreamProvider(); private final Rescheduler idleTimer; + private final MetricRecorder metricRecorder; ManagedChannelImpl( ManagedChannelImplBuilder builder, @@ -715,6 +718,8 @@ final class ManagedChannelImpl extends ManagedChannel implements } serviceConfigUpdated = true; } + this.metricRecorder = new MetricRecorderImpl(builder.metricSinks, + MetricInstrumentRegistry.getDefaultRegistry()); } @VisibleForTesting @@ -1743,6 +1748,11 @@ final class ManagedChannelImpl extends ManagedChannel implements return nameResolverRegistry; } + @Override + public MetricRecorder getMetricRecorder() { + return metricRecorder; + } + /** * A placeholder for channel creds if user did not specify channel creds for the channel. */ diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java index 1e40e54775..2c7603ad73 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java @@ -30,12 +30,15 @@ import io.grpc.ChannelCredentials; import io.grpc.ClientInterceptor; import io.grpc.ClientTransportFilter; import io.grpc.CompressorRegistry; +import io.grpc.Configurator; +import io.grpc.ConfiguratorRegistry; import io.grpc.DecompressorRegistry; import io.grpc.EquivalentAddressGroup; import io.grpc.InternalChannelz; import io.grpc.InternalGlobalInterceptors; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import io.grpc.MetricSink; import io.grpc.NameResolver; import io.grpc.NameResolverProvider; import io.grpc.NameResolverRegistry; @@ -192,6 +195,7 @@ public final class ManagedChannelImplBuilder private boolean recordRealTimeMetrics = false; private boolean recordRetryMetrics = true; private boolean tracingEnabled = true; + List metricSinks = new ArrayList<>(); /** * An interface for Transport implementors to provide the {@link ClientTransportFactory} @@ -340,6 +344,10 @@ public final class ManagedChannelImplBuilder } else { this.channelBuilderDefaultPortProvider = new ManagedChannelDefaultPortProvider(); } + // TODO(dnvindhya): Move configurator to all the individual builders + for (Configurator configurator : ConfiguratorRegistry.getDefaultRegistry().getConfigurators()) { + configurator.configureChannelBuilder(this); + } } @Override @@ -661,6 +669,12 @@ public final class ManagedChannelImplBuilder return this; } + @Override + public ManagedChannelImplBuilder addMetricSink(MetricSink metricSink) { + metricSinks.add(checkNotNull(metricSink, "metric sink")); + return this; + } + @Override public ManagedChannel build() { return new ManagedChannelOrphanWrapper(new ManagedChannelImpl( @@ -680,6 +694,7 @@ public final class ManagedChannelImplBuilder List getEffectiveInterceptors() { List effectiveInterceptors = new ArrayList<>(this.interceptors); boolean isGlobalInterceptorsSet = false; + // TODO(dnvindhya) : Convert to Configurator List globalClientInterceptors = InternalGlobalInterceptors.getClientInterceptors(); if (globalClientInterceptors != null) { diff --git a/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java b/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java index eab3e84d09..f718a92675 100644 --- a/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java +++ b/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java @@ -68,14 +68,14 @@ final class MetricRecorderImpl implements MetricRecorder { + metricInstrument.getOptionalLabelKeys().size()); for (MetricSink sink : metricSinks) { // TODO(dnvindhya): Move updating measures logic from sink to here - List measures = sink.getMetricsMeasures(); - if (measures.size() <= metricInstrument.getIndex()) { + int measuresSize = sink.getMeasuresSize(); + if (measuresSize <= 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); + sink.addDoubleCounter(metricInstrument, value, requiredLabelValues, optionalLabelValues); } } @@ -99,14 +99,14 @@ final class MetricRecorderImpl implements MetricRecorder { "Incorrect number of optional labels provided. Expected: " + metricInstrument.getOptionalLabelKeys().size()); for (MetricSink sink : metricSinks) { - List measures = sink.getMetricsMeasures(); - if (measures.size() <= metricInstrument.getIndex()) { + int measuresSize = sink.getMeasuresSize(); + if (measuresSize <= 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); + sink.addLongCounter(metricInstrument, value, requiredLabelValues, optionalLabelValues); } } @@ -130,8 +130,8 @@ final class MetricRecorderImpl implements MetricRecorder { "Incorrect number of optional labels provided. Expected: " + metricInstrument.getOptionalLabelKeys().size()); for (MetricSink sink : metricSinks) { - List measures = sink.getMetricsMeasures(); - if (measures.size() <= metricInstrument.getIndex()) { + int measuresSize = sink.getMeasuresSize(); + if (measuresSize <= 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. @@ -161,8 +161,8 @@ final class MetricRecorderImpl implements MetricRecorder { "Incorrect number of optional labels provided. Expected: " + metricInstrument.getOptionalLabelKeys().size()); for (MetricSink sink : metricSinks) { - List measures = sink.getMetricsMeasures(); - if (measures.size() <= metricInstrument.getIndex()) { + int measuresSize = sink.getMeasuresSize(); + if (measuresSize <= 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. diff --git a/core/src/main/java/io/grpc/internal/ServerImplBuilder.java b/core/src/main/java/io/grpc/internal/ServerImplBuilder.java index cd18457d51..85f9d56d7a 100644 --- a/core/src/main/java/io/grpc/internal/ServerImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ServerImplBuilder.java @@ -25,6 +25,8 @@ import com.google.errorprone.annotations.DoNotCall; import io.grpc.BinaryLog; import io.grpc.BindableService; import io.grpc.CompressorRegistry; +import io.grpc.Configurator; +import io.grpc.ConfiguratorRegistry; import io.grpc.Context; import io.grpc.Deadline; import io.grpc.DecompressorRegistry; @@ -113,6 +115,10 @@ public final class ServerImplBuilder extends ServerBuilder { public ServerImplBuilder(ClientTransportServersBuilder clientTransportServersBuilder) { this.clientTransportServersBuilder = checkNotNull(clientTransportServersBuilder, "clientTransportServersBuilder"); + // TODO(dnvindhya): Move configurator to all the individual builders + for (Configurator configurator : ConfiguratorRegistry.getDefaultRegistry().getConfigurators()) { + configurator.configureServerBuilder(this); + } } @Override diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java index faa03b2031..90c8951cbe 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java @@ -39,6 +39,7 @@ import io.grpc.DecompressorRegistry; import io.grpc.InternalGlobalInterceptors; import io.grpc.ManagedChannel; import io.grpc.MethodDescriptor; +import io.grpc.MetricSink; import io.grpc.NameResolver; import io.grpc.NameResolverRegistry; import io.grpc.StaticTestingClassLoader; @@ -733,4 +734,12 @@ public class ManagedChannelImplBuilderTest { builder.disableServiceConfigLookUp(); assertThat(builder.lookUpServiceConfig).isFalse(); } + + @Test + public void metricSinks() { + MetricSink mocksink = mock(MetricSink.class); + builder.addMetricSink(mocksink); + + assertThat(builder.metricSinks).contains(mocksink); + } } diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 9f7c043d72..6cd88001f8 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -97,10 +97,13 @@ import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; +import io.grpc.LongCounterMetricInstrument; import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.MethodDescriptor.MethodType; +import io.grpc.MetricInstrumentRegistry; +import io.grpc.MetricSink; import io.grpc.NameResolver; import io.grpc.NameResolver.ConfigOrError; import io.grpc.NameResolver.ResolutionResult; @@ -223,6 +226,9 @@ public class ManagedChannelImplTest { private final InternalChannelz channelz = new InternalChannelz(); + private final MetricInstrumentRegistry metricInstrumentRegistry = + MetricInstrumentRegistry.getDefaultRegistry(); + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); private ManagedChannelImpl channel; @@ -655,6 +661,24 @@ public class ManagedChannelImplTest { verify(tracer).addOptionalLabel("routed", "perfectly"); } + @Test + public void metricRecorder_recordsToMetricSink() { + MetricSink mockSink = mock(MetricSink.class); + channelBuilder.addMetricSink(mockSink); + createChannel(); + + LongCounterMetricInstrument counter = metricInstrumentRegistry.registerLongCounter( + "recorder_duration", "Time taken by metric recorder", "s", + ImmutableList.of("grpc.method"), Collections.emptyList(), false); + List requiredLabelValues = ImmutableList.of("testMethod"); + List optionalLabelValues = Collections.emptyList(); + + helper.getMetricRecorder() + .addLongCounter(counter, 32, requiredLabelValues, optionalLabelValues); + verify(mockSink).addLongCounter(eq(counter), eq(32L), eq(requiredLabelValues), + eq(optionalLabelValues)); + } + @Test public void shutdownWithNoTransportsEverCreated() { channelBuilder.nameResolverFactory( diff --git a/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java b/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java index 9f79732c7e..a10694135d 100644 --- a/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java +++ b/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java @@ -33,7 +33,6 @@ 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; @@ -81,18 +80,17 @@ public class MetricRecorderImplTest { } @Test - public void recordCounter() { - when(mockSink.getMetricsMeasures()).thenReturn( - Arrays.asList(new Object(), new Object(), new Object(), new Object())); + public void addCounter() { + when(mockSink.getMeasuresSize()).thenReturn(4); recorder.addDoubleCounter(doubleCounterInstrument, 1.0, REQUIRED_LABEL_VALUES, OPTIONAL_LABEL_VALUES); - verify(mockSink, times(2)).recordDoubleCounter(eq(doubleCounterInstrument), eq(1D), + verify(mockSink, times(2)).addDoubleCounter(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), + verify(mockSink, times(2)).addLongCounter(eq(longCounterInstrument), eq(1L), eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES)); verify(mockSink, never()).updateMeasures(registry.getMetricInstruments()); @@ -100,8 +98,7 @@ public class MetricRecorderImplTest { @Test public void recordHistogram() { - when(mockSink.getMetricsMeasures()).thenReturn( - Arrays.asList(new Object(), new Object(), new Object(), new Object())); + when(mockSink.getMeasuresSize()).thenReturn(4); recorder.recordDoubleHistogram(doubleHistogramInstrument, 99.0, REQUIRED_LABEL_VALUES, OPTIONAL_LABEL_VALUES); @@ -119,20 +116,20 @@ public class MetricRecorderImplTest { @Test public void newRegisteredMetricUpdateMeasures() { // Sink is initialized with zero measures, should trigger updateMeasures() on sinks - when(mockSink.getMetricsMeasures()).thenReturn(new ArrayList<>()); + when(mockSink.getMeasuresSize()).thenReturn(0); // 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), + verify(mockSink, times(2)).addDoubleCounter(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), + verify(mockSink, times(2)).addLongCounter(eq(longCounterInstrument), eq(1L), eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES)); // Double Histogram @@ -151,18 +148,16 @@ public class MetricRecorderImplTest { } @Test(expected = IllegalArgumentException.class) - public void recordDoubleCounterMismatchedRequiredLabelValues() { - when(mockSink.getMetricsMeasures()).thenReturn( - Arrays.asList(new Object(), new Object(), new Object(), new Object())); + public void addDoubleCounterMismatchedRequiredLabelValues() { + when(mockSink.getMeasuresSize()).thenReturn(4); 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())); + public void addLongCounterMismatchedRequiredLabelValues() { + when(mockSink.getMeasuresSize()).thenReturn(4); recorder.addLongCounter(longCounterInstrument, 1, ImmutableList.of(), OPTIONAL_LABEL_VALUES); @@ -170,8 +165,7 @@ public class MetricRecorderImplTest { @Test(expected = IllegalArgumentException.class) public void recordDoubleHistogramMismatchedRequiredLabelValues() { - when(mockSink.getMetricsMeasures()).thenReturn( - Arrays.asList(new Object(), new Object(), new Object(), new Object())); + when(mockSink.getMeasuresSize()).thenReturn(4); recorder.recordDoubleHistogram(doubleHistogramInstrument, 99.0, ImmutableList.of(), OPTIONAL_LABEL_VALUES); @@ -179,26 +173,23 @@ public class MetricRecorderImplTest { @Test(expected = IllegalArgumentException.class) public void recordLongHistogramMismatchedRequiredLabelValues() { - when(mockSink.getMetricsMeasures()).thenReturn( - Arrays.asList(new Object(), new Object(), new Object(), new Object())); + when(mockSink.getMeasuresSize()).thenReturn(4); 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())); + public void addDoubleCounterMismatchedOptionalLabelValues() { + when(mockSink.getMeasuresSize()).thenReturn(4); 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())); + public void addLongCounterMismatchedOptionalLabelValues() { + when(mockSink.getMeasuresSize()).thenReturn(4); recorder.addLongCounter(longCounterInstrument, 1, REQUIRED_LABEL_VALUES, ImmutableList.of()); @@ -206,8 +197,7 @@ public class MetricRecorderImplTest { @Test(expected = IllegalArgumentException.class) public void recordDoubleHistogramMismatchedOptionalLabelValues() { - when(mockSink.getMetricsMeasures()).thenReturn( - Arrays.asList(new Object(), new Object(), new Object(), new Object())); + when(mockSink.getMeasuresSize()).thenReturn(4); recorder.recordDoubleHistogram(doubleHistogramInstrument, 99.0, REQUIRED_LABEL_VALUES, ImmutableList.of()); @@ -215,8 +205,7 @@ public class MetricRecorderImplTest { @Test(expected = IllegalArgumentException.class) public void recordLongHistogramMismatchedOptionalLabelValues() { - when(mockSink.getMetricsMeasures()).thenReturn( - Arrays.asList(new Object(), new Object(), new Object(), new Object())); + when(mockSink.getMeasuresSize()).thenReturn(4); recorder.recordLongHistogram(longHistogramInstrument, 99, REQUIRED_LABEL_VALUES, ImmutableList.of()); diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java new file mode 100644 index 0000000000..720aff79ef --- /dev/null +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java @@ -0,0 +1,263 @@ +/* + * 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.opentelemetry; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.grpc.DoubleCounterMetricInstrument; +import io.grpc.DoubleHistogramMetricInstrument; +import io.grpc.LongCounterMetricInstrument; +import io.grpc.LongHistogramMetricInstrument; +import io.grpc.MetricInstrument; +import io.grpc.MetricSink; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.Meter; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +final class OpenTelemetryMetricSink implements MetricSink { + private static final Logger logger = Logger.getLogger(OpenTelemetryMetricSink.class.getName()); + private final Object lock = new Object(); + private final Meter openTelemetryMeter; + private final Map enableMetrics; + private final boolean disableDefaultMetrics; + private final Set optionalLabels; + private volatile List measures = new ArrayList<>(); + + OpenTelemetryMetricSink(Meter meter, Map enableMetrics, + boolean disableDefaultMetrics, List optionalLabels) { + this.openTelemetryMeter = checkNotNull(meter, "meter"); + this.enableMetrics = ImmutableMap.copyOf(enableMetrics); + this.disableDefaultMetrics = disableDefaultMetrics; + this.optionalLabels = ImmutableSet.copyOf(optionalLabels); + } + + @Override + public Map getEnabledMetrics() { + return enableMetrics; + } + + @Override + public Set getOptionalLabels() { + return optionalLabels; + } + + @Override + public int getMeasuresSize() { + return measures.size(); + } + + @VisibleForTesting + List getMeasures() { + synchronized (lock) { + return Collections.unmodifiableList(measures); + } + } + + @Override + public void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, double value, + List requiredLabelValues, List optionalLabelValues) { + MeasuresData instrumentData = measures.get(metricInstrument.getIndex()); + if (instrumentData == null) { + // Disabled metric + return; + } + Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(), + metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues, + instrumentData.getOptionalLabelsBitSet()); + DoubleCounter counter = (DoubleCounter) instrumentData.getMeasure(); + counter.add(value, attributes); + } + + @Override + public void addLongCounter(LongCounterMetricInstrument metricInstrument, long value, + List requiredLabelValues, List optionalLabelValues) { + MeasuresData instrumentData = measures.get(metricInstrument.getIndex()); + if (instrumentData == null) { + // Disabled metric + return; + } + Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(), + metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues, + instrumentData.getOptionalLabelsBitSet()); + LongCounter counter = (LongCounter) instrumentData.getMeasure(); + counter.add(value, attributes); + } + + @Override + public void recordDoubleHistogram(DoubleHistogramMetricInstrument metricInstrument, double value, + List requiredLabelValues, List optionalLabelValues) { + MeasuresData instrumentData = measures.get(metricInstrument.getIndex()); + if (instrumentData == null) { + // Disabled metric + return; + } + Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(), + metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues, + instrumentData.getOptionalLabelsBitSet()); + DoubleHistogram histogram = (DoubleHistogram) instrumentData.getMeasure(); + histogram.record(value, attributes); + } + + @Override + public void recordLongHistogram(LongHistogramMetricInstrument metricInstrument, long value, + List requiredLabelValues, List optionalLabelValues) { + MeasuresData instrumentData = measures.get(metricInstrument.getIndex()); + if (instrumentData == null) { + // Disabled metric + return; + } + Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(), + metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues, + instrumentData.getOptionalLabelsBitSet()); + LongHistogram histogram = (LongHistogram) instrumentData.getMeasure(); + histogram.record(value, attributes); + } + + @Override + public void updateMeasures(List instruments) { + synchronized (lock) { + if (measures.size() >= instruments.size()) { + // Already up-to-date + return; + } + + List newMeasures = new ArrayList<>(instruments.size()); + // Reuse existing measures + newMeasures.addAll(measures); + + for (int i = measures.size(); i < instruments.size(); i++) { + MetricInstrument instrument = instruments.get(i); + // Check if the metric is disabled + if (!shouldEnableMetric(instrument)) { + // Adding null measure for disabled Metric + newMeasures.add(null); + continue; + } + + BitSet bitSet = new BitSet(instrument.getOptionalLabelKeys().size()); + if (optionalLabels.isEmpty()) { + // initialize an empty list + } else { + List labels = instrument.getOptionalLabelKeys(); + for (int j = 0; j < labels.size(); j++) { + if (optionalLabels.contains(labels.get(j))) { + bitSet.set(j); + } + } + } + + int index = instrument.getIndex(); + String name = instrument.getName(); + String unit = instrument.getUnit(); + String description = instrument.getDescription(); + + Object openTelemetryMeasure; + if (instrument instanceof DoubleCounterMetricInstrument) { + openTelemetryMeasure = openTelemetryMeter.counterBuilder(name) + .setUnit(unit) + .setDescription(description) + .ofDoubles() + .build(); + } else if (instrument instanceof LongCounterMetricInstrument) { + openTelemetryMeasure = openTelemetryMeter.counterBuilder(name) + .setUnit(unit) + .setDescription(description) + .build(); + } else if (instrument instanceof DoubleHistogramMetricInstrument) { + openTelemetryMeasure = openTelemetryMeter.histogramBuilder(name) + .setUnit(unit) + .setDescription(description) + .build(); + } else if (instrument instanceof LongHistogramMetricInstrument) { + openTelemetryMeasure = openTelemetryMeter.histogramBuilder(name) + .setUnit(unit) + .setDescription(description) + .ofLongs() + .build(); + } else { + logger.log(Level.FINE, "Unsupported metric instrument type : {0}", instrument); + openTelemetryMeasure = null; + } + newMeasures.add(index, new MeasuresData(bitSet, openTelemetryMeasure)); + } + + measures = newMeasures; + } + } + + private boolean shouldEnableMetric(MetricInstrument instrument) { + Boolean explicitlyEnabled = enableMetrics.get(instrument.getName()); + if (explicitlyEnabled != null) { + return explicitlyEnabled; + } + return instrument.isEnableByDefault() && !disableDefaultMetrics; + } + + + private Attributes createAttributes(List requiredLabelKeys, + List optionalLabelKeys, + List requiredLabelValues, List optionalLabelValues, BitSet bitSet) { + AttributesBuilder builder = Attributes.builder(); + // Required Labels + for (int i = 0; i < requiredLabelKeys.size(); i++) { + builder.put(requiredLabelKeys.get(i), requiredLabelValues.get(i)); + } + // Optional labels + for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) { + if (i == Integer.MAX_VALUE) { + break; // or (i+1) would overflow + } + builder.put(optionalLabelKeys.get(i), optionalLabelValues.get(i)); + } + return builder.build(); + } + + + static final class MeasuresData { + final BitSet optionalLabelsIndices; + final Object measure; + + MeasuresData(BitSet optionalLabelsIndices, Object measure) { + this.optionalLabelsIndices = optionalLabelsIndices; + this.measure = measure; + } + + public BitSet getOptionalLabelsBitSet() { + return optionalLabelsIndices; + } + + public Object getMeasure() { + return measure; + } + } + +} diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java index 7ba886f725..144e8e52dc 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java @@ -23,6 +23,7 @@ import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.STATUS_KEY; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableSet; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -61,6 +62,17 @@ import javax.annotation.concurrent.GuardedBy; */ final class OpenTelemetryMetricsModule { private static final Logger logger = Logger.getLogger(OpenTelemetryMetricsModule.class.getName()); + public static final ImmutableSet DEFAULT_PER_CALL_METRICS_SET = + ImmutableSet.of( + "grpc.client.attempt.started", + "grpc.client.attempt.duration", + "grpc.client.attempt.sent_total_compressed_message_size", + "grpc.client.attempt.rcvd_total_compressed_message_size", + "grpc.client.call.duration", + "grpc.server.call.started", + "grpc.server.call.duration", + "grpc.server.call.sent_total_compressed_message_size", + "grpc.server.call.rcvd_total_compressed_message_size"); // Using floating point because TimeUnit.NANOSECONDS.toSeconds would discard // fractional seconds. @@ -182,12 +194,18 @@ final class OpenTelemetryMetricsModule { io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName, STATUS_KEY, statusCode.toString()); - module.resource.clientAttemptDurationCounter() - .record(attemptNanos * SECONDS_PER_NANO, attribute); - module.resource.clientTotalSentCompressedMessageSizeCounter() - .record(outboundWireSize, attribute); - module.resource.clientTotalReceivedCompressedMessageSizeCounter() - .record(inboundWireSize, attribute); + if (module.resource.clientAttemptCountCounter() != null ) { + module.resource.clientAttemptDurationCounter() + .record(attemptNanos * SECONDS_PER_NANO, attribute); + } + if (module.resource.clientTotalSentCompressedMessageSizeCounter() != null) { + module.resource.clientTotalSentCompressedMessageSizeCounter() + .record(outboundWireSize, attribute); + } + if (module.resource.clientTotalReceivedCompressedMessageSizeCounter() != null) { + module.resource.clientTotalReceivedCompressedMessageSizeCounter() + .record(inboundWireSize, attribute); + } } } @@ -219,7 +237,9 @@ final class OpenTelemetryMetricsModule { io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName); // Record here in case mewClientStreamTracer() would never be called. - module.resource.clientAttemptCountCounter().add(1, attribute); + if (module.resource.clientAttemptCountCounter() != null) { + module.resource.clientAttemptCountCounter().add(1, attribute); + } } @Override @@ -240,7 +260,9 @@ final class OpenTelemetryMetricsModule { // TODO(dnvindhya): Add target as an attribute io.opentelemetry.api.common.Attributes attribute = io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName); - module.resource.clientAttemptCountCounter().add(1, attribute); + if (module.resource.clientAttemptCountCounter() != null) { + module.resource.clientAttemptCountCounter().add(1, attribute); + } } if (!info.isTransparentRetry()) { attemptsPerCall.incrementAndGet(); @@ -298,8 +320,10 @@ final class OpenTelemetryMetricsModule { io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName, STATUS_KEY, status.getCode().toString()); - module.resource.clientCallDurationCounter() - .record(callLatencyNanos * SECONDS_PER_NANO, attribute); + if (module.resource.clientCallDurationCounter() != null) { + module.resource.clientCallDurationCounter() + .record(callLatencyNanos * SECONDS_PER_NANO, attribute); + } } } @@ -360,7 +384,9 @@ final class OpenTelemetryMetricsModule { io.opentelemetry.api.common.Attributes.of( METHOD_KEY, recordMethodName(fullMethodName, isSampledToLocalTracing)); - module.resource.serverCallCountCounter().add(1, attribute); + if (module.resource.serverCallCountCounter() != null) { + module.resource.serverCallCountCounter().add(1, attribute); + } } @Override @@ -408,12 +434,18 @@ final class OpenTelemetryMetricsModule { METHOD_KEY, recordMethodName(fullMethodName, isGeneratedMethod), STATUS_KEY, status.getCode().toString()); - module.resource.serverCallDurationCounter() - .record(elapsedTimeNanos * SECONDS_PER_NANO, attributes); - module.resource.serverTotalSentCompressedMessageSizeCounter() - .record(outboundWireSize, attributes); - module.resource.serverTotalReceivedCompressedMessageSizeCounter() - .record(inboundWireSize, attributes); + if (module.resource.serverCallDurationCounter() != null) { + module.resource.serverCallDurationCounter() + .record(elapsedTimeNanos * SECONDS_PER_NANO, attributes); + } + if (module.resource.serverTotalSentCompressedMessageSizeCounter() != null) { + module.resource.serverTotalSentCompressedMessageSizeCounter() + .record(outboundWireSize, attributes); + } + if (module.resource.serverTotalReceivedCompressedMessageSizeCounter() != null) { + module.resource.serverTotalReceivedCompressedMessageSizeCounter() + .record(inboundWireSize, attributes); + } } } diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsResource.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsResource.java index a435ec6bca..e519b7e1eb 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsResource.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsResource.java @@ -20,29 +20,39 @@ import com.google.auto.value.AutoValue; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.LongHistogram; +import javax.annotation.Nullable; @AutoValue abstract class OpenTelemetryMetricsResource { /* Client Metrics */ + @Nullable abstract DoubleHistogram clientCallDurationCounter(); + @Nullable abstract LongCounter clientAttemptCountCounter(); + @Nullable abstract DoubleHistogram clientAttemptDurationCounter(); + @Nullable abstract LongHistogram clientTotalSentCompressedMessageSizeCounter(); + @Nullable abstract LongHistogram clientTotalReceivedCompressedMessageSizeCounter(); /* Server Metrics */ + @Nullable abstract LongCounter serverCallCountCounter(); + @Nullable abstract DoubleHistogram serverCallDurationCounter(); + @Nullable abstract LongHistogram serverTotalSentCompressedMessageSizeCounter(); + @Nullable abstract LongHistogram serverTotalReceivedCompressedMessageSizeCounter(); static Builder builder() { diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryModule.java index f2deb60a9b..f01dbcf98d 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryModule.java @@ -22,13 +22,24 @@ import static io.grpc.internal.GrpcUtil.IMPLEMENTATION_VERSION; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.base.Supplier; -import io.grpc.ClientInterceptor; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.grpc.Configurator; +import io.grpc.ConfiguratorRegistry; import io.grpc.ExperimentalApi; -import io.grpc.ServerStreamTracer; +import io.grpc.ManagedChannelBuilder; +import io.grpc.MetricSink; +import io.grpc.ServerBuilder; import io.grpc.opentelemetry.internal.OpenTelemetryConstants; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.MeterProvider; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * The entrypoint for OpenTelemetry metrics functionality in gRPC. @@ -48,28 +59,38 @@ public final class OpenTelemetryModule { } }; - private final OpenTelemetry openTelemetryInstance; + private final OpenTelemetry openTelemetrySdk; private final MeterProvider meterProvider; private final Meter meter; + private final Map enableMetrics; + private final boolean disableDefault; private final OpenTelemetryMetricsResource resource; + private final OpenTelemetryMetricsModule openTelemetryMetricsModule; + private final List optionalLabels; + private final MetricSink sink; public static Builder newBuilder() { return new Builder(); } private OpenTelemetryModule(Builder builder) { - this.openTelemetryInstance = checkNotNull(builder.openTelemetrySdk, "openTelemetrySdk"); - this.meterProvider = checkNotNull(openTelemetryInstance.getMeterProvider(), "meterProvider"); + this.openTelemetrySdk = checkNotNull(builder.openTelemetrySdk, "openTelemetrySdk"); + this.meterProvider = checkNotNull(openTelemetrySdk.getMeterProvider(), "meterProvider"); this.meter = this.meterProvider .meterBuilder(OpenTelemetryConstants.INSTRUMENTATION_SCOPE) .setInstrumentationVersion(IMPLEMENTATION_VERSION) .build(); - this.resource = createMetricInstruments(meter); + this.enableMetrics = ImmutableMap.copyOf(builder.enableMetrics); + this.disableDefault = builder.disableAll; + this.resource = createMetricInstruments(meter, enableMetrics, disableDefault); + this.openTelemetryMetricsModule = new OpenTelemetryMetricsModule(STOPWATCH_SUPPLIER, resource); + this.optionalLabels = ImmutableList.copyOf(builder.optionalLabels); + this.sink = new OpenTelemetryMetricSink(meter, enableMetrics, disableDefault, optionalLabels); } @VisibleForTesting OpenTelemetry getOpenTelemetryInstance() { - return this.openTelemetryInstance; + return this.openTelemetrySdk; } @VisibleForTesting @@ -87,105 +108,170 @@ public final class OpenTelemetryModule { return this.resource; } - /** - * Returns a {@link ClientInterceptor} with metrics implementation. - */ - public ClientInterceptor getClientInterceptor() { - OpenTelemetryMetricsModule openTelemetryMetricsModule = - new OpenTelemetryMetricsModule( - STOPWATCH_SUPPLIER, - resource); - return openTelemetryMetricsModule.getClientInterceptor(); - } - - /** - * Returns a {@link ServerStreamTracer.Factory} with metrics implementation. - */ - public ServerStreamTracer.Factory getServerStreamTracerFactory() { - OpenTelemetryMetricsModule openTelemetryMetricsModule = - new OpenTelemetryMetricsModule( - STOPWATCH_SUPPLIER, - resource); - return openTelemetryMetricsModule.getServerTracerFactory(); + @VisibleForTesting + Map getEnableMetrics() { + return this.enableMetrics; } @VisibleForTesting - static OpenTelemetryMetricsResource createMetricInstruments(Meter meter) { + List getOptionalLabels() { + return optionalLabels; + } + + MetricSink getSink() { + return sink; + } + + /** + * Registers OpenTelemetryModule globally, applying its configuration to all subsequently created + * gRPC channels and servers. + */ + public void registerGlobal() { + ConfiguratorRegistry.getDefaultRegistry().setConfigurators(Collections.singletonList( + new Configurator() { + @Override + public void configureChannelBuilder(ManagedChannelBuilder channelBuilder) { + OpenTelemetryModule.this.configureChannelBuilder(channelBuilder); + } + + @Override + public void configureServerBuilder(ServerBuilder serverBuilder) { + OpenTelemetryModule.this.configureServerBuilder(serverBuilder); + } + })); + } + + /** + * Configures the given {@link ManagedChannelBuilder} with OpenTelemetry metrics instrumentation. + */ + public void configureChannelBuilder(ManagedChannelBuilder builder) { + builder.addMetricSink(sink); + builder.intercept(openTelemetryMetricsModule.getClientInterceptor()); + } + + /** + * Configures the given {@link ServerBuilder} with OpenTelemetry metrics instrumentation. + * + * @param serverBuilder the server builder to configure + */ + public void configureServerBuilder(ServerBuilder serverBuilder) { + serverBuilder.addStreamTracerFactory(openTelemetryMetricsModule.getServerTracerFactory()); + } + + @VisibleForTesting + static OpenTelemetryMetricsResource createMetricInstruments(Meter meter, + Map enableMetrics, boolean disableDefault) { OpenTelemetryMetricsResource.Builder builder = OpenTelemetryMetricsResource.builder(); - builder.clientCallDurationCounter( - meter.histogramBuilder("grpc.client.call.duration") - .setUnit("s") - .setDescription( - "Time taken by gRPC to complete an RPC from application's perspective") - .build()); + if (isMetricEnabled("grpc.client.call.duration", enableMetrics, disableDefault)) { + builder.clientCallDurationCounter( + meter.histogramBuilder("grpc.client.call.duration") + .setUnit("s") + .setDescription( + "Time taken by gRPC to complete an RPC from application's perspective") + .build()); + } - builder.clientAttemptCountCounter( - meter.counterBuilder("grpc.client.attempt.started") - .setUnit("{attempt}") - .setDescription("Number of client call attempts started") - .build()); + if (isMetricEnabled("grpc.client.attempt.started", enableMetrics, disableDefault)) { + builder.clientAttemptCountCounter( + meter.counterBuilder("grpc.client.attempt.started") + .setUnit("{attempt}") + .setDescription("Number of client call attempts started") + .build()); + } - builder.clientAttemptDurationCounter( - meter.histogramBuilder( - "grpc.client.attempt.duration") - .setUnit("s") - .setDescription("Time taken to complete a client call attempt") - .build()); + if (isMetricEnabled("grpc.client.attempt.duration", enableMetrics, disableDefault)) { + builder.clientAttemptDurationCounter( + meter.histogramBuilder( + "grpc.client.attempt.duration") + .setUnit("s") + .setDescription("Time taken to complete a client call attempt") + .build()); + } - builder.clientTotalSentCompressedMessageSizeCounter( - meter.histogramBuilder( - "grpc.client.attempt.sent_total_compressed_message_size") - .setUnit("By") - .setDescription("Compressed message bytes sent per client call attempt") - .ofLongs() - .build()); + if (isMetricEnabled("grpc.client.attempt.sent_total_compressed_message_size", enableMetrics, + disableDefault)) { + builder.clientTotalSentCompressedMessageSizeCounter( + meter.histogramBuilder( + "grpc.client.attempt.sent_total_compressed_message_size") + .setUnit("By") + .setDescription("Compressed message bytes sent per client call attempt") + .ofLongs() + .build()); + } - builder.clientTotalReceivedCompressedMessageSizeCounter( - meter.histogramBuilder( - "grpc.client.attempt.rcvd_total_compressed_message_size") - .setUnit("By") - .setDescription("Compressed message bytes received per call attempt") - .ofLongs() - .build()); + if (isMetricEnabled("grpc.client.attempt.rcvd_total_compressed_message_size", enableMetrics, + disableDefault)) { + builder.clientTotalReceivedCompressedMessageSizeCounter( + meter.histogramBuilder( + "grpc.client.attempt.rcvd_total_compressed_message_size") + .setUnit("By") + .setDescription("Compressed message bytes received per call attempt") + .ofLongs() + .build()); + } - builder.serverCallCountCounter( - meter.counterBuilder("grpc.server.call.started") - .setUnit("{call}") - .setDescription("Number of server calls started") - .build()); + if (isMetricEnabled("grpc.server.call.started", enableMetrics, disableDefault)) { + builder.serverCallCountCounter( + meter.counterBuilder("grpc.server.call.started") + .setUnit("{call}") + .setDescription("Number of server calls started") + .build()); + } - builder.serverCallDurationCounter( - meter.histogramBuilder("grpc.server.call.duration") - .setUnit("s") - .setDescription( - "Time taken to complete a call from server transport's perspective") - .build()); + if (isMetricEnabled("grpc.server.call.duration", enableMetrics, disableDefault)) { + builder.serverCallDurationCounter( + meter.histogramBuilder("grpc.server.call.duration") + .setUnit("s") + .setDescription( + "Time taken to complete a call from server transport's perspective") + .build()); + } - builder.serverTotalSentCompressedMessageSizeCounter( - meter.histogramBuilder( - "grpc.server.call.sent_total_compressed_message_size") - .setUnit("By") - .setDescription("Compressed message bytes sent per server call") - .ofLongs() - .build()); + if (isMetricEnabled("grpc.server.call.sent_total_compressed_message_size", enableMetrics, + disableDefault)) { + builder.serverTotalSentCompressedMessageSizeCounter( + meter.histogramBuilder( + "grpc.server.call.sent_total_compressed_message_size") + .setUnit("By") + .setDescription("Compressed message bytes sent per server call") + .ofLongs() + .build()); + } - builder.serverTotalReceivedCompressedMessageSizeCounter( - meter.histogramBuilder( - "grpc.server.call.rcvd_total_compressed_message_size") - .setUnit("By") - .setDescription("Compressed message bytes received per server call") - .ofLongs() - .build()); + if (isMetricEnabled("grpc.server.call.rcvd_total_compressed_message_size", enableMetrics, + disableDefault)) { + builder.serverTotalReceivedCompressedMessageSizeCounter( + meter.histogramBuilder( + "grpc.server.call.rcvd_total_compressed_message_size") + .setUnit("By") + .setDescription("Compressed message bytes received per server call") + .ofLongs() + .build()); + } return builder.build(); } + static boolean isMetricEnabled(String metricName, Map enableMetrics, + boolean disableDefault) { + Boolean explicitlyEnabled = enableMetrics.get(metricName); + if (explicitlyEnabled != null) { + return explicitlyEnabled; + } + return OpenTelemetryMetricsModule.DEFAULT_PER_CALL_METRICS_SET.contains(metricName) + && !disableDefault; + } + + /** * Builder for configuring {@link OpenTelemetryModule}. */ public static class Builder { private OpenTelemetry openTelemetrySdk = OpenTelemetry.noop(); + private final Collection optionalLabels = new ArrayList<>(); + private final Map enableMetrics = new HashMap<>(); + private boolean disableAll; private Builder() {} @@ -199,6 +285,45 @@ public final class OpenTelemetryModule { return this; } + /** + * Adds optionalLabelKey to all the metrics that can provide value for the + * optionalLabelKey. + */ + public Builder addOptionalLabel(String optionalLabelKey) { + this.optionalLabels.add(optionalLabelKey); + return this; + } + + /** + * Enables the specified metrics for collection and export. By default, only a subset of + * metrics are enabled. + */ + public Builder enableMetrics(Collection enableMetrics) { + for (String metric : enableMetrics) { + this.enableMetrics.put(metric, true); + } + return this; + } + + /** + * Disables the specified metrics from being collected and exported. + */ + public Builder disableMetrics(Collection disableMetrics) { + for (String metric : disableMetrics) { + this.enableMetrics.put(metric, false); + } + return this; + } + + /** + * Disable all metrics. If set to true all metrics must be explicitly enabled. + */ + public Builder disableAllMetrics() { + this.enableMetrics.clear(); + this.disableAll = true; + return this; + } + /** * Returns a new {@link OpenTelemetryModule} built with the configuration of this {@link * Builder}. diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java new file mode 100644 index 0000000000..9fa5806731 --- /dev/null +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java @@ -0,0 +1,347 @@ +/* + * 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.opentelemetry; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +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.MetricInstrument; +import io.grpc.opentelemetry.internal.OpenTelemetryConstants; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class OpenTelemetryMetricSinkTest { + + @Rule + public final OpenTelemetryRule openTelemetryTesting = OpenTelemetryRule.create(); + + private final Meter testMeter = openTelemetryTesting.getOpenTelemetry() + .getMeter(OpenTelemetryConstants.INSTRUMENTATION_SCOPE); + + private OpenTelemetryMetricSink sink; + + @Test + public void updateMeasures_enabledMetrics() { + Map enabledMetrics = new HashMap<>(); + enabledMetrics.put("client_calls_started", true); + enabledMetrics.put("server_calls_started", true); + + List optionalLabels = Arrays.asList("status"); + + List instruments = Arrays.asList( + new DoubleCounterMetricInstrument(0, "client_calls_started", + "Number of client calls started", "count", Collections.emptyList(), + Collections.emptyList(), + true), + new LongCounterMetricInstrument(1, "server_calls_started", "Number of server calls started", + "count", Collections.emptyList(), Collections.emptyList(), false), + new DoubleHistogramMetricInstrument(2, "client_message_size", "Sent message size", "bytes", + Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), true) + ); + + // Create sink + sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, false, optionalLabels); + + // Invoke updateMeasures + sink.updateMeasures(instruments); + + com.google.common.truth.Truth.assertThat(sink.getMeasuresSize()).isEqualTo(3); + // Metric is explicitly enabled for sink + com.google.common.truth.Truth.assertThat(sink.getMeasures().get(0).getMeasure()) + .isInstanceOf(DoubleCounter.class); + // Metric is explicitly enabled for sink + com.google.common.truth.Truth.assertThat(sink.getMeasures().get(1).getMeasure()) + .isInstanceOf(LongCounter.class); + // Metric is enabled by default + com.google.common.truth.Truth.assertThat(sink.getMeasures().get(2).getMeasure()) + .isInstanceOf(DoubleHistogram.class); + + } + + @Test + public void updateMeasure_disabledMetrics() { + Map enabledMetrics = new HashMap<>(); + enabledMetrics.put("client_calls_started", false); + enabledMetrics.put("server_calls_started", false); + + List optionalLabels = Arrays.asList("status"); + + List instruments = Arrays.asList( + new DoubleCounterMetricInstrument(0, "client_calls_started", + "Number of client calls started", "count", Collections.emptyList(), + Collections.emptyList(), true), + new LongCounterMetricInstrument(1, "server_calls_started", "Number of server calls started", + "count", Collections.emptyList(), Collections.emptyList(), true), + new DoubleHistogramMetricInstrument(2, "client_message_size", "Sent message size", "bytes", + Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), true) + ); + + // Create sink + sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, true, optionalLabels); + + // Invoke updateMeasures + sink.updateMeasures(instruments); + + com.google.common.truth.Truth.assertThat(sink.getMeasuresSize()).isEqualTo(3); + // Metric is explicitly disabled + com.google.common.truth.Truth.assertThat(sink.getMeasures().get(0)).isNull(); + // Metric is explicitly disabled + com.google.common.truth.Truth.assertThat(sink.getMeasures().get(1)).isNull(); + // Metric is enabled by default, but all default metrics are disabled + com.google.common.truth.Truth.assertThat(sink.getMeasures().get(2)).isNull(); + + } + + @Test + public void addCounter_enabledMetric() { + // set up sink with disabled metric + Map enabledMetrics = new HashMap<>(); + enabledMetrics.put("client_latency", true); + + LongCounterMetricInstrument longCounterInstrument = + new LongCounterMetricInstrument(0, "client_latency", "Client latency", "s", + Collections.emptyList(), + Collections.emptyList(), false); + DoubleCounterMetricInstrument doubleCounterInstrument = + new DoubleCounterMetricInstrument(1, "client_calls_started", + "Number of client calls started", "count", Collections.emptyList(), + Collections.emptyList(), + true); + // Create sink + sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, false, Collections.emptyList()); + + // Invoke updateMeasures + sink.updateMeasures(Arrays.asList(longCounterInstrument, doubleCounterInstrument)); + + sink.addLongCounter(longCounterInstrument, 123L, Collections.emptyList(), + Collections.emptyList()); + sink.addDoubleCounter(doubleCounterInstrument, 12.0, Collections.emptyList(), + Collections.emptyList()); + + assertThat(openTelemetryTesting.getMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName("client_latency") + .hasDescription("Client latency") + .hasUnit("s") + .hasLongSumSatisfying( + longSum -> + longSum + .hasPointsSatisfying( + point -> + point + .hasValue(123L))), + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName("client_calls_started") + .hasDescription("Number of client calls started") + .hasUnit("count") + .hasDoubleSumSatisfying( + doubleSum -> + doubleSum + .hasPointsSatisfying( + point -> + point + .hasValue(12.0D)))); + } + + @Test + public void addCounter_disabledMetric() { + // set up sink with disabled metric + Map enabledMetrics = new HashMap<>(); + enabledMetrics.put("client_latency", false); + + LongCounterMetricInstrument instrument = + new LongCounterMetricInstrument(0, "client_latency", "Client latency", "s", + Collections.emptyList(), + Collections.emptyList(), true); + // Create sink + sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, true, Collections.emptyList()); + + // Invoke updateMeasures + sink.updateMeasures(Arrays.asList(instrument)); + + sink.addLongCounter(instrument, 123L, Collections.emptyList(), Collections.emptyList()); + + assertThat(openTelemetryTesting.getMetrics()).isEmpty(); + } + + @Test + public void addHistogram_enabledMetric() { + // set up sink with disabled metric + Map enabledMetrics = new HashMap<>(); + enabledMetrics.put("client_message_size", true); + enabledMetrics.put("server_message_size", true); + + DoubleHistogramMetricInstrument doubleHistogramInstrument = + new DoubleHistogramMetricInstrument(0, "client_message_size", "Sent message size", "bytes", + Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), false); + LongHistogramMetricInstrument longHistogramInstrument = + new LongHistogramMetricInstrument(1, "server_message_size", "Received message size", + "bytes", + Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), true); + + // Create sink + sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, false, Collections.emptyList()); + + // Invoke updateMeasures + sink.updateMeasures(Arrays.asList(doubleHistogramInstrument, longHistogramInstrument)); + + sink.recordDoubleHistogram(doubleHistogramInstrument, 12.0, Collections.emptyList(), + Collections.emptyList()); + sink.recordLongHistogram(longHistogramInstrument, 123L, Collections.emptyList(), + Collections.emptyList()); + + assertThat(openTelemetryTesting.getMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName("client_message_size") + .hasDescription("Sent message size") + .hasUnit("bytes") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasCount(1) + .hasSum(12.0))), + + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName("server_message_size") + .hasDescription("Received message size") + .hasUnit("bytes") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasCount(1) + .hasSum(123L)))); + } + + @Test + public void addHistogram_disabledMetric() { + // set up sink with disabled metric + Map enabledMetrics = new HashMap<>(); + enabledMetrics.put("client_message_size", false); + enabledMetrics.put("server_message_size", false); + + DoubleHistogramMetricInstrument doubleHistogramInstrument = + new DoubleHistogramMetricInstrument(0, "client_message_size", "Sent message size", "bytes", + Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), false); + LongHistogramMetricInstrument longHistogramInstrument = + new LongHistogramMetricInstrument(1, "server_message_size", "Received message size", + "bytes", + Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), true); + + // Create sink + sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, false, Collections.emptyList()); + + // Invoke updateMeasures + sink.updateMeasures(Arrays.asList(doubleHistogramInstrument, longHistogramInstrument)); + + sink.recordDoubleHistogram(doubleHistogramInstrument, 12.0, Collections.emptyList(), + Collections.emptyList()); + sink.recordLongHistogram(longHistogramInstrument, 123L, Collections.emptyList(), + Collections.emptyList()); + + assertThat(openTelemetryTesting.getMetrics()).isEmpty(); + } + + @Test + public void recordLabels() { + Map enabledMetrics = new HashMap<>(); + enabledMetrics.put("client_latency", true); + + List optionalLabels = Arrays.asList("optional_label_key_2"); + + LongCounterMetricInstrument longCounterInstrument = + new LongCounterMetricInstrument(0, "client_latency", "Client latency", "s", + ImmutableList.of("required_label_key_1", "required_label_key_2"), + ImmutableList.of("optional_label_key_1", "optional_label_key_2"), false); + + // Create sink + sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, false, optionalLabels); + + // Invoke updateMeasures + sink.updateMeasures(Arrays.asList(longCounterInstrument)); + + sink.addLongCounter(longCounterInstrument, 123L, + ImmutableList.of("required_label_value_1", "required_label_value_2"), + ImmutableList.of("optional_label_value_1", "optional_label_value_2")); + + io.opentelemetry.api.common.Attributes expectedAtrributes + = io.opentelemetry.api.common.Attributes.of( + AttributeKey.stringKey("required_label_key_1"), "required_label_value_1", + AttributeKey.stringKey("required_label_key_2"), "required_label_value_2", + AttributeKey.stringKey("optional_label_key_2"), "optional_label_value_2"); + + assertThat(openTelemetryTesting.getMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName("client_latency") + .hasDescription("Client latency") + .hasUnit("s") + .hasLongSumSatisfying( + longSum -> + longSum + .hasPointsSatisfying( + point -> + point + .hasAttributes(expectedAtrributes) + .hasValue(123L)))); + } +} diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java index 5217d66db2..12c8e72f86 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; +import com.google.common.collect.ImmutableMap; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.Channel; @@ -50,6 +51,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule; import java.io.InputStream; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; @@ -141,6 +143,9 @@ public class OpenTelemetryMetricsModuleTest { .setSampledToLocalTracing(true) .build(); private Meter testMeter; + private final Map enabledMetricsMap = ImmutableMap.of(); + + private final boolean disableDefaultMetrics = false; @Before public void setUp() throws Exception { @@ -150,7 +155,8 @@ public class OpenTelemetryMetricsModuleTest { @Test public void testClientInterceptors() { - OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter); + OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule(fakeClock.getStopwatchSupplier(), resource); grpcServerRule.getServiceRegistry().addService( @@ -205,7 +211,8 @@ public class OpenTelemetryMetricsModuleTest { @Test public void clientBasicMetrics() { - OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter);; + OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule(fakeClock.getStopwatchSupplier(), resource); OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory = @@ -339,7 +346,8 @@ public class OpenTelemetryMetricsModuleTest { // This test is only unit-testing the metrics recording logic. The retry behavior is faked. @Test public void recordAttemptMetrics() { - OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter); + OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule(fakeClock.getStopwatchSupplier(), resource); OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory = @@ -759,7 +767,8 @@ public class OpenTelemetryMetricsModuleTest { @Test public void clientStreamNeverCreatedStillRecordMetrics() { - OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter); + OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule(fakeClock.getStopwatchSupplier(), resource); OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory = @@ -860,7 +869,8 @@ public class OpenTelemetryMetricsModuleTest { @Test public void serverBasicMetrics() { - OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter); + OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule( fakeClock.getStopwatchSupplier(), resource); ServerStreamTracer.Factory tracerFactory = module.getServerTracerFactory(); diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryModuleTest.java index 28d3026dd2..edcc0b5282 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryModuleTest.java @@ -18,11 +18,14 @@ package io.grpc.opentelemetry; import static com.google.common.truth.Truth.assertThat; +import com.google.common.collect.ImmutableList; +import io.grpc.MetricSink; import io.grpc.internal.GrpcUtil; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -38,8 +41,10 @@ public class OpenTelemetryModuleTest { public void build() { OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build(); + OpenTelemetryModule openTelemetryModule = OpenTelemetryModule.newBuilder() .sdk(sdk) + .addOptionalLabel("version") .build(); assertThat(openTelemetryModule.getOpenTelemetryInstance()).isSameInstanceAs(sdk); @@ -48,6 +53,7 @@ public class OpenTelemetryModuleTest { meterProvider.meterBuilder("grpc-java") .setInstrumentationVersion(GrpcUtil.IMPLEMENTATION_VERSION) .build()); + assertThat(openTelemetryModule.getOptionalLabels()).isEqualTo(ImmutableList.of("version")); } @Test @@ -64,5 +70,37 @@ public class OpenTelemetryModuleTest { .meterBuilder("grpc-java") .setInstrumentationVersion(GrpcUtil.IMPLEMENTATION_VERSION) .build()); + assertThat(module.getEnableMetrics()).isEmpty(); + assertThat(module.getOptionalLabels()).isEmpty(); + assertThat(module.getSink()).isInstanceOf(MetricSink.class); } + + @Test + public void enableDisableMetrics() { + OpenTelemetryModule.Builder builder = OpenTelemetryModule.newBuilder(); + builder.enableMetrics(Arrays.asList("metric1", "metric4")); + builder.disableMetrics(Arrays.asList("metric2", "metric3")); + + OpenTelemetryModule module = builder.build(); + + assertThat(module.getEnableMetrics().get("metric1")).isTrue(); + assertThat(module.getEnableMetrics().get("metric4")).isTrue(); + assertThat(module.getEnableMetrics().get("metric2")).isFalse(); + assertThat(module.getEnableMetrics().get("metric3")).isFalse(); + } + + @Test + public void disableAllMetrics() { + OpenTelemetryModule.Builder builder = OpenTelemetryModule.newBuilder(); + builder.enableMetrics(Arrays.asList("metric1", "metric4")); + builder.disableMetrics(Arrays.asList("metric2", "metric3")); + builder.disableAllMetrics(); + + OpenTelemetryModule module = builder.build(); + + assertThat(module.getEnableMetrics()).isEmpty(); + } + + // TODO(dnvindhya): Add tests for configurator + }