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

grpc / grpc-java / #20230

31 Mar 2026 09:55AM UTC coverage: 88.734% (+0.01%) from 88.72%
#20230

push

github

web-flow
openTelemetry: add tcp metrics (#12652)

Implements [A80](https://github.com/grpc/proposal/pull/519)

35697 of 40229 relevant lines covered (88.73%)

0.89 hits per line

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

94.48
/../opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java
1
/*
2
 * Copyright 2023 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
import static io.grpc.internal.GrpcUtil.IMPLEMENTATION_VERSION;
21
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.HEDGE_BUCKETS;
22
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.LATENCY_BUCKETS;
23
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.RETRY_BUCKETS;
24
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.SIZE_BUCKETS;
25
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.TRANSPARENT_RETRY_BUCKETS;
26

27
import com.google.common.annotations.VisibleForTesting;
28
import com.google.common.base.Stopwatch;
29
import com.google.common.base.Supplier;
30
import com.google.common.collect.ImmutableList;
31
import com.google.common.collect.ImmutableMap;
32
import io.grpc.ExperimentalApi;
33
import io.grpc.InternalConfigurator;
34
import io.grpc.InternalConfiguratorRegistry;
35
import io.grpc.InternalManagedChannelBuilder;
36
import io.grpc.ManagedChannelBuilder;
37
import io.grpc.MetricSink;
38
import io.grpc.ServerBuilder;
39
import io.grpc.internal.GrpcUtil;
40
import io.grpc.opentelemetry.internal.OpenTelemetryConstants;
41
import io.opentelemetry.api.OpenTelemetry;
42
import io.opentelemetry.api.metrics.Meter;
43
import io.opentelemetry.api.metrics.MeterProvider;
44
import io.opentelemetry.api.trace.Tracer;
45
import java.util.ArrayList;
46
import java.util.Collection;
47
import java.util.Collections;
48
import java.util.HashMap;
49
import java.util.List;
50
import java.util.Map;
51
import java.util.function.Predicate;
52
import javax.annotation.Nullable;
53
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
54

55
/**
56
 *  The entrypoint for OpenTelemetry metrics functionality in gRPC.
57
 *
58
 *  <p>GrpcOpenTelemetry uses {@link io.opentelemetry.api.OpenTelemetry} APIs for instrumentation.
59
 *  When no SDK is explicitly added no telemetry data will be collected. See
60
 *  {@code io.opentelemetry.sdk.OpenTelemetrySdk} for information on how to construct the SDK.
61
 *
62
 */
63
public final class GrpcOpenTelemetry {
64

65
  private static final Supplier<Stopwatch> STOPWATCH_SUPPLIER = new Supplier<Stopwatch>() {
1✔
66
    @Override
67
    public Stopwatch get() {
68
      return Stopwatch.createUnstarted();
1✔
69
    }
70
  };
71

72
  @VisibleForTesting
73
  static boolean ENABLE_OTEL_TRACING =
1✔
74
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ENABLE_OTEL_TRACING", false);
1✔
75

76
  private final OpenTelemetry openTelemetrySdk;
77
  private final MeterProvider meterProvider;
78
  private final Meter meter;
79
  private final Map<String, Boolean> enableMetrics;
80
  private final boolean disableDefault;
81
  private final OpenTelemetryMetricsResource resource;
82
  private final OpenTelemetryMetricsModule openTelemetryMetricsModule;
83
  private final OpenTelemetryTracingModule openTelemetryTracingModule;
84
  private final List<String> optionalLabels;
85
  private final MetricSink sink;
86

87
  public static Builder newBuilder() {
88
    return new Builder();
1✔
89
  }
90

91
  private GrpcOpenTelemetry(Builder builder) {
1✔
92
    this.openTelemetrySdk = checkNotNull(builder.openTelemetrySdk, "openTelemetrySdk");
1✔
93
    this.meterProvider = checkNotNull(openTelemetrySdk.getMeterProvider(), "meterProvider");
1✔
94
    this.meter = this.meterProvider
1✔
95
        .meterBuilder(OpenTelemetryConstants.INSTRUMENTATION_SCOPE)
1✔
96
        .setInstrumentationVersion(IMPLEMENTATION_VERSION)
1✔
97
        .build();
1✔
98
    this.enableMetrics = ImmutableMap.copyOf(builder.enableMetrics);
1✔
99
    this.disableDefault = builder.disableAll;
1✔
100
    this.resource = createMetricInstruments(meter, enableMetrics, disableDefault);
1✔
101
    this.optionalLabels = ImmutableList.copyOf(builder.optionalLabels);
1✔
102
    this.openTelemetryMetricsModule = new OpenTelemetryMetricsModule(
1✔
103
        STOPWATCH_SUPPLIER, resource, optionalLabels, builder.plugins,
1✔
104
        builder.targetFilter);
1✔
105
    this.openTelemetryTracingModule = new OpenTelemetryTracingModule(openTelemetrySdk);
1✔
106
    this.sink = new OpenTelemetryMetricSink(meter, enableMetrics, disableDefault, optionalLabels);
1✔
107
  }
1✔
108

109
  @VisibleForTesting
110
  OpenTelemetry getOpenTelemetryInstance() {
111
    return this.openTelemetrySdk;
1✔
112
  }
113

114
  @VisibleForTesting
115
  MeterProvider getMeterProvider() {
116
    return this.meterProvider;
1✔
117
  }
118

119
  @VisibleForTesting
120
  Meter getMeter() {
121
    return this.meter;
1✔
122
  }
123

124
  @VisibleForTesting
125
  OpenTelemetryMetricsResource getResource() {
126
    return this.resource;
×
127
  }
128

129
  @VisibleForTesting
130
  Map<String, Boolean> getEnableMetrics() {
131
    return this.enableMetrics;
1✔
132
  }
133

134
  @VisibleForTesting
135
  List<String> getOptionalLabels() {
136
    return optionalLabels;
1✔
137
  }
138

139
  MetricSink getSink() {
140
    return sink;
×
141
  }
142

143
  @VisibleForTesting
144
  Tracer getTracer() {
145
    return this.openTelemetryTracingModule.getTracer();
1✔
146
  }
147

148
  @VisibleForTesting
149
  TargetFilter getTargetAttributeFilter() {
150
    return this.openTelemetryMetricsModule.getTargetAttributeFilter();
1✔
151
  }
152

153
  /**
154
   * Registers GrpcOpenTelemetry globally, applying its configuration to all subsequently created
155
   * gRPC channels and servers.
156
   */
157
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10591")
158
  public void registerGlobal() {
159
    InternalConfiguratorRegistry.setConfigurators(Collections.singletonList(
×
160
        new InternalConfigurator() {
×
161
          @Override
162
          public void configureChannelBuilder(ManagedChannelBuilder<?> channelBuilder) {
163
            GrpcOpenTelemetry.this.configureChannelBuilder(channelBuilder);
×
164
          }
×
165

166
          @Override
167
          public void configureServerBuilder(ServerBuilder<?> serverBuilder) {
168
            GrpcOpenTelemetry.this.configureServerBuilder(serverBuilder);
×
169
          }
×
170
        }));
171
  }
×
172

173
  /**
174
   * Configures the given {@link ManagedChannelBuilder} with OpenTelemetry metrics instrumentation.
175
   */
176
  public void configureChannelBuilder(ManagedChannelBuilder<?> builder) {
177
    InternalManagedChannelBuilder.addMetricSink(builder, sink);
1✔
178
    InternalManagedChannelBuilder.interceptWithTarget(
1✔
179
        builder, openTelemetryMetricsModule::getClientInterceptor);
180
    if (ENABLE_OTEL_TRACING) {
1✔
181
      builder.intercept(openTelemetryTracingModule.getClientInterceptor());
1✔
182
    }
183
  }
1✔
184

185
  /**
186
   * Configures the given {@link ServerBuilder} with OpenTelemetry metrics instrumentation.
187
   *
188
   * @param serverBuilder the server builder to configure
189
   */
190
  public void configureServerBuilder(ServerBuilder<?> serverBuilder) {
191
    /* To ensure baggage propagation to metrics, we need the tracing
192
    tracers to be initialised before metrics */
193
    if (ENABLE_OTEL_TRACING) {
1✔
194
      serverBuilder.addStreamTracerFactory(
1✔
195
          openTelemetryTracingModule.getServerTracerFactory());
1✔
196
      serverBuilder.intercept(openTelemetryTracingModule.getServerSpanPropagationInterceptor());
1✔
197
    }
198
    serverBuilder.addStreamTracerFactory(openTelemetryMetricsModule.getServerTracerFactory());
1✔
199
    serverBuilder.addMetricSink(sink);
1✔
200
  }
1✔
201

202
  @VisibleForTesting
203
  static OpenTelemetryMetricsResource createMetricInstruments(Meter meter,
204
      Map<String, Boolean> enableMetrics, boolean disableDefault) {
205
    OpenTelemetryMetricsResource.Builder builder = OpenTelemetryMetricsResource.builder();
1✔
206

207
    if (isMetricEnabled("grpc.client.call.duration", enableMetrics, disableDefault)) {
1✔
208
      builder.clientCallDurationCounter(
1✔
209
          meter.histogramBuilder("grpc.client.call.duration")
1✔
210
              .setUnit("s")
1✔
211
              .setDescription(
1✔
212
                  "Time taken by gRPC to complete an RPC from application's perspective")
213
              .setExplicitBucketBoundariesAdvice(LATENCY_BUCKETS)
1✔
214
              .build());
1✔
215
    }
216

217
    if (isMetricEnabled("grpc.client.attempt.started", enableMetrics, disableDefault)) {
1✔
218
      builder.clientAttemptCountCounter(
1✔
219
          meter.counterBuilder("grpc.client.attempt.started")
1✔
220
              .setUnit("{attempt}")
1✔
221
              .setDescription("Number of client call attempts started")
1✔
222
              .build());
1✔
223
    }
224

225
    if (isMetricEnabled("grpc.client.attempt.duration", enableMetrics, disableDefault)) {
1✔
226
      builder.clientAttemptDurationCounter(
1✔
227
          meter.histogramBuilder(
1✔
228
                  "grpc.client.attempt.duration")
229
              .setUnit("s")
1✔
230
              .setDescription("Time taken to complete a client call attempt")
1✔
231
              .setExplicitBucketBoundariesAdvice(LATENCY_BUCKETS)
1✔
232
              .build());
1✔
233
    }
234

235
    if (isMetricEnabled("grpc.client.attempt.sent_total_compressed_message_size", enableMetrics,
1✔
236
        disableDefault)) {
237
      builder.clientTotalSentCompressedMessageSizeCounter(
1✔
238
          meter.histogramBuilder(
1✔
239
                  "grpc.client.attempt.sent_total_compressed_message_size")
240
              .setUnit("By")
1✔
241
              .setDescription("Compressed message bytes sent per client call attempt")
1✔
242
              .ofLongs()
1✔
243
              .setExplicitBucketBoundariesAdvice(SIZE_BUCKETS)
1✔
244
              .build());
1✔
245
    }
246

247
    if (isMetricEnabled("grpc.client.attempt.rcvd_total_compressed_message_size", enableMetrics,
1✔
248
        disableDefault)) {
249
      builder.clientTotalReceivedCompressedMessageSizeCounter(
1✔
250
          meter.histogramBuilder(
1✔
251
                  "grpc.client.attempt.rcvd_total_compressed_message_size")
252
              .setUnit("By")
1✔
253
              .setDescription("Compressed message bytes received per call attempt")
1✔
254
              .ofLongs()
1✔
255
              .setExplicitBucketBoundariesAdvice(SIZE_BUCKETS)
1✔
256
              .build());
1✔
257
    }
258

259
    if (isMetricEnabled("grpc.client.call.retries", enableMetrics, disableDefault)) {
1✔
260
      builder.clientCallRetriesCounter(
1✔
261
          meter.histogramBuilder(
1✔
262
                  "grpc.client.call.retries")
263
              .setUnit("{retry}")
1✔
264
              .setDescription("Number of retries during the client call. "
1✔
265
                  + "If there were no retries, 0 is not reported.")
266
              .ofLongs()
1✔
267
              .setExplicitBucketBoundariesAdvice(RETRY_BUCKETS)
1✔
268
              .build());
1✔
269
    }
270

271
    if (isMetricEnabled("grpc.client.call.transparent_retries", enableMetrics,
1✔
272
        disableDefault)) {
273
      builder.clientCallTransparentRetriesCounter(
1✔
274
          meter.histogramBuilder(
1✔
275
                  "grpc.client.call.transparent_retries")
276
              .setUnit("{transparent_retry}")
1✔
277
              .setDescription("Number of transparent retries during the client call. "
1✔
278
                  + "If there were no transparent retries, 0 is not reported.")
279
              .ofLongs()
1✔
280
              .setExplicitBucketBoundariesAdvice(TRANSPARENT_RETRY_BUCKETS)
1✔
281
              .build());
1✔
282
    }
283

284
    if (isMetricEnabled("grpc.client.call.hedges", enableMetrics, disableDefault)) {
1✔
285
      builder.clientCallHedgesCounter(
1✔
286
          meter.histogramBuilder(
1✔
287
                  "grpc.client.call.hedges")
288
              .setUnit("{hedge}")
1✔
289
              .setDescription("Number of hedges during the client call. "
1✔
290
                  + "If there were no hedges, 0 is not reported.")
291
              .ofLongs()
1✔
292
              .setExplicitBucketBoundariesAdvice(HEDGE_BUCKETS)
1✔
293
              .build());
1✔
294
    }
295

296
    if (isMetricEnabled("grpc.client.call.retry_delay", enableMetrics, disableDefault)) {
1✔
297
      builder.clientCallRetryDelayCounter(
1✔
298
          meter.histogramBuilder(
1✔
299
                  "grpc.client.call.retry_delay")
300
              .setUnit("s")
1✔
301
              .setDescription("Total time of delay while there is no active attempt during the "
1✔
302
                  + "client call")
303
              .setExplicitBucketBoundariesAdvice(LATENCY_BUCKETS)
1✔
304
              .build());
1✔
305
    }
306

307
    if (isMetricEnabled("grpc.server.call.started", enableMetrics, disableDefault)) {
1✔
308
      builder.serverCallCountCounter(
1✔
309
          meter.counterBuilder("grpc.server.call.started")
1✔
310
              .setUnit("{call}")
1✔
311
              .setDescription("Number of server calls started")
1✔
312
              .build());
1✔
313
    }
314

315
    if (isMetricEnabled("grpc.server.call.duration", enableMetrics, disableDefault)) {
1✔
316
      builder.serverCallDurationCounter(
1✔
317
          meter.histogramBuilder("grpc.server.call.duration")
1✔
318
              .setUnit("s")
1✔
319
              .setDescription(
1✔
320
                  "Time taken to complete a call from server transport's perspective")
321
              .setExplicitBucketBoundariesAdvice(LATENCY_BUCKETS)
1✔
322
              .build());
1✔
323
    }
324

325
    if (isMetricEnabled("grpc.server.call.sent_total_compressed_message_size",
1✔
326
        enableMetrics, disableDefault)) {
327
      builder.serverTotalSentCompressedMessageSizeCounter(
1✔
328
          meter.histogramBuilder(
1✔
329
                  "grpc.server.call.sent_total_compressed_message_size")
330
              .setUnit("By")
1✔
331
              .setDescription("Compressed message bytes sent per server call")
1✔
332
              .ofLongs()
1✔
333
              .setExplicitBucketBoundariesAdvice(SIZE_BUCKETS)
1✔
334
              .build());
1✔
335
    }
336

337
    if (isMetricEnabled("grpc.server.call.rcvd_total_compressed_message_size",
1✔
338
        enableMetrics, disableDefault)) {
339
      builder.serverTotalReceivedCompressedMessageSizeCounter(
1✔
340
          meter.histogramBuilder(
1✔
341
                  "grpc.server.call.rcvd_total_compressed_message_size")
342
              .setUnit("By")
1✔
343
              .setDescription("Compressed message bytes received per server call")
1✔
344
              .ofLongs()
1✔
345
              .setExplicitBucketBoundariesAdvice(SIZE_BUCKETS)
1✔
346
              .build());
1✔
347
    }
348

349
    return builder.build();
1✔
350
  }
351

352
  static boolean isMetricEnabled(String metricName, Map<String, Boolean> enableMetrics,
353
      boolean disableDefault) {
354
    Boolean explicitlyEnabled = enableMetrics.get(metricName);
1✔
355
    if (explicitlyEnabled != null) {
1✔
356
      return explicitlyEnabled;
1✔
357
    }
358
    return OpenTelemetryMetricsModule.DEFAULT_PER_CALL_METRICS_SET.contains(metricName)
1✔
359
        && !disableDefault;
360
  }
361

362
  /**
363
   * Internal interface to avoid storing a {@link java.util.function.Predicate} directly, ensuring
364
   * compatibility with Android devices (API level < 24) that do not use library desugaring.
365
   */
366
  interface TargetFilter {
367
    boolean test(String target);
368
  }
369

370
  /**
371
   * Builder for configuring {@link GrpcOpenTelemetry}.
372
   */
373
  public static class Builder {
374
    private OpenTelemetry openTelemetrySdk = OpenTelemetry.noop();
1✔
375
    private final List<OpenTelemetryPlugin> plugins = new ArrayList<>();
1✔
376
    private final Collection<String> optionalLabels = new ArrayList<>();
1✔
377
    private final Map<String, Boolean> enableMetrics = new HashMap<>();
1✔
378
    private boolean disableAll;
379
    @Nullable
380
    private TargetFilter targetFilter;
381

382
    private Builder() {}
1✔
383

384
    /**
385
     * Sets the {@link io.opentelemetry.api.OpenTelemetry} entrypoint to use. This can be used to
386
     * configure OpenTelemetry by returning the instance created by a
387
     * {@code io.opentelemetry.sdk.OpenTelemetrySdkBuilder}.
388
     */
389
    public Builder sdk(OpenTelemetry sdk) {
390
      this.openTelemetrySdk = sdk;
1✔
391
      return this;
1✔
392
    }
393

394
    Builder plugin(OpenTelemetryPlugin plugin) {
395
      plugins.add(checkNotNull(plugin, "plugin"));
1✔
396
      return this;
1✔
397
    }
398

399
    /**
400
     * Adds optionalLabelKey to all the metrics that can provide value for the
401
     * optionalLabelKey.
402
     */
403
    public Builder addOptionalLabel(String optionalLabelKey) {
404
      this.optionalLabels.add(optionalLabelKey);
1✔
405
      return this;
1✔
406
    }
407

408
    /**
409
     * Enables the specified metrics for collection and export. By default, only a subset of
410
     * metrics are enabled.
411
     */
412
    public Builder enableMetrics(Collection<String> enableMetrics) {
413
      for (String metric : enableMetrics) {
1✔
414
        this.enableMetrics.put(metric, true);
1✔
415
      }
1✔
416
      return this;
1✔
417
    }
418

419
    /**
420
     * Disables the specified metrics from being collected and exported.
421
     */
422
    public Builder disableMetrics(Collection<String> disableMetrics) {
423
      for (String metric : disableMetrics) {
1✔
424
        this.enableMetrics.put(metric, false);
1✔
425
      }
1✔
426
      return this;
1✔
427
    }
428

429
    /**
430
     * Disable all metrics. If set to true all metrics must be explicitly enabled.
431
     */
432
    public Builder disableAllMetrics() {
433
      this.enableMetrics.clear();
1✔
434
      this.disableAll = true;
1✔
435
      return this;
1✔
436
    }
437

438
    Builder enableTracing(boolean enable) {
439
      ENABLE_OTEL_TRACING = enable;
1✔
440
      return this;
1✔
441
    }
442

443
    /**
444
     * Sets an optional filter to control recording of the {@code grpc.target} metric
445
     * attribute.
446
     *
447
     * <p>If the predicate returns {@code true}, the original target is recorded. Otherwise,
448
     * the target is recorded as {@code "other"} to limit metric cardinality.
449
     *
450
     * <p>If unset, all targets are recorded as-is.
451
     */
452
    @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12595")
453
    @IgnoreJRERequirement
454
    public Builder targetAttributeFilter(@Nullable Predicate<String> filter) {
455
      if (filter == null) {
1✔
456
        this.targetFilter = null;
×
457
      } else {
458
        this.targetFilter = filter::test;
1✔
459
      }
460
      return this;
1✔
461
    }
462

463
    /**
464
     * Returns a new {@link GrpcOpenTelemetry} built with the configuration of this {@link
465
     * Builder}.
466
     */
467
    public GrpcOpenTelemetry build() {
468
      return new GrpcOpenTelemetry(this);
1✔
469
    }
470
  }
471
}
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

© 2026 Coveralls, Inc