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

grpc / grpc-java / #20390

03 Aug 2026 09:17PM UTC coverage: 89.149% (+0.01%) from 89.137%
#20390

push

github

jdcormie
binder: Let servers load their SecurityPolicy asynchronously

Android IPC servers can't defer "listening" while some slow or async
initialization process completes. Instead, Android *tells* a server to
initialize itself just-in-time for the first client transaction. This
instruction arrives as a callback to Service#onCreate() then
Service#onBind() on the app's main thread, where blocking to load a
security policy would risk an "Application Not Responding" (ANR) error.

Introduce AsyncSecurityPolicies#deferred which defers policy
creation until it's actually needed by a Channel or Server.

38309 of 42972 relevant lines covered (89.15%)

0.89 hits per line

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

92.43
/../opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.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.opentelemetry.internal.OpenTelemetryConstants.BACKEND_SERVICE_KEY;
21
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.BAGGAGE_KEY;
22
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.CUSTOM_LABEL_KEY;
23
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.LOCALITY_KEY;
24
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.METHOD_KEY;
25
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.STATUS_KEY;
26
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.TARGET_KEY;
27

28
import com.google.common.annotations.VisibleForTesting;
29
import com.google.common.base.Stopwatch;
30
import com.google.common.base.Supplier;
31
import com.google.common.collect.ImmutableList;
32
import com.google.common.collect.ImmutableSet;
33
import com.google.errorprone.annotations.concurrent.GuardedBy;
34
import io.grpc.CallOptions;
35
import io.grpc.Channel;
36
import io.grpc.ClientCall;
37
import io.grpc.ClientInterceptor;
38
import io.grpc.ClientStreamTracer;
39
import io.grpc.ClientStreamTracer.StreamInfo;
40
import io.grpc.Deadline;
41
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
42
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
43
import io.grpc.Grpc;
44
import io.grpc.Metadata;
45
import io.grpc.MethodDescriptor;
46
import io.grpc.ServerStreamTracer;
47
import io.grpc.Status;
48
import io.grpc.Status.Code;
49
import io.grpc.StreamTracer;
50
import io.grpc.internal.StatsTraceContext.ServerCallMethodListener;
51
import io.grpc.opentelemetry.GrpcOpenTelemetry.TargetFilter;
52
import io.opentelemetry.api.baggage.Baggage;
53
import io.opentelemetry.api.common.Attributes;
54
import io.opentelemetry.api.common.AttributesBuilder;
55
import io.opentelemetry.context.Context;
56
import java.util.ArrayList;
57
import java.util.Collection;
58
import java.util.Collections;
59
import java.util.List;
60
import java.util.Objects;
61
import java.util.concurrent.TimeUnit;
62
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
63
import java.util.concurrent.atomic.AtomicLong;
64
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
65
import java.util.logging.Level;
66
import java.util.logging.Logger;
67
import javax.annotation.Nullable;
68

69
/**
70
 * Provides factories for {@link StreamTracer} that records metrics to OpenTelemetry.
71
 *
72
 * <p>On the client-side, a factory is created for each call, and the factory creates a stream
73
 * tracer for each attempt. If there is no stream created when the call is ended, we still create a
74
 * tracer. It's the tracer that reports per-attempt stats, and the factory that reports the stats
75
 * of the overall RPC, such as RETRIES_PER_CALL, to OpenTelemetry.
76
 *
77
 * <p>This module optionally applies a target attribute filter to limit the cardinality of
78
 * the {@code grpc.target} attribute in client-side metrics by mapping disallowed targets
79
 * to a stable placeholder value.
80
 *
81
 * <p>On the server-side, there is only one ServerStream per each ServerCall, and ServerStream
82
 * starts earlier than the ServerCall. Therefore, only one tracer is created per stream/call, and
83
 * it's the tracer that reports the summary to OpenTelemetry.
84
 */
85
final class OpenTelemetryMetricsModule {
86
  private static final Logger logger = Logger.getLogger(OpenTelemetryMetricsModule.class.getName());
1✔
87
  public static final ImmutableSet<String> DEFAULT_PER_CALL_METRICS_SET =
1✔
88
      ImmutableSet.of(
1✔
89
          "grpc.client.attempt.started",
90
          "grpc.client.attempt.duration",
91
          "grpc.client.attempt.sent_total_compressed_message_size",
92
          "grpc.client.attempt.rcvd_total_compressed_message_size",
93
          "grpc.client.call.duration",
94
          "grpc.server.call.started",
95
          "grpc.server.call.duration",
96
          "grpc.server.call.sent_total_compressed_message_size",
97
          "grpc.server.call.rcvd_total_compressed_message_size");
98

99
  // Using floating point because TimeUnit.NANOSECONDS.toSeconds would discard
100
  // fractional seconds.
101
  private static final double SECONDS_PER_NANO = 1e-9;
102

103
  private final OpenTelemetryMetricsResource resource;
104
  private final Supplier<Stopwatch> stopwatchSupplier;
105
  private final boolean localityEnabled;
106
  private final boolean backendServiceEnabled;
107
  private final boolean customLabelEnabled;
108
  private final ImmutableList<OpenTelemetryPlugin> plugins;
109
  @Nullable
110
  private final TargetFilter targetAttributeFilter;
111

112
  OpenTelemetryMetricsModule(Supplier<Stopwatch> stopwatchSupplier,
113
                             OpenTelemetryMetricsResource resource,
114
                             Collection<String> optionalLabels, List<OpenTelemetryPlugin> plugins) {
115
    this(stopwatchSupplier, resource, optionalLabels, plugins, null);
1✔
116
  }
1✔
117

118
  OpenTelemetryMetricsModule(Supplier<Stopwatch> stopwatchSupplier,
119
      OpenTelemetryMetricsResource resource,
120
      Collection<String> optionalLabels, List<OpenTelemetryPlugin> plugins,
121
      @Nullable TargetFilter targetAttributeFilter) {
1✔
122
    this.resource = checkNotNull(resource, "resource");
1✔
123
    this.stopwatchSupplier = checkNotNull(stopwatchSupplier, "stopwatchSupplier");
1✔
124
    this.localityEnabled = optionalLabels.contains(LOCALITY_KEY.getKey());
1✔
125
    this.backendServiceEnabled = optionalLabels.contains(BACKEND_SERVICE_KEY.getKey());
1✔
126
    this.customLabelEnabled = optionalLabels.contains(CUSTOM_LABEL_KEY.getKey());
1✔
127
    this.plugins = ImmutableList.copyOf(plugins);
1✔
128
    this.targetAttributeFilter = targetAttributeFilter;
1✔
129
  }
1✔
130

131
  @VisibleForTesting
132
  TargetFilter getTargetAttributeFilter() {
133
    return targetAttributeFilter;
1✔
134
  }
135

136
  /**
137
   * Returns the server tracer factory.
138
   */
139
  ServerStreamTracer.Factory getServerTracerFactory() {
140
    return new ServerTracerFactory();
1✔
141
  }
142

143
  /**
144
   * Returns the client interceptor that facilitates OpenTelemetry metrics reporting.
145
   */
146
  ClientInterceptor getClientInterceptor(String target) {
147
    ImmutableList.Builder<OpenTelemetryPlugin> pluginBuilder =
1✔
148
        ImmutableList.builderWithExpectedSize(plugins.size());
1✔
149
    for (OpenTelemetryPlugin plugin : plugins) {
1✔
150
      if (plugin.enablePluginForChannel(target)) {
1✔
151
        pluginBuilder.add(plugin);
1✔
152
      }
153
    }
1✔
154
    String filteredTarget = recordTarget(target);
1✔
155
    return new MetricsClientInterceptor(filteredTarget, pluginBuilder.build());
1✔
156
  }
157

158
  String recordTarget(String target) {
159
    if (targetAttributeFilter == null || target == null) {
1✔
160
      return target;
1✔
161
    }
162
    return targetAttributeFilter.test(target) ? target : "other";
1✔
163
  }
164

165
  static String recordMethodName(String fullMethodName, boolean isGeneratedMethod) {
166
    return isGeneratedMethod ? fullMethodName : "other";
1✔
167
  }
168

169
  private static final class ClientTracer extends ClientStreamTracer {
170
    @Nullable private static final AtomicLongFieldUpdater<ClientTracer> outboundWireSizeUpdater;
171
    @Nullable private static final AtomicLongFieldUpdater<ClientTracer> inboundWireSizeUpdater;
172

173
    /*
174
     * When using Atomic*FieldUpdater, some Samsung Android 5.0.x devices encounter a bug in their
175
     * JDK reflection API that triggers a NoSuchFieldException. When this occurs, we fall back to
176
     * (potentially racy) direct updates of the volatile variables.
177
     */
178
    static {
179
      AtomicLongFieldUpdater<ClientTracer> tmpOutboundWireSizeUpdater;
180
      AtomicLongFieldUpdater<ClientTracer> tmpInboundWireSizeUpdater;
181
      try {
182
        tmpOutboundWireSizeUpdater =
1✔
183
            AtomicLongFieldUpdater.newUpdater(ClientTracer.class, "outboundWireSize");
1✔
184
        tmpInboundWireSizeUpdater =
1✔
185
            AtomicLongFieldUpdater.newUpdater(ClientTracer.class, "inboundWireSize");
1✔
186
      } catch (Throwable t) {
×
187
        logger.log(Level.SEVERE, "Creating atomic field updaters failed", t);
×
188
        tmpOutboundWireSizeUpdater = null;
×
189
        tmpInboundWireSizeUpdater = null;
×
190
      }
1✔
191
      outboundWireSizeUpdater = tmpOutboundWireSizeUpdater;
1✔
192
      inboundWireSizeUpdater = tmpInboundWireSizeUpdater;
1✔
193
    }
1✔
194

195
    final Stopwatch stopwatch;
196
    final CallAttemptsTracerFactory attemptsState;
197
    final OpenTelemetryMetricsModule module;
198
    final StreamInfo info;
199
    final String target;
200
    final String fullMethodName;
201
    final List<OpenTelemetryPlugin.ClientStreamPlugin> streamPlugins;
202
    volatile long outboundWireSize;
203
    volatile long inboundWireSize;
204
    volatile String locality;
205
    volatile String backendService;
206
    long attemptNanos;
207
    Code statusCode;
208
    @Nullable private volatile Stopwatch activeDelayStopwatch;
209
    @Nullable private volatile String activeDelayType;
210

211
    ClientTracer(CallAttemptsTracerFactory attemptsState, OpenTelemetryMetricsModule module,
212
        StreamInfo info, String target, String fullMethodName,
213
        List<OpenTelemetryPlugin.ClientStreamPlugin> streamPlugins) {
1✔
214
      this.attemptsState = attemptsState;
1✔
215
      this.module = module;
1✔
216
      this.info = info;
1✔
217
      this.target = target;
1✔
218
      this.fullMethodName = fullMethodName;
1✔
219
      this.streamPlugins = streamPlugins;
1✔
220
      this.stopwatch = module.stopwatchSupplier.get().start();
1✔
221
    }
1✔
222

223
    @Override
224
    public void streamCreated(io.grpc.Attributes transportAtts, Metadata headers) {
225
      recordAttemptDelayEnd();
1✔
226
    }
1✔
227

228
    @Override
229
    public void recordAttemptDelayStart(String delayType, String delayReason) {
230
      if (!GrpcOpenTelemetry.isDelayObservabilityEnabled()
1✔
231
          || (activeDelayStopwatch != null && Objects.equals(activeDelayType, delayType))) {
×
232
        // Do not reset the stopwatch if the delay type is unchanged.
233
        return;
1✔
234
      }
235
      recordAttemptDelayEnd();
1✔
236
      activeDelayType = delayType;
1✔
237
      activeDelayStopwatch = module.stopwatchSupplier.get().start();
1✔
238
    }
1✔
239

240
    @Override
241
    public void recordAttemptDelayReasonChanged(String delayReason) {
242
      // Reason strings are high-cardinality diagnostics intended for tracing spans.
243
    }
×
244

245
    @Override
246
    public void recordAttemptDelayEnd() {
247
      Stopwatch delayStopwatch = activeDelayStopwatch;
1✔
248
      String delayType = activeDelayType;
1✔
249
      if (delayStopwatch != null && delayType != null) {
1✔
250
        delayStopwatch.stop();
1✔
251
        long delayNanos = delayStopwatch.elapsed(TimeUnit.NANOSECONDS);
1✔
252
        activeDelayStopwatch = null;
1✔
253
        activeDelayType = null;
1✔
254
        if (module.resource.clientAttemptDelayCounter() != null) {
1✔
255
          AttributesBuilder builder = Attributes.builder()
1✔
256
              .put(METHOD_KEY, fullMethodName)
1✔
257
              .put(TARGET_KEY, target)
1✔
258
              .put("grpc.delay_type", delayType);
1✔
259
          if (module.customLabelEnabled) {
1✔
260
            builder.put(
×
261
                CUSTOM_LABEL_KEY, info.getCallOptions().getOption(Grpc.CALL_OPTION_CUSTOM_LABEL));
×
262
          }
263
          for (OpenTelemetryPlugin.ClientStreamPlugin plugin : streamPlugins) {
1✔
264
            plugin.addLabels(builder);
×
265
          }
×
266
          module.resource.clientAttemptDelayCounter()
1✔
267
              .record(delayNanos * SECONDS_PER_NANO, builder.build(), attemptsState.otelContext);
1✔
268
        }
269
      }
270
    }
1✔
271

272
    @Override
273
    public void inboundHeaders(Metadata headers) {
274
      for (OpenTelemetryPlugin.ClientStreamPlugin plugin : streamPlugins) {
1✔
275
        plugin.inboundHeaders(headers);
1✔
276
      }
1✔
277
    }
1✔
278

279
    @Override
280
    @SuppressWarnings("NonAtomicVolatileUpdate")
281
    public void outboundWireSize(long bytes) {
282
      if (outboundWireSizeUpdater != null) {
1✔
283
        outboundWireSizeUpdater.getAndAdd(this, bytes);
1✔
284
      } else {
285
        outboundWireSize += bytes;
×
286
      }
287
    }
1✔
288

289
    @Override
290
    @SuppressWarnings("NonAtomicVolatileUpdate")
291
    public void inboundWireSize(long bytes) {
292
      if (inboundWireSizeUpdater != null) {
1✔
293
        inboundWireSizeUpdater.getAndAdd(this, bytes);
1✔
294
      } else {
295
        inboundWireSize += bytes;
×
296
      }
297
    }
1✔
298

299
    @Override
300
    public void addOptionalLabel(String key, String value) {
301
      if ("grpc.lb.locality".equals(key)) {
1✔
302
        locality = value;
1✔
303
      }
304
      if ("grpc.lb.backend_service".equals(key)) {
1✔
305
        backendService = value;
1✔
306
      }
307
    }
1✔
308

309
    @Override
310
    public void inboundTrailers(Metadata trailers) {
311
      for (OpenTelemetryPlugin.ClientStreamPlugin plugin : streamPlugins) {
1✔
312
        plugin.inboundTrailers(trailers);
1✔
313
      }
1✔
314
    }
1✔
315

316
    @Override
317
    public void streamClosed(Status status) {
318
      recordAttemptDelayEnd();
1✔
319
      stopwatch.stop();
1✔
320
      attemptNanos = stopwatch.elapsed(TimeUnit.NANOSECONDS);
1✔
321
      Deadline deadline = info.getCallOptions().getDeadline();
1✔
322
      statusCode = status.getCode();
1✔
323
      if (statusCode == Code.CANCELLED && deadline != null) {
1✔
324
        // When the server's deadline expires, it can only reset the stream with CANCEL and no
325
        // description. Since our timer may be delayed in firing, we double-check the deadline and
326
        // turn the failure into the likely more helpful DEADLINE_EXCEEDED status.
327
        if (deadline.isExpired()) {
1✔
328
          statusCode = Code.DEADLINE_EXCEEDED;
1✔
329
        }
330
      }
331
      attemptsState.attemptEnded(info.getCallOptions());
1✔
332
      recordFinishedAttempt();
1✔
333
    }
1✔
334

335
    void recordFinishedAttempt() {
336
      AttributesBuilder builder = io.opentelemetry.api.common.Attributes.builder()
1✔
337
          .put(METHOD_KEY, fullMethodName)
1✔
338
          .put(TARGET_KEY, target)
1✔
339
          .put(STATUS_KEY, statusCode.toString());
1✔
340
      if (module.localityEnabled) {
1✔
341
        String savedLocality = locality;
1✔
342
        if (savedLocality == null) {
1✔
343
          savedLocality = "";
1✔
344
        }
345
        builder.put(LOCALITY_KEY, savedLocality);
1✔
346
      }
347
      if (module.backendServiceEnabled) {
1✔
348
        String savedBackendService = backendService;
1✔
349
        if (savedBackendService == null) {
1✔
350
          savedBackendService = "";
1✔
351
        }
352
        builder.put(BACKEND_SERVICE_KEY, savedBackendService);
1✔
353
      }
354
      if (module.customLabelEnabled) {
1✔
355
        builder.put(
1✔
356
            CUSTOM_LABEL_KEY, info.getCallOptions().getOption(Grpc.CALL_OPTION_CUSTOM_LABEL));
1✔
357
      }
358
      for (OpenTelemetryPlugin.ClientStreamPlugin plugin : streamPlugins) {
1✔
359
        plugin.addLabels(builder);
1✔
360
      }
1✔
361
      io.opentelemetry.api.common.Attributes attribute = builder.build();
1✔
362

363
      if (module.resource.clientAttemptDurationCounter() != null ) {
1✔
364
        module.resource.clientAttemptDurationCounter()
1✔
365
            .record(attemptNanos * SECONDS_PER_NANO, attribute, attemptsState.otelContext);
1✔
366
      }
367
      if (module.resource.clientTotalSentCompressedMessageSizeCounter() != null) {
1✔
368
        module.resource.clientTotalSentCompressedMessageSizeCounter()
1✔
369
            .record(outboundWireSize, attribute, attemptsState.otelContext);
1✔
370
      }
371
      if (module.resource.clientTotalReceivedCompressedMessageSizeCounter() != null) {
1✔
372
        module.resource.clientTotalReceivedCompressedMessageSizeCounter()
1✔
373
            .record(inboundWireSize, attribute, attemptsState.otelContext);
1✔
374
      }
375
    }
1✔
376
  }
377

378
  @VisibleForTesting
379
  static final class CallAttemptsTracerFactory extends ClientStreamTracer.Factory {
380
    private final OpenTelemetryMetricsModule module;
381
    private final String target;
382
    private final Stopwatch attemptDelayStopwatch;
383
    private final Stopwatch callStopWatch;
384
    @GuardedBy("lock")
385
    private boolean callEnded;
386
    private final String fullMethodName;
387
    private final List<OpenTelemetryPlugin.ClientCallPlugin> callPlugins;
388
    private final Context otelContext;
389
    private Status status;
390
    private long retryDelayNanos;
391
    private long callLatencyNanos;
392
    private final Object lock = new Object();
1✔
393
    private final AtomicLong attemptsPerCall = new AtomicLong();
1✔
394
    private final AtomicLong hedgedAttemptsPerCall = new AtomicLong();
1✔
395
    private final AtomicLong transparentRetriesPerCall = new AtomicLong();
1✔
396
    @GuardedBy("lock")
397
    private int activeStreams;
398
    @GuardedBy("lock")
399
    private boolean finishedCallToBeRecorded;
400

401
    CallAttemptsTracerFactory(
402
        OpenTelemetryMetricsModule module,
403
        String target,
404
        CallOptions callOptions,
405
        String fullMethodName,
406
        List<OpenTelemetryPlugin.ClientCallPlugin> callPlugins, Context otelContext) {
1✔
407
      this.module = checkNotNull(module, "module");
1✔
408
      this.target = checkNotNull(target, "target");
1✔
409
      this.fullMethodName = checkNotNull(fullMethodName, "fullMethodName");
1✔
410
      this.callPlugins = checkNotNull(callPlugins, "callPlugins");
1✔
411
      this.otelContext = checkNotNull(otelContext, "otelContext");
1✔
412
      this.attemptDelayStopwatch = module.stopwatchSupplier.get();
1✔
413
      this.callStopWatch = module.stopwatchSupplier.get().start();
1✔
414

415
      AttributesBuilder builder = io.opentelemetry.api.common.Attributes.builder()
1✔
416
          .put(METHOD_KEY, fullMethodName)
1✔
417
          .put(TARGET_KEY, target);
1✔
418
      if (module.customLabelEnabled) {
1✔
419
        builder.put(
1✔
420
            CUSTOM_LABEL_KEY, callOptions.getOption(Grpc.CALL_OPTION_CUSTOM_LABEL));
1✔
421
      }
422
      io.opentelemetry.api.common.Attributes attribute = builder.build();
1✔
423

424
      // Record here in case mewClientStreamTracer() would never be called.
425
      if (module.resource.clientAttemptCountCounter() != null) {
1✔
426
        module.resource.clientAttemptCountCounter().add(1, attribute, otelContext);
1✔
427
      }
428
    }
1✔
429

430
    @Override
431
    public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata metadata) {
432
      synchronized (lock) {
1✔
433
        if (finishedCallToBeRecorded) {
1✔
434
          // This can be the case when the call is cancelled but a retry attempt is created.
435
          return new ClientStreamTracer() {};
×
436
        }
437
        if (++activeStreams == 1 && attemptDelayStopwatch.isRunning()) {
1✔
438
          attemptDelayStopwatch.stop();
1✔
439
          retryDelayNanos = attemptDelayStopwatch.elapsed(TimeUnit.NANOSECONDS);
1✔
440
        }
441
      }
1✔
442
      // Skip recording for the first time, since it is already recorded in
443
      // CallAttemptsTracerFactory constructor. attemptsPerCall will be non-zero after the first
444
      // attempt, as first attempt cannot be a transparent retry.
445
      if (attemptsPerCall.get() > 0) {
1✔
446
        AttributesBuilder builder = io.opentelemetry.api.common.Attributes.builder()
1✔
447
            .put(METHOD_KEY, fullMethodName)
1✔
448
            .put(TARGET_KEY, target);
1✔
449
        if (module.customLabelEnabled) {
1✔
450
          builder.put(
1✔
451
              CUSTOM_LABEL_KEY, info.getCallOptions().getOption(Grpc.CALL_OPTION_CUSTOM_LABEL));
1✔
452
        }
453
        io.opentelemetry.api.common.Attributes attribute = builder.build();
1✔
454
        if (module.resource.clientAttemptCountCounter() != null) {
1✔
455
          module.resource.clientAttemptCountCounter().add(1, attribute, otelContext);
1✔
456
        }
457
      }
458
      if (info.isTransparentRetry()) {
1✔
459
        transparentRetriesPerCall.incrementAndGet();
1✔
460
      } else if (info.isHedging()) {
1✔
461
        hedgedAttemptsPerCall.incrementAndGet();
1✔
462
      } else {
463
        attemptsPerCall.incrementAndGet();
1✔
464
      }
465
      return newClientTracer(info);
1✔
466
    }
467

468
    private ClientTracer newClientTracer(StreamInfo info) {
469
      List<OpenTelemetryPlugin.ClientStreamPlugin> streamPlugins = Collections.emptyList();
1✔
470
      if (!callPlugins.isEmpty()) {
1✔
471
        streamPlugins = new ArrayList<>(callPlugins.size());
1✔
472
        for (OpenTelemetryPlugin.ClientCallPlugin plugin : callPlugins) {
1✔
473
          streamPlugins.add(plugin.newClientStreamPlugin());
1✔
474
        }
1✔
475
        streamPlugins = Collections.unmodifiableList(streamPlugins);
1✔
476
      }
477
      return new ClientTracer(this, module, info, target, fullMethodName, streamPlugins);
1✔
478
    }
479

480
    // Called whenever each attempt is ended.
481
    void attemptEnded(CallOptions callOptions) {
482
      boolean shouldRecordFinishedCall = false;
1✔
483
      synchronized (lock) {
1✔
484
        if (--activeStreams == 0) {
1✔
485
          attemptDelayStopwatch.start();
1✔
486
          if (callEnded && !finishedCallToBeRecorded) {
1✔
487
            shouldRecordFinishedCall = true;
×
488
            finishedCallToBeRecorded = true;
×
489
          }
490
        }
491
      }
1✔
492
      if (shouldRecordFinishedCall) {
1✔
493
        recordFinishedCall(callOptions);
×
494
      }
495
    }
1✔
496

497
    void callEnded(Status status, CallOptions callOptions) {
498
      callStopWatch.stop();
1✔
499
      this.status = status;
1✔
500
      boolean shouldRecordFinishedCall = false;
1✔
501
      synchronized (lock) {
1✔
502
        if (callEnded) {
1✔
503
          // TODO(https://github.com/grpc/grpc-java/issues/7921): this shouldn't happen
504
          return;
×
505
        }
506
        callEnded = true;
1✔
507
        if (activeStreams == 0 && !finishedCallToBeRecorded) {
1✔
508
          shouldRecordFinishedCall = true;
1✔
509
          finishedCallToBeRecorded = true;
1✔
510
        }
511
      }
1✔
512
      if (shouldRecordFinishedCall) {
1✔
513
        recordFinishedCall(callOptions);
1✔
514
      }
515
    }
1✔
516

517
    void recordFinishedCall(CallOptions callOptions) {
518
      if (attemptsPerCall.get() == 0) {
1✔
519
        ClientTracer tracer = newClientTracer(null);
1✔
520
        tracer.attemptNanos = attemptDelayStopwatch.elapsed(TimeUnit.NANOSECONDS);
1✔
521
        tracer.statusCode = status.getCode();
1✔
522
        tracer.recordFinishedAttempt();
1✔
523
      }
524
      callLatencyNanos = callStopWatch.elapsed(TimeUnit.NANOSECONDS);
1✔
525

526
      // Base attributes
527
      AttributesBuilder builder = io.opentelemetry.api.common.Attributes.builder()
1✔
528
          .put(METHOD_KEY, fullMethodName)
1✔
529
          .put(TARGET_KEY, target);
1✔
530
      if (module.customLabelEnabled) {
1✔
531
        builder.put(CUSTOM_LABEL_KEY, callOptions.getOption(Grpc.CALL_OPTION_CUSTOM_LABEL));
1✔
532
      }
533
      io.opentelemetry.api.common.Attributes baseAttributes = builder.build();
1✔
534

535
      // Duration
536
      if (module.resource.clientCallDurationCounter() != null) {
1✔
537
        module.resource.clientCallDurationCounter().record(
1✔
538
            callLatencyNanos * SECONDS_PER_NANO,
539
            baseAttributes.toBuilder()
1✔
540
                .put(STATUS_KEY, status.getCode().toString())
1✔
541
                .build(),
1✔
542
            otelContext
543
        );
544
      }
545

546
      // Retry counts
547
      if (module.resource.clientCallRetriesCounter() != null) {
1✔
548
        long retriesPerCall = Math.max(attemptsPerCall.get() - 1, 0);
1✔
549
        if (retriesPerCall > 0) {
1✔
550
          module.resource.clientCallRetriesCounter()
1✔
551
              .record(retriesPerCall, baseAttributes, otelContext);
1✔
552
        }
553
      }
554

555
      // Hedge counts
556
      if (module.resource.clientCallHedgesCounter() != null) {
1✔
557
        long hedges = hedgedAttemptsPerCall.get();
1✔
558
        if (hedges > 0) {
1✔
559
          module.resource.clientCallHedgesCounter()
1✔
560
              .record(hedges, baseAttributes, otelContext);
1✔
561
        }
562
      }
563

564
      // Transparent Retry counts
565
      if (module.resource.clientCallTransparentRetriesCounter() != null) {
1✔
566
        long transparentRetries = transparentRetriesPerCall.get();
1✔
567
        if (transparentRetries > 0) {
1✔
568
          module.resource.clientCallTransparentRetriesCounter()
1✔
569
              .record(transparentRetries, baseAttributes, otelContext);
1✔
570
        }
571
      }
572

573
      // Retry delay
574
      if (module.resource.clientCallRetryDelayCounter() != null) {
1✔
575
        module.resource.clientCallRetryDelayCounter().record(
1✔
576
            retryDelayNanos * SECONDS_PER_NANO,
577
            baseAttributes,
578
            otelContext
579
        );
580
      }
581
    }
1✔
582
  }
583

584
  private static final class ServerTracer extends ServerStreamTracer
585
      implements ServerCallMethodListener {
586
    @Nullable private static final AtomicIntegerFieldUpdater<ServerTracer> streamClosedUpdater;
587
    @Nullable private static final AtomicLongFieldUpdater<ServerTracer> outboundWireSizeUpdater;
588
    @Nullable private static final AtomicLongFieldUpdater<ServerTracer> inboundWireSizeUpdater;
589

590
    /*
591
     * When using Atomic*FieldUpdater, some Samsung Android 5.0.x devices encounter a bug in their
592
     * JDK reflection API that triggers a NoSuchFieldException. When this occurs, we fall back to
593
     * (potentially racy) direct updates of the volatile variables.
594
     */
595
    static {
596
      AtomicIntegerFieldUpdater<ServerTracer> tmpStreamClosedUpdater;
597
      AtomicLongFieldUpdater<ServerTracer> tmpOutboundWireSizeUpdater;
598
      AtomicLongFieldUpdater<ServerTracer> tmpInboundWireSizeUpdater;
599
      try {
600
        tmpStreamClosedUpdater =
1✔
601
            AtomicIntegerFieldUpdater.newUpdater(ServerTracer.class, "streamClosed");
1✔
602
        tmpOutboundWireSizeUpdater =
1✔
603
            AtomicLongFieldUpdater.newUpdater(ServerTracer.class, "outboundWireSize");
1✔
604
        tmpInboundWireSizeUpdater =
1✔
605
            AtomicLongFieldUpdater.newUpdater(ServerTracer.class, "inboundWireSize");
1✔
606
      } catch (Throwable t) {
×
607
        logger.log(Level.SEVERE, "Creating atomic field updaters failed", t);
×
608
        tmpStreamClosedUpdater = null;
×
609
        tmpOutboundWireSizeUpdater = null;
×
610
        tmpInboundWireSizeUpdater = null;
×
611
      }
1✔
612
      streamClosedUpdater = tmpStreamClosedUpdater;
1✔
613
      outboundWireSizeUpdater = tmpOutboundWireSizeUpdater;
1✔
614
      inboundWireSizeUpdater = tmpInboundWireSizeUpdater;
1✔
615
    }
1✔
616

617
    private final OpenTelemetryMetricsModule module;
618
    private final String fullMethodName;
619
    private final List<OpenTelemetryPlugin.ServerStreamPlugin> streamPlugins;
620
    private Context otelContext = Context.root();
1✔
621
    private volatile boolean isGeneratedMethod;
622
    private volatile int streamClosed;
623
    private final Stopwatch stopwatch;
624
    private volatile long outboundWireSize;
625
    private volatile long inboundWireSize;
626

627
    ServerTracer(OpenTelemetryMetricsModule module, String fullMethodName,
628
        List<OpenTelemetryPlugin.ServerStreamPlugin> streamPlugins) {
1✔
629
      this.module = checkNotNull(module, "module");
1✔
630
      this.fullMethodName = fullMethodName;
1✔
631
      this.streamPlugins = checkNotNull(streamPlugins, "streamPlugins");
1✔
632
      this.stopwatch = module.stopwatchSupplier.get().start();
1✔
633
    }
1✔
634

635
    @Override
636
    public io.grpc.Context filterContext(io.grpc.Context context) {
637
      Baggage baggage = BAGGAGE_KEY.get(context);
1✔
638
      if (baggage != null) {
1✔
639
        otelContext = Context.current().with(baggage);
1✔
640
      } else {
641
        otelContext = Context.current();
1✔
642
      }
643
      return context;
1✔
644
    }
645

646
    @Override
647
    public void serverCallMethodResolved(MethodDescriptor<?, ?> method) {
648
      isGeneratedMethod = method.isSampledToLocalTracing();
1✔
649
    }
1✔
650

651
    @Override
652
    public void serverCallStarted(ServerCallInfo<?, ?> callInfo) {
653
      // Only record method name as an attribute if isSampledToLocalTracing is set to true,
654
      // which is true for all generated methods. Otherwise, programmatically
655
      // created methods result in high cardinality metrics.
656
      boolean isSampledToLocalTracing = callInfo.getMethodDescriptor().isSampledToLocalTracing();
1✔
657
      isGeneratedMethod = isSampledToLocalTracing;
1✔
658

659
      io.opentelemetry.api.common.Attributes attribute =
1✔
660
          io.opentelemetry.api.common.Attributes.of(
1✔
661
              METHOD_KEY, recordMethodName(fullMethodName, isSampledToLocalTracing));
1✔
662

663
      if (module.resource.serverCallCountCounter() != null) {
1✔
664
        module.resource.serverCallCountCounter().add(1, attribute, otelContext);
1✔
665
      }
666
    }
1✔
667

668
    @Override
669
    @SuppressWarnings("NonAtomicVolatileUpdate")
670
    public void outboundWireSize(long bytes) {
671
      if (outboundWireSizeUpdater != null) {
1✔
672
        outboundWireSizeUpdater.getAndAdd(this, bytes);
1✔
673
      } else {
674
        outboundWireSize += bytes;
×
675
      }
676
    }
1✔
677

678
    @Override
679
    @SuppressWarnings("NonAtomicVolatileUpdate")
680
    public void inboundWireSize(long bytes) {
681
      if (inboundWireSizeUpdater != null) {
1✔
682
        inboundWireSizeUpdater.getAndAdd(this, bytes);
1✔
683
      } else {
684
        inboundWireSize += bytes;
×
685
      }
686
    }
1✔
687

688
    /**
689
     * Record a finished stream and mark the current time as the end time.
690
     *
691
     * <p>Can be called from any thread without synchronization.  Calling it the second time or more
692
     * is a no-op.
693
     */
694
    @Override
695
    public void streamClosed(Status status) {
696
      if (streamClosedUpdater != null) {
1✔
697
        if (streamClosedUpdater.getAndSet(this, 1) != 0) {
1✔
698
          return;
×
699
        }
700
      } else {
701
        if (streamClosed != 0) {
×
702
          return;
×
703
        }
704
        streamClosed = 1;
×
705
      }
706
      stopwatch.stop();
1✔
707
      long elapsedTimeNanos = stopwatch.elapsed(TimeUnit.NANOSECONDS);
1✔
708
      recordClosedStream(
1✔
709
          status,
710
          elapsedTimeNanos,
711
          outboundWireSize,
712
          inboundWireSize,
713
          isGeneratedMethod);
714
    }
1✔
715

716
    private void recordClosedStream(
717
        Status status,
718
        long elapsedTimeNanos,
719
        long closedOutboundWireSize,
720
        long closedInboundWireSize,
721
        boolean generatedMethod) {
722
      AttributesBuilder builder =
723
          io.opentelemetry.api.common.Attributes.builder()
1✔
724
              .put(METHOD_KEY, recordMethodName(fullMethodName, generatedMethod))
1✔
725
              .put(STATUS_KEY, status.getCode().toString());
1✔
726
      for (OpenTelemetryPlugin.ServerStreamPlugin plugin : streamPlugins) {
1✔
727
        plugin.addLabels(builder);
1✔
728
      }
1✔
729
      io.opentelemetry.api.common.Attributes attributes = builder.build();
1✔
730

731
      if (module.resource.serverCallDurationCounter() != null) {
1✔
732
        module.resource.serverCallDurationCounter()
1✔
733
            .record(elapsedTimeNanos * SECONDS_PER_NANO, attributes, otelContext);
1✔
734
      }
735
      if (module.resource.serverTotalSentCompressedMessageSizeCounter() != null) {
1✔
736
        module.resource.serverTotalSentCompressedMessageSizeCounter()
1✔
737
            .record(closedOutboundWireSize, attributes, otelContext);
1✔
738
      }
739
      if (module.resource.serverTotalReceivedCompressedMessageSizeCounter() != null) {
1✔
740
        module.resource.serverTotalReceivedCompressedMessageSizeCounter()
1✔
741
            .record(closedInboundWireSize, attributes, otelContext);
1✔
742
      }
743
    }
1✔
744
  }
745

746
  @VisibleForTesting
747
  final class ServerTracerFactory extends ServerStreamTracer.Factory {
1✔
748
    @Override
749
    public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata headers) {
750
      final List<OpenTelemetryPlugin.ServerStreamPlugin> streamPlugins;
751
      if (plugins.isEmpty()) {
1✔
752
        streamPlugins = Collections.emptyList();
1✔
753
      } else {
754
        List<OpenTelemetryPlugin.ServerStreamPlugin> streamPluginsMutable =
1✔
755
            new ArrayList<>(plugins.size());
1✔
756
        for (OpenTelemetryPlugin plugin : plugins) {
1✔
757
          streamPluginsMutable.add(plugin.newServerStreamPlugin(headers));
1✔
758
        }
1✔
759
        streamPlugins = Collections.unmodifiableList(streamPluginsMutable);
1✔
760
      }
761
      return new ServerTracer(OpenTelemetryMetricsModule.this, fullMethodName,
1✔
762
          streamPlugins);
763
    }
764
  }
765

766
  @VisibleForTesting
767
  final class MetricsClientInterceptor implements ClientInterceptor {
768
    private final String target;
769
    private final ImmutableList<OpenTelemetryPlugin> plugins;
770

771
    MetricsClientInterceptor(String target, ImmutableList<OpenTelemetryPlugin> plugins) {
1✔
772
      this.target = checkNotNull(target, "target");
1✔
773
      this.plugins = checkNotNull(plugins, "plugins");
1✔
774
    }
1✔
775

776
    @Override
777
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
778
        MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
779
      final List<OpenTelemetryPlugin.ClientCallPlugin> callPlugins;
780
      if (plugins.isEmpty()) {
1✔
781
        callPlugins = Collections.emptyList();
1✔
782
      } else {
783
        List<OpenTelemetryPlugin.ClientCallPlugin> callPluginsMutable =
1✔
784
            new ArrayList<>(plugins.size());
1✔
785
        for (OpenTelemetryPlugin plugin : plugins) {
1✔
786
          callPluginsMutable.add(plugin.newClientCallPlugin());
1✔
787
        }
1✔
788
        callPlugins = Collections.unmodifiableList(callPluginsMutable);
1✔
789
        for (OpenTelemetryPlugin.ClientCallPlugin plugin : callPlugins) {
1✔
790
          callOptions = plugin.filterCallOptions(callOptions);
1✔
791
        }
1✔
792
      }
793
      final CallOptions finalCallOptions = callOptions;
1✔
794
      // Only record method name as an attribute if isSampledToLocalTracing is set to true,
795
      // which is true for all generated methods. Otherwise, programatically
796
      // created methods result in high cardinality metrics.
797
      final CallAttemptsTracerFactory tracerFactory = new CallAttemptsTracerFactory(
1✔
798
          OpenTelemetryMetricsModule.this, target, callOptions,
799
          recordMethodName(method.getFullMethodName(), method.isSampledToLocalTracing()),
1✔
800
          callPlugins, Context.current());
1✔
801
      ClientCall<ReqT, RespT> call =
1✔
802
          next.newCall(method, callOptions.withStreamTracerFactory(tracerFactory));
1✔
803
      return new SimpleForwardingClientCall<ReqT, RespT>(call) {
1✔
804
        @Override
805
        public void start(Listener<RespT> responseListener, Metadata headers) {
806
          for (OpenTelemetryPlugin.ClientCallPlugin plugin : callPlugins) {
1✔
807
            plugin.addMetadata(headers);
1✔
808
          }
1✔
809
          delegate().start(
1✔
810
              new SimpleForwardingClientCallListener<RespT>(responseListener) {
1✔
811
                @Override
812
                public void onClose(Status status, Metadata trailers) {
813
                  tracerFactory.callEnded(status, finalCallOptions);
1✔
814
                  super.onClose(status, trailers);
1✔
815
                }
1✔
816
              },
817
              headers);
818
        }
1✔
819
      };
820
    }
821
  }
822
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc