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

grpc / grpc-java / #18713

pending completion
#18713

push

github-actions

web-flow
services, xds, orca: LRS named metrics support (#10282)

Implements gRFC A64: xDS LRS Custom Metrics Support

30622 of 34704 relevant lines covered (88.24%)

0.88 hits per line

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

97.22
/../services/src/main/java/io/grpc/services/CallMetricRecorder.java
1
/*
2
 * Copyright 2019 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.services;
18

19
import com.google.common.annotations.VisibleForTesting;
20
import com.google.errorprone.annotations.InlineMe;
21
import io.grpc.Context;
22
import io.grpc.ExperimentalApi;
23
import java.util.Collections;
24
import java.util.Map;
25
import java.util.concurrent.ConcurrentHashMap;
26
import java.util.concurrent.atomic.AtomicReference;
27
import javax.annotation.concurrent.ThreadSafe;
28

29
/**
30
 * Utility to record call metrics for load-balancing. One instance per call.
31
 */
32
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/6012")
33
@ThreadSafe
34
public final class CallMetricRecorder {
1✔
35
  private static final CallMetricRecorder NOOP = new CallMetricRecorder().disable();
1✔
36

37
  static final Context.Key<CallMetricRecorder> CONTEXT_KEY =
1✔
38
      Context.key("io.grpc.services.CallMetricRecorder");
1✔
39

40
  private final AtomicReference<ConcurrentHashMap<String, Double>> utilizationMetrics =
1✔
41
      new AtomicReference<>();
42
  private final AtomicReference<ConcurrentHashMap<String, Double>> requestCostMetrics =
1✔
43
      new AtomicReference<>();
44
  private final AtomicReference<ConcurrentHashMap<String, Double>> namedMetrics =
1✔
45
      new AtomicReference<>();
46
  private double cpuUtilizationMetric = 0;
1✔
47
  private double applicationUtilizationMetric = 0;
1✔
48
  private double memoryUtilizationMetric = 0;
1✔
49
  private double qps = 0;
1✔
50
  private double eps = 0;
1✔
51
  private volatile boolean disabled;
52

53
  /**
54
   * Returns the call metric recorder attached to the current {@link Context}.  If there is none,
55
   * returns a no-op recorder.
56
   *
57
   * <p><strong>IMPORTANT:</strong>It returns the recorder specifically for the current RPC call.
58
   * <b>DO NOT</b> save the returned object or share it between different RPC calls.
59
   *
60
   * <p><strong>IMPORTANT:</strong>It must be called under the {@link Context} under which the RPC
61
   * handler was called.  If it is called from a different thread, the Context must be propagated to
62
   * the same thread, e.g., with {@link Context#wrap(Runnable)}.
63
   *
64
   * @since 1.23.0
65
   */
66
  public static CallMetricRecorder getCurrent() {
67
    CallMetricRecorder recorder = CONTEXT_KEY.get();
1✔
68
    return recorder != null ? recorder : NOOP;
1✔
69
  }
70

71
  /**
72
   * Records a call metric measurement for utilization in the range [0, 1]. Values outside the valid
73
   * range are ignored. If RPC has already finished, this method is no-op.
74
   *
75
   * <p>A latter record will overwrite its former name-sakes.
76
   *
77
   * @return this recorder object
78
   * @since 1.23.0
79
   */
80
  public CallMetricRecorder recordUtilizationMetric(String name, double value) {
81
    if (disabled || !MetricRecorderHelper.isUtilizationValid(value)) {
1✔
82
      return this;
1✔
83
    }
84
    if (utilizationMetrics.get() == null) {
1✔
85
      // The chance of race of creation of the map should be very small, so it should be fine
86
      // to create these maps that might be discarded.
87
      utilizationMetrics.compareAndSet(null, new ConcurrentHashMap<String, Double>());
1✔
88
    }
89
    utilizationMetrics.get().put(name, value);
1✔
90
    return this;
1✔
91
  }
92

93
  /**
94
   * Records a call metric measurement for request cost.
95
   * If RPC has already finished, this method is no-op.
96
   *
97
   * <p>A latter record will overwrite its former name-sakes.
98
   *
99
   * @return this recorder object
100
   * @since 1.47.0
101
   * @deprecated use {@link #recordRequestCostMetric} instead.
102
   *     This method will be removed in the future.
103
   */
104
  @Deprecated
105
  @InlineMe(replacement = "this.recordRequestCostMetric(name, value)")
106
  public CallMetricRecorder recordCallMetric(String name, double value) {
107
    return recordRequestCostMetric(name, value);
×
108
  }
109

110
  /**
111
   * Records a call metric measurement for request cost.
112
   * If RPC has already finished, this method is no-op.
113
   *
114
   * <p>A latter record will overwrite its former name-sakes.
115
   *
116
   * @return this recorder object
117
   * @since 1.48.1
118
   */
119
  public CallMetricRecorder recordRequestCostMetric(String name, double value) {
120
    if (disabled) {
1✔
121
      return this;
×
122
    }
123
    if (requestCostMetrics.get() == null) {
1✔
124
      // The chance of race of creation of the map should be very small, so it should be fine
125
      // to create these maps that might be discarded.
126
      requestCostMetrics.compareAndSet(null, new ConcurrentHashMap<String, Double>());
1✔
127
    }
128
    requestCostMetrics.get().put(name, value);
1✔
129
    return this;
1✔
130
  }
131

132
  /**
133
   * Records an application-specific opaque custom metric measurement. If RPC has already finished,
134
   * this method is no-op.
135
   *
136
   * <p>A latter record will overwrite its former name-sakes.
137
   *
138
   * @return this recorder object
139
   */
140
  public CallMetricRecorder recordNamedMetric(String name, double value) {
141
    if (disabled) {
1✔
142
      return this;
1✔
143
    }
144
    if (namedMetrics.get() == null) {
1✔
145
      // The chance of race of creation of the map should be very small, so it should be fine
146
      // to create these maps that might be discarded.
147
      namedMetrics.compareAndSet(null, new ConcurrentHashMap<String, Double>());
1✔
148
    }
149
    namedMetrics.get().put(name, value);
1✔
150
    return this;
1✔
151
  }
152

153
  /**
154
   * Records a call metric measurement for CPU utilization in the range [0, inf). Values outside the
155
   * valid range are ignored. If RPC has already finished, this method is no-op.
156
   *
157
   * <p>A latter record will overwrite its former name-sakes.
158
   *
159
   * @return this recorder object
160
   * @since 1.47.0
161
   */
162
  public CallMetricRecorder recordCpuUtilizationMetric(double value) {
163
    if (disabled || !MetricRecorderHelper.isCpuOrApplicationUtilizationValid(value)) {
1✔
164
      return this;
1✔
165
    }
166
    cpuUtilizationMetric = value;
1✔
167
    return this;
1✔
168
  }
169

170
  /**
171
   * Records a call metric measurement for application specific utilization in the range [0, inf).
172
   * Values outside the valid range are ignored. If RPC has already finished, this method is no-op.
173
   *
174
   * <p>A latter record will overwrite its former name-sakes.
175
   *
176
   * @return this recorder object
177
   */
178
  public CallMetricRecorder recordApplicationUtilizationMetric(double value) {
179
    if (disabled || !MetricRecorderHelper.isCpuOrApplicationUtilizationValid(value)) {
1✔
180
      return this;
1✔
181
    }
182
    applicationUtilizationMetric = value;
1✔
183
    return this;
1✔
184
  }
185

186
  /**
187
   * Records a call metric measurement for memory utilization in the range [0, 1]. Values outside
188
   * the valid range are ignored. If RPC has already finished, this method is no-op.
189
   *
190
   * <p>A latter record will overwrite its former name-sakes.
191
   *
192
   * @return this recorder object
193
   * @since 1.47.0
194
   */
195
  public CallMetricRecorder recordMemoryUtilizationMetric(double value) {
196
    if (disabled || !MetricRecorderHelper.isUtilizationValid(value)) {
1✔
197
      return this;
1✔
198
    }
199
    memoryUtilizationMetric = value;
1✔
200
    return this;
1✔
201
  }
202

203
  /**
204
   * Records a call metric measurement for queries per second (qps) in the range [0, inf). Values
205
   * outside the valid range are ignored. If RPC has already finished, this method is no-op.
206
   *
207
   * <p>A latter record will overwrite its former name-sakes.
208
   *
209
   * @return this recorder object
210
   * @since 1.54.0
211
   */
212
  public CallMetricRecorder recordQpsMetric(double value) {
213
    if (disabled || !MetricRecorderHelper.isRateValid(value)) {
1✔
214
      return this;
1✔
215
    }
216
    qps = value;
1✔
217
    return this;
1✔
218
  }
219

220
  /**
221
   * Records a call metric measurement for errors per second (eps) in the range [0, inf). Values
222
   * outside the valid range are ignored. If RPC has already finished, this method is no-op.
223
   *
224
   * <p>A latter record will overwrite its former name-sakes.
225
   *
226
   * @return this recorder object
227
   */
228
  public CallMetricRecorder recordEpsMetric(double value) {
229
    if (disabled || !MetricRecorderHelper.isRateValid(value)) {
1✔
230
      return this;
1✔
231
    }
232
    eps = value;
1✔
233
    return this;
1✔
234
  }
235

236
  /**
237
   * Returns all request cost metric values. No more metric values will be recorded after this
238
   * method is called. Calling this method multiple times returns the same collection of metric
239
   * values.
240
   *
241
   * @return a map containing all saved metric name-value pairs.
242
   */
243
  Map<String, Double> finalizeAndDump() {
244
    disabled = true;
1✔
245
    Map<String, Double> savedMetrics = requestCostMetrics.get();
1✔
246
    if (savedMetrics == null) {
1✔
247
      return Collections.emptyMap();
1✔
248
    }
249
    return Collections.unmodifiableMap(savedMetrics);
1✔
250
  }
251

252
  /**
253
   * Returns all save metric values. No more metric values will be recorded after this method is
254
   * called. Calling this method multiple times returns the same collection of metric values.
255
   *
256
   * @return a per-request ORCA reports containing all saved metrics.
257
   */
258
  MetricReport finalizeAndDump2() {
259
    Map<String, Double> savedRequestCostMetrics = finalizeAndDump();
1✔
260
    Map<String, Double> savedUtilizationMetrics = utilizationMetrics.get();
1✔
261
    Map<String, Double> savedNamedMetrics = namedMetrics.get();
1✔
262
    if (savedUtilizationMetrics == null) {
1✔
263
      savedUtilizationMetrics = Collections.emptyMap();
1✔
264
    }
265
    if (savedNamedMetrics == null) {
1✔
266
      savedNamedMetrics = Collections.emptyMap();
1✔
267
    }
268
    return new MetricReport(cpuUtilizationMetric, applicationUtilizationMetric,
1✔
269
        memoryUtilizationMetric, qps, eps, Collections.unmodifiableMap(savedRequestCostMetrics),
1✔
270
        Collections.unmodifiableMap(savedUtilizationMetrics),
1✔
271
        Collections.unmodifiableMap(savedNamedMetrics)
1✔
272
    );
273
  }
274

275
  @VisibleForTesting
276
  boolean isDisabled() {
277
    return disabled;
1✔
278
  }
279

280
  /**
281
   * Turn this recorder into a no-op one.
282
   */
283
  private CallMetricRecorder disable() {
284
    disabled = true;
1✔
285
    return this;
1✔
286
  }
287
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc