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

grpc / grpc-java / #20365

27 Jul 2026 06:04AM UTC coverage: 89.199% (+0.07%) from 89.125%
#20365

push

github

web-flow
core: Implement LB Delay Observability (Proposal A121) (#12807)

This PR implements **Attempt-Level RPC Delay Observability** across the core channel transport, built-in load balancers, xDS policies, and the OpenTelemetry telemetry plugin, aligned with [gRPC Proposal A121](https://github.com/grpc/proposal/pull/556).

38276 of 42911 relevant lines covered (89.2%)

0.89 hits per line

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

86.0
/../api/src/main/java/io/grpc/ClientStreamTracer.java
1
/*
2
 * Copyright 2017 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;
18

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

21
import com.google.common.base.MoreObjects;
22

23
/**
24
 * {@link StreamTracer} for the client-side.
25
 *
26
 * <p>This class is thread-safe.
27
 */
28
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2861")
29
public abstract class ClientStreamTracer extends StreamTracer {
1✔
30
  /**
31
   * Indicates how long the call was delayed, in nanoseconds, due to waiting for name resolution
32
   * result. If the call option is not set, the call did not experience name resolution delay.
33
   */
34
  public static final CallOptions.Key<Long> NAME_RESOLUTION_DELAYED =
1✔
35
      CallOptions.Key.create("io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED");
1✔
36

37
  /**
38
   * The stream is being created on a ready transport.
39
   *
40
   * @param headers the mutable initial metadata. Modifications to it will be sent to the socket but
41
   *     not be seen by client interceptors and the application.
42
   *
43
   * @since 1.40.0
44
   */
45
  public void streamCreated(@Grpc.TransportAttr Attributes transportAttrs, Metadata headers) {
46
  }
1✔
47

48
  /**
49
   * Name resolution is completed and the connection starts getting established. This method is only
50
   * invoked on the streams that encounter such delay.
51
   *
52
   * </p>gRPC buffers the client call if the remote address and configurations, e.g. timeouts and
53
   * retry policy, are not ready. Asynchronously gRPC internally does the name resolution to get
54
   * this information. The streams that are processed immediately on ready transports by the time
55
   * the RPC comes do not go through the pending process, thus this callback will not be invoked.
56
   */
57
  public void createPendingStream() {
58
  }
1✔
59

60
  /**
61
   * Called when an attempt-level delay segment (such as waiting for a load balancing pick or
62
   * connection establishment) starts.
63
   *
64
   * <p>This method is invoked synchronously on the attempt thread. Implementations should start
65
   * internal timers or child tracing spans (named strictly {@code "Attempt Delay"}) carrying the
66
   * canonical {@code grpc.delay_type} attribute.
67
   *
68
   * @param delayType canonical low-cardinality label categorizing the delay (e.g., "connecting")
69
   * @param delayReason high-cardinality diagnostic string describing granular runtime conditions
70
   * @since 1.82.0
71
   */
72
  public void recordAttemptDelayStart(String delayType, String delayReason) {
73
  }
1✔
74

75
  /**
76
   * Called when an attempt-level delay reason changes while the overall delay type remains
77
   * constant (for example, when a priority load balancing policy fails over between tiers).
78
   *
79
   * <p>Implementations should record structured events (such as {@code "Delay state transition"})
80
   * on the active delay span without recreating the span or resetting cumulative timers.
81
   *
82
   * @param delayReason updated high-cardinality diagnostic string describing new conditions
83
   * @since 1.82.0
84
   */
85
  public void recordAttemptDelayReasonChanged(String delayReason) {
86
  }
1✔
87

88
  /**
89
   * Called when an attempt-level delay segment ends upon successful pick or stream creation.
90
   *
91
   * <p>Implementations should simultaneously close active child tracing spans and record elapsed
92
   * duration to the {@code grpc.client.attempt.delay.duration} histogram.
93
   *
94
   * @since 1.82.0
95
   */
96
  public void recordAttemptDelayEnd() {
97
  }
1✔
98

99
  /**
100
   * Headers has been sent to the socket.
101
   */
102
  public void outboundHeaders() {
103
  }
1✔
104

105
  /**
106
   * Headers has been received from the server.
107
   */
108
  public void inboundHeaders() {
109
  }
1✔
110

111
  /**
112
   * Headers has been received from the server. This method does not pass ownership to {@code
113
   * headers}, so implementations must not access the metadata after returning. Modifications to the
114
   * metadata within this method will be seen by interceptors and the application.
115
   *
116
   * @param headers the received header metadata
117
   */
118
  public void inboundHeaders(Metadata headers) {
119
    inboundHeaders();
1✔
120
  }
1✔
121

122
  /**
123
   * Trailing metadata has been received from the server. This method does not pass ownership to
124
   * {@code trailers}, so implementations must not access the metadata after returning.
125
   * Modifications to the metadata within this method will be seen by interceptors and the
126
   * application.
127
   *
128
   * @param trailers the received trailing metadata
129
   * @since 1.17.0
130
   */
131
  public void inboundTrailers(Metadata trailers) {
132
  }
1✔
133

134
  /**
135
   * Information providing context to the call became available.
136
   */
137
  @Internal
138
  public void addOptionalLabel(String key, String value) {
139
  }
1✔
140

141
  /**
142
   * Factory class for {@link ClientStreamTracer}.
143
   */
144
  public abstract static class Factory {
1✔
145
    /**
146
     * Creates a {@link ClientStreamTracer} for a new client stream.  This is called inside the
147
     * transport when it's creating the stream.
148
     *
149
     * @param info information about the stream
150
     * @param headers the mutable headers of the stream. It can be safely mutated within this
151
     *        method.  Changes made to it will be sent by the stream.  It should not be saved
152
     *        because it is not safe for read or write after the method returns.
153
     *
154
     * @since 1.20.0
155
     */
156
    public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) {
157
      throw new UnsupportedOperationException("Not implemented");
×
158
    }
159
  }
160

161
  /**
162
   * Information about a stream.
163
   *
164
   * <p>Note this class doesn't override {@code equals()} and {@code hashCode}, as is the case for
165
   * {@link CallOptions}.
166
   *
167
   * @since 1.20.0
168
   */
169
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2861")
170
  public static final class StreamInfo {
171
    private final CallOptions callOptions;
172
    private final int previousAttempts;
173
    private final boolean isTransparentRetry;
174
    private final boolean isHedging;
175

176
    StreamInfo(
177
        CallOptions callOptions, int previousAttempts, boolean isTransparentRetry,
178
        boolean isHedging) {
1✔
179
      this.callOptions = checkNotNull(callOptions, "callOptions");
1✔
180
      this.previousAttempts = previousAttempts;
1✔
181
      this.isTransparentRetry = isTransparentRetry;
1✔
182
      this.isHedging = isHedging;
1✔
183
    }
1✔
184

185
    /**
186
     * Returns the effective CallOptions of the call.
187
     */
188
    public CallOptions getCallOptions() {
189
      return callOptions;
1✔
190
    }
191

192
    /**
193
     * Returns the number of preceding attempts for the RPC.
194
     *
195
     * @since 1.40.0
196
     */
197
    public int getPreviousAttempts() {
198
      return previousAttempts;
1✔
199
    }
200

201
    /**
202
     * Whether the stream is a transparent retry.
203
     *
204
     * @since 1.40.0
205
     */
206
    public boolean isTransparentRetry() {
207
      return isTransparentRetry;
1✔
208
    }
209

210
    /**
211
     * Whether the stream is hedging.
212
     *
213
     * @since 1.74.0
214
     */
215
    public boolean isHedging() {
216
      return isHedging;
1✔
217
    }
218

219
    /**
220
     * Converts this StreamInfo into a new Builder.
221
     *
222
     * @since 1.21.0
223
     */
224
    public Builder toBuilder() {
225
      return new Builder()
1✔
226
          .setCallOptions(callOptions)
1✔
227
          .setPreviousAttempts(previousAttempts)
1✔
228
          .setIsTransparentRetry(isTransparentRetry)
1✔
229
          .setIsHedging(isHedging);
1✔
230

231
    }
232

233
    /**
234
     * Creates an empty Builder.
235
     *
236
     * @since 1.21.0
237
     */
238
    public static Builder newBuilder() {
239
      return new Builder();
1✔
240
    }
241

242
    @Override
243
    public String toString() {
244
      return MoreObjects.toStringHelper(this)
×
245
          .add("callOptions", callOptions)
×
246
          .add("previousAttempts", previousAttempts)
×
247
          .add("isTransparentRetry", isTransparentRetry)
×
248
          .add("isHedging", isHedging)
×
249
          .toString();
×
250
    }
251

252
    /**
253
     * Builds {@link StreamInfo} objects.
254
     *
255
     * @since 1.21.0
256
     */
257
    public static final class Builder {
258
      private CallOptions callOptions = CallOptions.DEFAULT;
1✔
259
      private int previousAttempts;
260
      private boolean isTransparentRetry;
261
      private boolean isHedging;
262

263
      Builder() {
1✔
264
      }
1✔
265

266
      /**
267
       * Sets the effective CallOptions of the call.  This field is optional.
268
       */
269
      public Builder setCallOptions(CallOptions callOptions) {
270
        this.callOptions = checkNotNull(callOptions, "callOptions cannot be null");
1✔
271
        return this;
1✔
272
      }
273

274
      /**
275
       * Set the number of preceding attempts of the RPC.
276
       *
277
       * @since 1.40.0
278
       */
279
      public Builder setPreviousAttempts(int previousAttempts) {
280
        this.previousAttempts = previousAttempts;
1✔
281
        return this;
1✔
282
      }
283

284
      /**
285
       * Sets whether the stream is a transparent retry.
286
       *
287
       * @since 1.40.0
288
       */
289
      public Builder setIsTransparentRetry(boolean isTransparentRetry) {
290
        this.isTransparentRetry = isTransparentRetry;
1✔
291
        return this;
1✔
292
      }
293

294
      /**
295
       * Sets whether the stream is hedging.
296
       *
297
       * @since 1.74.0
298
       */
299
      public Builder setIsHedging(boolean isHedging) {
300
        this.isHedging = isHedging;
1✔
301
        return this;
1✔
302
      }
303

304
      /**
305
       * Builds a new StreamInfo.
306
       */
307
      public StreamInfo build() {
308
        return new StreamInfo(callOptions, previousAttempts, isTransparentRetry, isHedging);
1✔
309
      }
310
    }
311
  }
312
}
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