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

grpc / grpc-java / #20143

09 Jan 2026 02:16PM UTC coverage: 88.687% (+0.03%) from 88.656%
#20143

push

github

web-flow
opentelemetry: Add target attribute filter for metrics (#12587)

Introduce an optional Predicate<String> targetAttributeFilter to control how grpc.target is recorded in OpenTelemetry client metrics.

When a filter is provided, targets rejected by the predicate are normalized to "other" to reduce grpc.target metric cardinality, while accepted targets are recorded as-is. If no filter is set, existing behavior is preserved.

This change adds a new Builder API on GrpcOpenTelemetry to allow applications to configure the filter. Tests verify both the Builder
wiring and the target normalization behavior.

This is an optional API; annotation (e.g., experimental) can be added
per maintainer guidance.

Refs #12322
Related: gRFC A109 – Target Attribute Filter for OpenTelemetry Metrics
https://github.com/grpc/proposal/pull/528

35356 of 39866 relevant lines covered (88.69%)

0.89 hits per line

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

95.0
/../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;
1✔
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
  }
1✔
200

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

381
    private Builder() {}
1✔
382

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

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

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

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

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

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

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

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

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