• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #19975

08 Sep 2025 09:55PM UTC coverage: 88.547% (+0.01%) from 88.535%
#19975

push

github

web-flow
otel: subchannel metrics A94 (#12202)

Implements [A94](https://github.com/grpc/proposal/pull/485/files) except for the exact reason for disconnect_error

34806 of 39308 relevant lines covered (88.55%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.83
/../opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java
1
/*
2
 * Copyright 2024 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.opentelemetry;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20

21
import com.google.common.annotations.VisibleForTesting;
22
import com.google.common.collect.ImmutableMap;
23
import com.google.common.collect.ImmutableSet;
24
import io.grpc.CallbackMetricInstrument;
25
import io.grpc.DoubleCounterMetricInstrument;
26
import io.grpc.DoubleHistogramMetricInstrument;
27
import io.grpc.LongCounterMetricInstrument;
28
import io.grpc.LongGaugeMetricInstrument;
29
import io.grpc.LongHistogramMetricInstrument;
30
import io.grpc.LongUpDownCounterMetricInstrument;
31
import io.grpc.MetricInstrument;
32
import io.grpc.MetricSink;
33
import io.opentelemetry.api.common.Attributes;
34
import io.opentelemetry.api.common.AttributesBuilder;
35
import io.opentelemetry.api.metrics.BatchCallback;
36
import io.opentelemetry.api.metrics.DoubleCounter;
37
import io.opentelemetry.api.metrics.DoubleHistogram;
38
import io.opentelemetry.api.metrics.LongCounter;
39
import io.opentelemetry.api.metrics.LongHistogram;
40
import io.opentelemetry.api.metrics.LongUpDownCounter;
41
import io.opentelemetry.api.metrics.Meter;
42
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
43
import io.opentelemetry.api.metrics.ObservableMeasurement;
44
import java.util.ArrayList;
45
import java.util.BitSet;
46
import java.util.Collections;
47
import java.util.List;
48
import java.util.Map;
49
import java.util.Set;
50
import java.util.logging.Level;
51
import java.util.logging.Logger;
52

53
final class OpenTelemetryMetricSink implements MetricSink {
54
  private static final Logger logger = Logger.getLogger(OpenTelemetryMetricSink.class.getName());
1✔
55
  private final Object lock = new Object();
1✔
56
  private final Meter openTelemetryMeter;
57
  private final Map<String, Boolean> enableMetrics;
58
  private final boolean disableDefaultMetrics;
59
  private final Set<String> optionalLabels;
60
  private volatile List<MeasuresData> measures = new ArrayList<>();
1✔
61

62
  OpenTelemetryMetricSink(Meter meter, Map<String, Boolean> enableMetrics,
63
      boolean disableDefaultMetrics, List<String> optionalLabels) {
1✔
64
    this.openTelemetryMeter = checkNotNull(meter, "meter");
1✔
65
    this.enableMetrics = ImmutableMap.copyOf(enableMetrics);
1✔
66
    this.disableDefaultMetrics = disableDefaultMetrics;
1✔
67
    this.optionalLabels = ImmutableSet.copyOf(optionalLabels);
1✔
68
  }
1✔
69

70
  @Override
71
  public Map<String, Boolean> getEnabledMetrics() {
72
    return enableMetrics;
×
73
  }
74

75
  @Override
76
  public Set<String> getOptionalLabels() {
77
    return optionalLabels;
×
78
  }
79

80
  @Override
81
  public int getMeasuresSize() {
82
    return measures.size();
1✔
83
  }
84

85
  @VisibleForTesting
86
  List<MeasuresData> getMeasures() {
87
    synchronized (lock) {
1✔
88
      return Collections.unmodifiableList(measures);
1✔
89
    }
90
  }
91

92
  @Override
93
  public void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, double value,
94
      List<String> requiredLabelValues, List<String> optionalLabelValues) {
95
    MeasuresData instrumentData = measures.get(metricInstrument.getIndex());
1✔
96
    if (instrumentData == null) {
1✔
97
      // Disabled metric
98
      return;
×
99
    }
100
    Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(),
1✔
101
        metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues,
1✔
102
        instrumentData.getOptionalLabelsBitSet());
1✔
103
    DoubleCounter counter = (DoubleCounter) instrumentData.getMeasure();
1✔
104
    counter.add(value, attributes);
1✔
105
  }
1✔
106

107
  @Override
108
  public void addLongCounter(LongCounterMetricInstrument metricInstrument, long value,
109
      List<String> requiredLabelValues, List<String> optionalLabelValues) {
110
    MeasuresData instrumentData = measures.get(metricInstrument.getIndex());
1✔
111
    if (instrumentData == null) {
1✔
112
      // Disabled metric
113
      return;
1✔
114
    }
115
    Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(),
1✔
116
        metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues,
1✔
117
        instrumentData.getOptionalLabelsBitSet());
1✔
118
    LongCounter counter = (LongCounter) instrumentData.getMeasure();
1✔
119
    counter.add(value, attributes);
1✔
120
  }
1✔
121

122
  @Override
123
  public void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value,
124
                                   List<String> requiredLabelValues,
125
                                   List<String> optionalLabelValues) {
126
    MeasuresData instrumentData = measures.get(metricInstrument.getIndex());
1✔
127
    if (instrumentData == null) {
1✔
128
      // Disabled metric
129
      return;
1✔
130
    }
131
    Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(),
1✔
132
        metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues,
1✔
133
        instrumentData.getOptionalLabelsBitSet());
1✔
134
    LongUpDownCounter counter = (LongUpDownCounter) instrumentData.getMeasure();
1✔
135
    counter.add(value, attributes);
1✔
136
  }
1✔
137

138
  @Override
139
  public void recordDoubleHistogram(DoubleHistogramMetricInstrument metricInstrument, double value,
140
      List<String> requiredLabelValues, List<String> optionalLabelValues) {
141
    MeasuresData instrumentData = measures.get(metricInstrument.getIndex());
1✔
142
    if (instrumentData == null) {
1✔
143
      // Disabled metric
144
      return;
1✔
145
    }
146
    Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(),
1✔
147
        metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues,
1✔
148
        instrumentData.getOptionalLabelsBitSet());
1✔
149
    DoubleHistogram histogram = (DoubleHistogram) instrumentData.getMeasure();
1✔
150
    histogram.record(value, attributes);
1✔
151
  }
1✔
152

153
  @Override
154
  public void recordLongHistogram(LongHistogramMetricInstrument metricInstrument, long value,
155
      List<String> requiredLabelValues, List<String> optionalLabelValues) {
156
    MeasuresData instrumentData = measures.get(metricInstrument.getIndex());
1✔
157
    if (instrumentData == null) {
1✔
158
      // Disabled metric
159
      return;
1✔
160
    }
161
    Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(),
1✔
162
        metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues,
1✔
163
        instrumentData.getOptionalLabelsBitSet());
1✔
164
    LongHistogram histogram = (LongHistogram) instrumentData.getMeasure();
1✔
165
    histogram.record(value, attributes);
1✔
166
  }
1✔
167

168
  @Override
169
  public void recordLongGauge(LongGaugeMetricInstrument metricInstrument, long value,
170
      List<String> requiredLabelValues, List<String> optionalLabelValues) {
171
    MeasuresData instrumentData = measures.get(metricInstrument.getIndex());
1✔
172
    if (instrumentData == null) {
1✔
173
      // Disabled metric
174
      return;
1✔
175
    }
176
    Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(),
1✔
177
        metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues,
1✔
178
        instrumentData.getOptionalLabelsBitSet());
1✔
179
    ObservableLongMeasurement gauge = (ObservableLongMeasurement) instrumentData.getMeasure();
1✔
180
    gauge.record(value, attributes);
1✔
181
  }
1✔
182

183
  @Override
184
  public Registration registerBatchCallback(Runnable callback,
185
      CallbackMetricInstrument... metricInstruments) {
186
    List<ObservableMeasurement> measurements = new ArrayList<>(metricInstruments.length);
1✔
187
    for (CallbackMetricInstrument metricInstrument: metricInstruments) {
1✔
188
      MeasuresData instrumentData = measures.get(metricInstrument.getIndex());
1✔
189
      if (instrumentData == null) {
1✔
190
        // Disabled metric
191
        continue;
1✔
192
      }
193
      if (!(instrumentData.getMeasure() instanceof ObservableMeasurement)) {
1✔
194
        logger.log(Level.FINE, "Unsupported metric instrument type : {0} {1}",
×
195
            new Object[] {metricInstrument, instrumentData.getMeasure().getClass()});
×
196
        continue;
×
197
      }
198
      measurements.add((ObservableMeasurement) instrumentData.getMeasure());
1✔
199
    }
200
    if (measurements.isEmpty()) {
1✔
201
      return () -> { };
1✔
202
    }
203
    ObservableMeasurement first = measurements.get(0);
1✔
204
    measurements.remove(0);
1✔
205
    BatchCallback closeable = openTelemetryMeter.batchCallback(
1✔
206
        callback, first, measurements.toArray(new ObservableMeasurement[0]));
1✔
207
    return closeable::close;
1✔
208
  }
209

210
  @Override
211
  public void updateMeasures(List<MetricInstrument> instruments) {
212
    synchronized (lock) {
1✔
213
      if (measures.size() >= instruments.size()) {
1✔
214
        // Already up-to-date
215
        return;
×
216
      }
217

218
      List<MeasuresData> newMeasures = new ArrayList<>(instruments.size());
1✔
219
      // Reuse existing measures
220
      newMeasures.addAll(measures);
1✔
221

222
      for (int i = measures.size(); i < instruments.size(); i++) {
1✔
223
        MetricInstrument instrument = instruments.get(i);
1✔
224
        // Check if the metric is disabled
225
        if (!shouldEnableMetric(instrument)) {
1✔
226
          // Adding null measure for disabled Metric
227
          newMeasures.add(null);
1✔
228
          continue;
1✔
229
        }
230

231
        BitSet bitSet = new BitSet(instrument.getOptionalLabelKeys().size());
1✔
232
        if (optionalLabels.isEmpty()) {
1✔
233
          // initialize an empty list
234
        } else {
235
          List<String> labels = instrument.getOptionalLabelKeys();
1✔
236
          for (int j = 0; j < labels.size(); j++) {
1✔
237
            if (optionalLabels.contains(labels.get(j))) {
1✔
238
              bitSet.set(j);
1✔
239
            }
240
          }
241
        }
242

243
        int index = instrument.getIndex();
1✔
244
        String name = instrument.getName();
1✔
245
        String unit = instrument.getUnit();
1✔
246
        String description = instrument.getDescription();
1✔
247

248
        Object openTelemetryMeasure;
249
        if (instrument instanceof DoubleCounterMetricInstrument) {
1✔
250
          openTelemetryMeasure = openTelemetryMeter.counterBuilder(name)
1✔
251
              .setUnit(unit)
1✔
252
              .setDescription(description)
1✔
253
              .ofDoubles()
1✔
254
              .build();
1✔
255
        } else if (instrument instanceof LongCounterMetricInstrument) {
1✔
256
          openTelemetryMeasure = openTelemetryMeter.counterBuilder(name)
1✔
257
              .setUnit(unit)
1✔
258
              .setDescription(description)
1✔
259
              .build();
1✔
260
        } else if (instrument instanceof DoubleHistogramMetricInstrument) {
1✔
261
          openTelemetryMeasure = openTelemetryMeter.histogramBuilder(name)
1✔
262
              .setUnit(unit)
1✔
263
              .setDescription(description)
1✔
264
              .build();
1✔
265
        } else if (instrument instanceof LongHistogramMetricInstrument) {
1✔
266
          openTelemetryMeasure = openTelemetryMeter.histogramBuilder(name)
1✔
267
              .setUnit(unit)
1✔
268
              .setDescription(description)
1✔
269
              .ofLongs()
1✔
270
              .build();
1✔
271
        } else if (instrument instanceof LongGaugeMetricInstrument) {
1✔
272
          openTelemetryMeasure = openTelemetryMeter.gaugeBuilder(name)
1✔
273
              .setUnit(unit)
1✔
274
              .setDescription(description)
1✔
275
              .ofLongs()
1✔
276
              .buildObserver();
1✔
277
        } else if (instrument instanceof LongUpDownCounterMetricInstrument) {
1✔
278
          openTelemetryMeasure = openTelemetryMeter.upDownCounterBuilder(name)
1✔
279
              .setUnit(unit)
1✔
280
              .setDescription(description)
1✔
281
              .build();
1✔
282
        } else {
283
          logger.log(Level.FINE, "Unsupported metric instrument type : {0}", instrument);
×
284
          openTelemetryMeasure = null;
×
285
        }
286
        newMeasures.add(index, new MeasuresData(bitSet, openTelemetryMeasure));
1✔
287
      }
288

289
      measures = newMeasures;
1✔
290
    }
1✔
291
  }
1✔
292

293
  private boolean shouldEnableMetric(MetricInstrument instrument) {
294
    Boolean explicitlyEnabled = enableMetrics.get(instrument.getName());
1✔
295
    if (explicitlyEnabled != null) {
1✔
296
      return explicitlyEnabled;
1✔
297
    }
298
    return instrument.isEnableByDefault() && !disableDefaultMetrics;
1✔
299
  }
300

301

302
  private Attributes createAttributes(List<String> requiredLabelKeys,
303
      List<String> optionalLabelKeys,
304
      List<String> requiredLabelValues, List<String> optionalLabelValues, BitSet bitSet) {
305
    AttributesBuilder builder = Attributes.builder();
1✔
306
    // Required Labels
307
    for (int i = 0; i < requiredLabelKeys.size(); i++) {
1✔
308
      builder.put(requiredLabelKeys.get(i), requiredLabelValues.get(i));
1✔
309
    }
310
    // Optional labels
311
    for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
1✔
312
      if (i == Integer.MAX_VALUE) {
1✔
313
        break; // or (i+1) would overflow
×
314
      }
315
      builder.put(optionalLabelKeys.get(i), optionalLabelValues.get(i));
1✔
316
    }
317
    return builder.build();
1✔
318
  }
319

320
  static final class MeasuresData {
321
    final BitSet optionalLabelsIndices;
322
    final Object measure;
323

324
    MeasuresData(BitSet optionalLabelsIndices, Object measure) {
1✔
325
      this.optionalLabelsIndices = optionalLabelsIndices;
1✔
326
      this.measure = measure;
1✔
327
    }
1✔
328

329
    public BitSet getOptionalLabelsBitSet() {
330
      return optionalLabelsIndices;
1✔
331
    }
332

333
    public Object getMeasure() {
334
      return measure;
1✔
335
    }
336
  }
337

338
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc