/build/source/nativelink-util/src/metrics.rs
Line | Count | Source |
1 | | // Copyright 2025 Trace Machina, Inc. All rights reserved. |
2 | | // |
3 | | // Licensed under the Business Source License, Version 1.1 (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may request a copy of the License by emailing contact@nativelink.com. |
6 | | // |
7 | | // Use of this module requires an enterprise license agreement, which can be |
8 | | // attained by emailing contact@nativelink.com or signing up for Nativelink |
9 | | // Cloud at app.nativelink.com. |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, software |
12 | | // distributed under the License is distributed on an "AS IS" BASIS, |
13 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14 | | // See the License for the specific language governing permissions and |
15 | | // limitations under the License. |
16 | | |
17 | | use std::sync::LazyLock; |
18 | | use std::time::SystemTime; |
19 | | |
20 | | use opentelemetry::{InstrumentationScope, KeyValue, Value, global, metrics}; |
21 | | |
22 | | use crate::action_messages::{ActionResult, ActionStage}; |
23 | | |
24 | | // Metric attribute keys for cache operations. |
25 | | pub const CACHE_TYPE: &str = "cache.type"; |
26 | | pub const CACHE_OPERATION: &str = "cache.operation.name"; |
27 | | pub const CACHE_RESULT: &str = "cache.operation.result"; |
28 | | |
29 | | // Metric attribute keys for remote execution operations. |
30 | | pub const EXECUTION_STAGE: &str = "execution.stage"; |
31 | | pub const EXECUTION_RESULT: &str = "execution.result"; |
32 | | pub const EXECUTION_INSTANCE: &str = "execution.instance"; |
33 | | pub const EXECUTION_PRIORITY: &str = "execution.priority"; |
34 | | pub const EXECUTION_WORKER_ID: &str = "execution.worker_id"; |
35 | | pub const EXECUTION_OUTCOME: &str = "execution.outcome"; |
36 | | pub const EXECUTION_ACTION_MNEMONIC: &str = "execution.action_mnemonic"; |
37 | | pub const EXECUTION_EXIT_CODE: &str = "execution.exit_code"; |
38 | | pub const EXECUTION_ACTION_DIGEST: &str = "execution.action_digest"; |
39 | | |
40 | | // Metric attribute keys for gRPC serving, following OTel rpc semconv. |
41 | | pub const RPC_SERVICE: &str = "rpc.service"; |
42 | | pub const RPC_METHOD: &str = "rpc.method"; |
43 | | pub const RPC_STATUS_CODE: &str = "rpc.grpc.status_code"; |
44 | | |
45 | | // Metric attribute keys for the scheduler. |
46 | | pub const SCHEDULER_MATCH_RESULT: &str = "scheduler.match.result"; |
47 | | /// The platform properties no worker could satisfy, comma separated. |
48 | | pub const SCHEDULER_UNSATISFIABLE_PROPERTIES: &str = "scheduler.unsatisfiable.properties"; |
49 | | pub const SCHEDULER_HOLD_OUTCOME: &str = "scheduler.hold.outcome"; |
50 | | /// The age bucket of a queued or executing action. |
51 | | pub const SCHEDULER_ACTION_AGE: &str = "scheduler.action.age"; |
52 | | |
53 | | // Metric attribute keys for the worker. |
54 | | pub const WORKER_PREPARE_OUTCOME: &str = "worker.prepare.outcome"; |
55 | | pub const WORKER_RELINK_KIND: &str = "worker.relink.kind"; |
56 | | |
57 | | // Metric attribute keys for object stores. |
58 | | pub const STORE_OBJECT_PROVIDER: &str = "store.object.provider"; |
59 | | pub const STORE_OBJECT_OPERATION: &str = "store.object.operation"; |
60 | | |
61 | | // Metric attribute keys for tiered stores. |
62 | | pub const STORE_TIER: &str = "store.tier"; |
63 | | pub const STORE_RESULT: &str = "store.result"; |
64 | | pub const STORE_DIRECTION: &str = "store.direction"; |
65 | | |
66 | | // Metric attribute keys for connection pools. |
67 | | pub const CONNECTION_POOL: &str = "connection.pool"; |
68 | | pub const CONNECTION_RESULT: &str = "connection.result"; |
69 | | |
70 | | // Metric attribute keys for health checks. |
71 | | pub const HEALTH_NAMESPACE: &str = "health.namespace"; |
72 | | pub const HEALTH_STATUS: &str = "health.status"; |
73 | | |
74 | | // Metric attribute keys for the worker fleet. |
75 | | pub const WORKER_STATE: &str = "worker.state"; |
76 | | pub const WORKER_DISCONNECT_REASON: &str = "worker.disconnect.reason"; |
77 | | |
78 | | /// Why a worker left the pool. |
79 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
80 | | pub enum WorkerDisconnectReason { |
81 | | /// The worker's connection ended without a drain. |
82 | | Disconnected, |
83 | | /// The worker announced a drain and its connection ended after it. |
84 | | Drained, |
85 | | /// No message within `worker_timeout_s`. |
86 | | Timeout, |
87 | | /// A kill sent to the worker was never acknowledged. |
88 | | KillUnacknowledged, |
89 | | /// A command to the worker or an update from it failed. |
90 | | Error, |
91 | | /// Removed through the admin API. |
92 | | Removed, |
93 | | /// The scheduler is shutting down. |
94 | | Shutdown, |
95 | | } |
96 | | |
97 | | impl WorkerDisconnectReason { |
98 | | #[must_use] |
99 | 937 | pub const fn as_str(self) -> &'static str { |
100 | 937 | match self { |
101 | 578 | Self::Disconnected => "disconnected", |
102 | 1 | Self::Drained => "drained", |
103 | 334 | Self::Timeout => "timeout", |
104 | 2 | Self::KillUnacknowledged => "kill_unacknowledged", |
105 | 1 | Self::Error => "error", |
106 | 21 | Self::Removed => "removed", |
107 | 0 | Self::Shutdown => "shutdown", |
108 | | } |
109 | 937 | } |
110 | | } |
111 | | |
112 | | /// Cache operation types for metrics classification. |
113 | | #[derive(Debug, Clone, Copy)] |
114 | | pub enum CacheOperationName { |
115 | | /// Data retrieval operations (get, peek, contains, etc.) |
116 | | Read, |
117 | | /// Data storage operations (insert, update, replace, etc.) |
118 | | Write, |
119 | | /// Explicit data removal operations |
120 | | Delete, |
121 | | /// Automatic cache maintenance (evictions, TTL cleanup, etc.) |
122 | | Evict, |
123 | | } |
124 | | |
125 | | impl From<CacheOperationName> for Value { |
126 | 198 | fn from(op: CacheOperationName) -> Self { |
127 | 198 | match op { |
128 | 72 | CacheOperationName::Read => Self::from("read"), |
129 | 36 | CacheOperationName::Write => Self::from("write"), |
130 | 54 | CacheOperationName::Delete => Self::from("delete"), |
131 | 36 | CacheOperationName::Evict => Self::from("evict"), |
132 | | } |
133 | 198 | } |
134 | | } |
135 | | |
136 | | /// Results of cache operations. |
137 | | /// |
138 | | /// Result semantics vary by operation type: |
139 | | /// - Read: Hit/Miss/Expired indicate data availability |
140 | | /// - Write/Delete/Evict: Success/Error indicate completion status |
141 | | #[derive(Debug, Clone, Copy)] |
142 | | pub enum CacheOperationResult { |
143 | | /// Data found and valid (Read operations) |
144 | | Hit, |
145 | | /// Data not found (Read operations) |
146 | | Miss, |
147 | | /// Data found but invalid/expired (Read operations) |
148 | | Expired, |
149 | | /// Operation completed successfully (Write/Delete/Evict operations) |
150 | | Success, |
151 | | /// Operation failed (any operation type) |
152 | | Error, |
153 | | } |
154 | | |
155 | | impl From<CacheOperationResult> for Value { |
156 | 198 | fn from(result: CacheOperationResult) -> Self { |
157 | 198 | match result { |
158 | 18 | CacheOperationResult::Hit => Self::from("hit"), |
159 | 36 | CacheOperationResult::Miss => Self::from("miss"), |
160 | 36 | CacheOperationResult::Expired => Self::from("expired"), |
161 | 54 | CacheOperationResult::Success => Self::from("success"), |
162 | 54 | CacheOperationResult::Error => Self::from("error"), |
163 | | } |
164 | 198 | } |
165 | | } |
166 | | |
167 | | /// Remote execution stages for metrics classification. |
168 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
169 | | pub enum ExecutionStage { |
170 | | /// Unknown stage |
171 | | Unknown, |
172 | | /// Checking cache for existing results |
173 | | CacheCheck, |
174 | | /// Action is queued waiting for execution |
175 | | Queued, |
176 | | /// Action is being executed by a worker |
177 | | Executing, |
178 | | /// Action execution completed |
179 | | Completed, |
180 | | } |
181 | | |
182 | | impl From<ExecutionStage> for Value { |
183 | 76.6k | fn from(stage: ExecutionStage) -> Self { |
184 | 76.6k | match stage { |
185 | 1 | ExecutionStage::Unknown => Self::from("unknown"), |
186 | 2 | ExecutionStage::CacheCheck => Self::from("cache_check"), |
187 | 24.7k | ExecutionStage::Queued => Self::from("queued"), |
188 | 30.5k | ExecutionStage::Executing => Self::from("executing"), |
189 | 21.2k | ExecutionStage::Completed => Self::from("completed"), |
190 | | } |
191 | 76.6k | } |
192 | | } |
193 | | |
194 | | impl From<ActionStage> for ExecutionStage { |
195 | 5 | fn from(stage: ActionStage) -> Self { |
196 | 5 | match stage { |
197 | 1 | ActionStage::Unknown => Self::Unknown, |
198 | 1 | ActionStage::CacheCheck => Self::CacheCheck, |
199 | 1 | ActionStage::Queued => Self::Queued, |
200 | 1 | ActionStage::Executing => Self::Executing, |
201 | 1 | ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => Self::Completed, |
202 | | } |
203 | 5 | } |
204 | | } |
205 | | |
206 | | impl From<&ActionStage> for ExecutionStage { |
207 | 75.0k | fn from(stage: &ActionStage) -> Self { |
208 | 75.0k | match stage { |
209 | 1 | ActionStage::Unknown => Self::Unknown, |
210 | 1 | ActionStage::CacheCheck => Self::CacheCheck, |
211 | 13.2k | ActionStage::Queued => Self::Queued, |
212 | 30.5k | ActionStage::Executing => Self::Executing, |
213 | 31.2k | ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => Self::Completed, |
214 | | } |
215 | 75.0k | } |
216 | | } |
217 | | |
218 | | /// Results of remote execution operations. |
219 | | #[derive(Debug, Clone, Copy)] |
220 | | pub enum ExecutionResult { |
221 | | /// Execution completed successfully |
222 | | Success, |
223 | | /// Execution failed |
224 | | Failure, |
225 | | /// Execution was cancelled |
226 | | Cancelled, |
227 | | /// Execution timed out |
228 | | Timeout, |
229 | | /// Result was found in cache |
230 | | CacheHit, |
231 | | } |
232 | | |
233 | | impl From<ExecutionResult> for Value { |
234 | 21.2k | fn from(result: ExecutionResult) -> Self { |
235 | 21.2k | match result { |
236 | 19.0k | ExecutionResult::Success => Self::from("success"), |
237 | 2.15k | ExecutionResult::Failure => Self::from("failure"), |
238 | 1 | ExecutionResult::Cancelled => Self::from("cancelled"), |
239 | 1 | ExecutionResult::Timeout => Self::from("timeout"), |
240 | 1 | ExecutionResult::CacheHit => Self::from("cache_hit"), |
241 | | } |
242 | 21.2k | } |
243 | | } |
244 | | |
245 | | /// Pre-allocated attribute combinations for efficient cache metrics collection. |
246 | | /// |
247 | | /// Avoids runtime allocation by pre-computing common attribute combinations |
248 | | /// for cache operations and results. |
249 | | #[derive(Debug)] |
250 | | pub struct CacheMetricAttrs { |
251 | | // Read operation attributes |
252 | | read_hit: Vec<KeyValue>, |
253 | | read_miss: Vec<KeyValue>, |
254 | | read_expired: Vec<KeyValue>, |
255 | | read_error: Vec<KeyValue>, |
256 | | |
257 | | // Write operation attributes |
258 | | write_success: Vec<KeyValue>, |
259 | | write_error: Vec<KeyValue>, |
260 | | |
261 | | // Delete operation attributes |
262 | | delete_success: Vec<KeyValue>, |
263 | | delete_miss: Vec<KeyValue>, |
264 | | delete_error: Vec<KeyValue>, |
265 | | |
266 | | // Evict operation attributes |
267 | | evict_success: Vec<KeyValue>, |
268 | | evict_expired: Vec<KeyValue>, |
269 | | } |
270 | | |
271 | | impl CacheMetricAttrs { |
272 | | /// Creates a new set of pre-computed attributes. |
273 | | /// |
274 | | /// The `base_attrs` are included in all attribute combinations (e.g., cache |
275 | | /// type, instance ID). |
276 | | #[must_use] |
277 | 18 | pub fn new(base_attrs: &[KeyValue]) -> Self { |
278 | 198 | let make_attrs18 = |op: CacheOperationName, result: CacheOperationResult| { |
279 | 198 | let mut attrs = base_attrs.to_vec(); |
280 | 198 | attrs.push(KeyValue::new(CACHE_OPERATION, op)); |
281 | 198 | attrs.push(KeyValue::new(CACHE_RESULT, result)); |
282 | 198 | attrs |
283 | 198 | }; |
284 | | |
285 | 18 | Self { |
286 | 18 | read_hit: make_attrs(CacheOperationName::Read, CacheOperationResult::Hit), |
287 | 18 | read_miss: make_attrs(CacheOperationName::Read, CacheOperationResult::Miss), |
288 | 18 | read_expired: make_attrs(CacheOperationName::Read, CacheOperationResult::Expired), |
289 | 18 | read_error: make_attrs(CacheOperationName::Read, CacheOperationResult::Error), |
290 | 18 | |
291 | 18 | write_success: make_attrs(CacheOperationName::Write, CacheOperationResult::Success), |
292 | 18 | write_error: make_attrs(CacheOperationName::Write, CacheOperationResult::Error), |
293 | 18 | |
294 | 18 | delete_success: make_attrs(CacheOperationName::Delete, CacheOperationResult::Success), |
295 | 18 | delete_miss: make_attrs(CacheOperationName::Delete, CacheOperationResult::Miss), |
296 | 18 | delete_error: make_attrs(CacheOperationName::Delete, CacheOperationResult::Error), |
297 | 18 | |
298 | 18 | evict_success: make_attrs(CacheOperationName::Evict, CacheOperationResult::Success), |
299 | 18 | evict_expired: make_attrs(CacheOperationName::Evict, CacheOperationResult::Expired), |
300 | 18 | } |
301 | 18 | } |
302 | | |
303 | | // Attribute accessors |
304 | | #[must_use] |
305 | 126 | pub fn read_hit(&self) -> &[KeyValue] { |
306 | 126 | &self.read_hit |
307 | 126 | } |
308 | | #[must_use] |
309 | 24 | pub fn read_miss(&self) -> &[KeyValue] { |
310 | 24 | &self.read_miss |
311 | 24 | } |
312 | | #[must_use] |
313 | 0 | pub fn read_expired(&self) -> &[KeyValue] { |
314 | 0 | &self.read_expired |
315 | 0 | } |
316 | | #[must_use] |
317 | 0 | pub fn read_error(&self) -> &[KeyValue] { |
318 | 0 | &self.read_error |
319 | 0 | } |
320 | | #[must_use] |
321 | 36 | pub fn write_success(&self) -> &[KeyValue] { |
322 | 36 | &self.write_success |
323 | 36 | } |
324 | | #[must_use] |
325 | 4 | pub fn write_error(&self) -> &[KeyValue] { |
326 | 4 | &self.write_error |
327 | 4 | } |
328 | | #[must_use] |
329 | 6 | pub fn delete_success(&self) -> &[KeyValue] { |
330 | 6 | &self.delete_success |
331 | 6 | } |
332 | | #[must_use] |
333 | 15 | pub fn delete_miss(&self) -> &[KeyValue] { |
334 | 15 | &self.delete_miss |
335 | 15 | } |
336 | | #[must_use] |
337 | 0 | pub fn delete_error(&self) -> &[KeyValue] { |
338 | 0 | &self.delete_error |
339 | 0 | } |
340 | | #[must_use] |
341 | 0 | pub fn evict_success(&self) -> &[KeyValue] { |
342 | 0 | &self.evict_success |
343 | 0 | } |
344 | | #[must_use] |
345 | 0 | pub fn evict_expired(&self) -> &[KeyValue] { |
346 | 0 | &self.evict_expired |
347 | 0 | } |
348 | | } |
349 | | |
350 | | /// Pre-allocated attribute combinations for efficient remote execution metrics collection. |
351 | | #[derive(Debug)] |
352 | | pub struct ExecutionMetricAttrs { |
353 | | // Stage transition attributes |
354 | | unknown: Vec<KeyValue>, |
355 | | cache_check: Vec<KeyValue>, |
356 | | queued: Vec<KeyValue>, |
357 | | executing: Vec<KeyValue>, |
358 | | completed_success: Vec<KeyValue>, |
359 | | completed_failure: Vec<KeyValue>, |
360 | | completed_cancelled: Vec<KeyValue>, |
361 | | completed_timeout: Vec<KeyValue>, |
362 | | completed_cache_hit: Vec<KeyValue>, |
363 | | } |
364 | | |
365 | | impl ExecutionMetricAttrs { |
366 | | /// Creates a new set of pre-computed attributes. |
367 | | /// |
368 | | /// The `base_attrs` are included in all attribute combinations (e.g., instance |
369 | | /// name, worker ID). |
370 | | #[must_use] |
371 | 1 | pub fn new(base_attrs: &[KeyValue]) -> Self { |
372 | 9 | let make_attrs1 = |stage: ExecutionStage, result: Option<ExecutionResult>| { |
373 | 9 | let mut attrs = base_attrs.to_vec(); |
374 | 9 | attrs.push(KeyValue::new(EXECUTION_STAGE, stage)); |
375 | 9 | if let Some(result5 ) = result { |
376 | 5 | attrs.push(KeyValue::new(EXECUTION_RESULT, result)); |
377 | 5 | }4 |
378 | 9 | attrs |
379 | 9 | }; |
380 | | |
381 | 1 | Self { |
382 | 1 | unknown: make_attrs(ExecutionStage::Unknown, None), |
383 | 1 | cache_check: make_attrs(ExecutionStage::CacheCheck, None), |
384 | 1 | queued: make_attrs(ExecutionStage::Queued, None), |
385 | 1 | executing: make_attrs(ExecutionStage::Executing, None), |
386 | 1 | completed_success: make_attrs( |
387 | 1 | ExecutionStage::Completed, |
388 | 1 | Some(ExecutionResult::Success), |
389 | 1 | ), |
390 | 1 | completed_failure: make_attrs( |
391 | 1 | ExecutionStage::Completed, |
392 | 1 | Some(ExecutionResult::Failure), |
393 | 1 | ), |
394 | 1 | completed_cancelled: make_attrs( |
395 | 1 | ExecutionStage::Completed, |
396 | 1 | Some(ExecutionResult::Cancelled), |
397 | 1 | ), |
398 | 1 | completed_timeout: make_attrs( |
399 | 1 | ExecutionStage::Completed, |
400 | 1 | Some(ExecutionResult::Timeout), |
401 | 1 | ), |
402 | 1 | completed_cache_hit: make_attrs( |
403 | 1 | ExecutionStage::Completed, |
404 | 1 | Some(ExecutionResult::CacheHit), |
405 | 1 | ), |
406 | 1 | } |
407 | 1 | } |
408 | | |
409 | | // Attribute accessors |
410 | | #[must_use] |
411 | 0 | pub fn unknown(&self) -> &[KeyValue] { |
412 | 0 | &self.unknown |
413 | 0 | } |
414 | | #[must_use] |
415 | 0 | pub fn cache_check(&self) -> &[KeyValue] { |
416 | 0 | &self.cache_check |
417 | 0 | } |
418 | | #[must_use] |
419 | 0 | pub fn queued(&self) -> &[KeyValue] { |
420 | 0 | &self.queued |
421 | 0 | } |
422 | | #[must_use] |
423 | 0 | pub fn executing(&self) -> &[KeyValue] { |
424 | 0 | &self.executing |
425 | 0 | } |
426 | | #[must_use] |
427 | 0 | pub fn completed_success(&self) -> &[KeyValue] { |
428 | 0 | &self.completed_success |
429 | 0 | } |
430 | | #[must_use] |
431 | 0 | pub fn completed_failure(&self) -> &[KeyValue] { |
432 | 0 | &self.completed_failure |
433 | 0 | } |
434 | | #[must_use] |
435 | 0 | pub fn completed_cancelled(&self) -> &[KeyValue] { |
436 | 0 | &self.completed_cancelled |
437 | 0 | } |
438 | | #[must_use] |
439 | 0 | pub fn completed_timeout(&self) -> &[KeyValue] { |
440 | 0 | &self.completed_timeout |
441 | 0 | } |
442 | | #[must_use] |
443 | 0 | pub fn completed_cache_hit(&self) -> &[KeyValue] { |
444 | 0 | &self.completed_cache_hit |
445 | 0 | } |
446 | | } |
447 | | |
448 | | /// Global cache metrics instruments. |
449 | 4 | pub static CACHE_METRICS: LazyLock<CacheMetrics> = LazyLock::new(|| { |
450 | 4 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
451 | | |
452 | 4 | CacheMetrics { |
453 | 4 | cache_operation_duration: meter |
454 | 4 | .f64_histogram("cache.operation.duration") |
455 | 4 | .with_description("Duration of cache operations in milliseconds") |
456 | 4 | .with_unit("ms") |
457 | 4 | // The range of these is quite large as a cache might be backed by |
458 | 4 | // memory, a filesystem, or network storage. The current values were |
459 | 4 | // determined empirically and might need adjustment. |
460 | 4 | .with_boundaries(vec![ |
461 | 4 | // Microsecond range |
462 | 4 | 0.001, // 1μs |
463 | 4 | 0.005, // 5μs |
464 | 4 | 0.01, // 10μs |
465 | 4 | 0.05, // 50μs |
466 | 4 | 0.1, // 100μs |
467 | 4 | // Sub-millisecond range |
468 | 4 | 0.2, // 200μs |
469 | 4 | 0.5, // 500μs |
470 | 4 | 1.0, // 1ms |
471 | 4 | // Low millisecond range |
472 | 4 | 2.0, // 2ms |
473 | 4 | 5.0, // 5ms |
474 | 4 | 10.0, // 10ms |
475 | 4 | 20.0, // 20ms |
476 | 4 | 50.0, // 50ms |
477 | 4 | 100.0, // 100ms |
478 | 4 | // Higher latency range |
479 | 4 | 200.0, // 200ms |
480 | 4 | 500.0, // 500ms |
481 | 4 | 1000.0, // 1 second |
482 | 4 | 2000.0, // 2 seconds |
483 | 4 | 5000.0, // 5 seconds |
484 | 4 | ]) |
485 | 4 | .build(), |
486 | 4 | |
487 | 4 | cache_operations: meter |
488 | 4 | .u64_counter("cache.operations") |
489 | 4 | .with_description("Total cache operations by type and result") |
490 | 4 | .build(), |
491 | 4 | |
492 | 4 | cache_io: meter |
493 | 4 | .u64_counter("cache.io") |
494 | 4 | .with_description("Total bytes processed by cache operations") |
495 | 4 | .with_unit("By") |
496 | 4 | .build(), |
497 | 4 | |
498 | 4 | cache_size: meter |
499 | 4 | .i64_up_down_counter("cache.size") |
500 | 4 | .with_description("Current total size of cached data") |
501 | 4 | .with_unit("By") |
502 | 4 | .build(), |
503 | 4 | |
504 | 4 | cache_entries: meter |
505 | 4 | .i64_up_down_counter("cache.entries") |
506 | 4 | .with_description("Current number of cached entries") |
507 | 4 | .with_unit("{entry}") |
508 | 4 | .build(), |
509 | 4 | |
510 | 4 | cache_entry_size: meter |
511 | 4 | .u64_histogram("cache.item.size") |
512 | 4 | .with_description("Size distribution of cached entries") |
513 | 4 | .with_unit("By") |
514 | 4 | .build(), |
515 | 4 | } |
516 | 4 | }); |
517 | | |
518 | | /// OpenTelemetry metrics instruments for cache monitoring. |
519 | | #[derive(Debug)] |
520 | | pub struct CacheMetrics { |
521 | | /// Histogram of cache operation durations in milliseconds |
522 | | pub cache_operation_duration: metrics::Histogram<f64>, |
523 | | /// Counter of cache operations by type and result |
524 | | pub cache_operations: metrics::Counter<u64>, |
525 | | /// Counter of bytes read/written during cache operations |
526 | | pub cache_io: metrics::Counter<u64>, |
527 | | /// Current total size of all cached data in bytes |
528 | | pub cache_size: metrics::UpDownCounter<i64>, |
529 | | /// Current number of entries in cache |
530 | | pub cache_entries: metrics::UpDownCounter<i64>, |
531 | | /// Histogram of individual cache entry sizes in bytes |
532 | | pub cache_entry_size: metrics::Histogram<u64>, |
533 | | } |
534 | | |
535 | | /// Records a net change in a cache's contents, for `cache.size` and |
536 | | /// `cache.entries`. Deltas are signed: entries leaving a cache, whether |
537 | | /// evicted, replaced or deleted, pass negative values. |
538 | 14 | pub fn record_cache_entries_delta(size_delta: i64, entries_delta: i64, attrs: &[KeyValue]) { |
539 | 14 | CACHE_METRICS.cache_size.add(size_delta, attrs); |
540 | 14 | CACHE_METRICS.cache_entries.add(entries_delta, attrs); |
541 | 14 | } |
542 | | |
543 | | /// Converts a size to the signed type the cache instruments take, clamping |
544 | | /// rather than wrapping on a value too large to represent. |
545 | 87.1k | pub fn saturating_i64(value: u64) -> i64 { |
546 | 87.1k | i64::try_from(value).unwrap_or(i64::MAX) |
547 | 87.1k | } |
548 | | |
549 | | /// Global remote execution metrics instruments. |
550 | 25 | pub static EXECUTION_METRICS: LazyLock<ExecutionMetrics> = LazyLock::new(|| { |
551 | 25 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
552 | | |
553 | 25 | ExecutionMetrics { |
554 | 25 | execute_stream_heartbeats: meter |
555 | 25 | .u64_counter("execution.stream.heartbeats") |
556 | 25 | .with_description( |
557 | 25 | "Operation states re-sent on idle Execute/WaitExecution streams by the stream heartbeat", |
558 | 25 | ) |
559 | 25 | .with_unit("{message}") |
560 | 25 | .build(), |
561 | 25 | |
562 | 25 | execute_stream_drops: meter |
563 | 25 | .u64_counter("execution.stream.drops") |
564 | 25 | .with_description( |
565 | 25 | "Execute/WaitExecution response streams dropped before the operation finished: client disconnects, plus streams torn down by a server shutdown or an intermediary", |
566 | 25 | ) |
567 | 25 | .with_unit("{stream}") |
568 | 25 | .build(), |
569 | 25 | |
570 | 25 | execution_stage_duration: meter |
571 | 25 | .f64_histogram("execution.stage.duration") |
572 | 25 | .with_description("Duration of each execution stage in seconds") |
573 | 25 | .with_unit("s") |
574 | 25 | .with_boundaries(vec![ |
575 | 25 | // Sub-second range |
576 | 25 | 0.001, // 1ms |
577 | 25 | 0.01, // 10ms |
578 | 25 | 0.1, // 100ms |
579 | 25 | 0.5, // 500ms |
580 | 25 | 1.0, // 1s |
581 | 25 | // Multi-second range |
582 | 25 | 2.0, // 2s |
583 | 25 | 5.0, // 5s |
584 | 25 | 10.0, // 10s |
585 | 25 | 30.0, // 30s |
586 | 25 | 60.0, // 1 minute |
587 | 25 | 120.0, // 2 minutes |
588 | 25 | 300.0, // 5 minutes |
589 | 25 | 600.0, // 10 minutes |
590 | 25 | 1800.0, // 30 minutes |
591 | 25 | 3600.0, // 1 hour |
592 | 25 | ]) |
593 | 25 | .build(), |
594 | 25 | |
595 | 25 | execution_total_duration: meter |
596 | 25 | .f64_histogram("execution.total.duration") |
597 | 25 | .with_description( |
598 | 25 | "Total duration of action execution from submission to completion in seconds", |
599 | 25 | ) |
600 | 25 | .with_unit("s") |
601 | 25 | .with_boundaries(vec![ |
602 | 25 | // Sub-second range |
603 | 25 | 0.01, // 10ms |
604 | 25 | 0.1, // 100ms |
605 | 25 | 0.5, // 500ms |
606 | 25 | 1.0, // 1s |
607 | 25 | // Multi-second range |
608 | 25 | 5.0, // 5s |
609 | 25 | 10.0, // 10s |
610 | 25 | 30.0, // 30s |
611 | 25 | 60.0, // 1 minute |
612 | 25 | 300.0, // 5 minutes |
613 | 25 | 600.0, // 10 minutes |
614 | 25 | 1800.0, // 30 minutes |
615 | 25 | 3600.0, // 1 hour |
616 | 25 | 7200.0, // 2 hours |
617 | 25 | ]) |
618 | 25 | .build(), |
619 | 25 | |
620 | 25 | execution_queue_time: meter |
621 | 25 | .f64_histogram("execution.queue.time") |
622 | 25 | .with_description("Time spent waiting in queue before execution in seconds") |
623 | 25 | .with_unit("s") |
624 | 25 | .with_boundaries(vec![ |
625 | 25 | 0.001, // 1ms |
626 | 25 | 0.01, // 10ms |
627 | 25 | 0.1, // 100ms |
628 | 25 | 0.5, // 500ms |
629 | 25 | 1.0, // 1s |
630 | 25 | 2.0, // 2s |
631 | 25 | 5.0, // 5s |
632 | 25 | 10.0, // 10s |
633 | 25 | 30.0, // 30s |
634 | 25 | 60.0, // 1 minute |
635 | 25 | 300.0, // 5 minutes |
636 | 25 | 600.0, // 10 minutes |
637 | 25 | ]) |
638 | 25 | .build(), |
639 | 25 | |
640 | 25 | execution_active_count: meter |
641 | 25 | .i64_up_down_counter("execution.active.count") |
642 | 25 | .with_description("Number of actions currently in each stage") |
643 | 25 | .with_unit("{action}") |
644 | 25 | .build(), |
645 | 25 | |
646 | 25 | execution_completed_count: meter |
647 | 25 | .u64_counter("execution.completed.count") |
648 | 25 | .with_description("Total number of completed executions by result") |
649 | 25 | .with_unit("{action}") |
650 | 25 | .build(), |
651 | 25 | |
652 | 25 | execution_stage_transitions: meter |
653 | 25 | .u64_counter("execution.stage.transitions") |
654 | 25 | .with_description("Number of stage transitions") |
655 | 25 | .with_unit("{transition}") |
656 | 25 | .build(), |
657 | 25 | |
658 | 25 | execution_cpu_time: meter |
659 | 25 | .f64_histogram("execution.cpu.time") |
660 | 25 | .with_description("CPU time consumed by an action in seconds") |
661 | 25 | .with_unit("s") |
662 | 25 | .with_boundaries(vec![ |
663 | 25 | 0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 300.0, 600.0, 1800.0, 3600.0, 7200.0, |
664 | 25 | ]) |
665 | 25 | .build(), |
666 | 25 | |
667 | 25 | execution_peak_memory: meter |
668 | 25 | .u64_histogram("execution.peak.memory") |
669 | 25 | .with_description("Peak resident memory observed while running an action in bytes") |
670 | 25 | .with_unit("By") |
671 | 25 | .with_boundaries(vec![ |
672 | 25 | 1_048_576.0, // 1MiB |
673 | 25 | 16_777_216.0, // 16MiB |
674 | 25 | 67_108_864.0, // 64MiB |
675 | 25 | 268_435_456.0, // 256MiB |
676 | 25 | 1_073_741_824.0, // 1GiB |
677 | 25 | 4_294_967_296.0, // 4GiB |
678 | 25 | 17_179_869_184.0, // 16GiB |
679 | 25 | 68_719_476_736.0, // 64GiB |
680 | 25 | ]) |
681 | 25 | .build(), |
682 | 25 | |
683 | 25 | execution_output_size: meter |
684 | 25 | .u64_histogram("execution.output.size") |
685 | 25 | .with_description("Size of execution outputs in bytes") |
686 | 25 | .with_unit("By") |
687 | 25 | .with_boundaries(vec![ |
688 | 25 | 1_024.0, // 1KB |
689 | 25 | 10_240.0, // 10KB |
690 | 25 | 102_400.0, // 100KB |
691 | 25 | 1_048_576.0, // 1MB |
692 | 25 | 10_485_760.0, // 10MB |
693 | 25 | 104_857_600.0, // 100MB |
694 | 25 | 1_073_741_824.0, // 1GB |
695 | 25 | 10_737_418_240.0, // 10GB |
696 | 25 | ]) |
697 | 25 | .build(), |
698 | 25 | |
699 | 25 | execution_retry_count: meter |
700 | 25 | .u64_counter("execution.retry.count") |
701 | 25 | .with_description("Number of execution retries") |
702 | 25 | .with_unit("{retry}") |
703 | 25 | .build(), |
704 | 25 | } |
705 | 25 | }); |
706 | | |
707 | | /// OpenTelemetry metrics instruments for remote execution monitoring. |
708 | | #[derive(Debug)] |
709 | | pub struct ExecutionMetrics { |
710 | | /// Operation states re-sent on idle `Execute`/`WaitExecution` streams |
711 | | /// by the stream heartbeat. |
712 | | pub execute_stream_heartbeats: metrics::Counter<u64>, |
713 | | /// `Execute`/`WaitExecution` response streams dropped before the |
714 | | /// operation finished, whichever side tore the stream down. |
715 | | pub execute_stream_drops: metrics::Counter<u64>, |
716 | | /// Histogram of stage durations in seconds |
717 | | pub execution_stage_duration: metrics::Histogram<f64>, |
718 | | /// Histogram of total execution durations in seconds |
719 | | pub execution_total_duration: metrics::Histogram<f64>, |
720 | | /// Histogram of queue wait times in seconds |
721 | | pub execution_queue_time: metrics::Histogram<f64>, |
722 | | /// Current number of actions in each stage |
723 | | pub execution_active_count: metrics::UpDownCounter<i64>, |
724 | | /// Total number of completed executions |
725 | | pub execution_completed_count: metrics::Counter<u64>, |
726 | | /// Number of stage transitions |
727 | | pub execution_stage_transitions: metrics::Counter<u64>, |
728 | | /// Histogram of output sizes in bytes |
729 | | pub execution_output_size: metrics::Histogram<u64>, |
730 | | /// Counter for execution retries |
731 | | pub execution_retry_count: metrics::Counter<u64>, |
732 | | /// Peak memory an action used, as sampled by the worker. |
733 | | pub execution_peak_memory: metrics::Histogram<u64>, |
734 | | /// CPU time an action used, as sampled by the worker. |
735 | | pub execution_cpu_time: metrics::Histogram<f64>, |
736 | | } |
737 | | |
738 | | /// Records the CPU time a worker observed for an action. |
739 | | /// |
740 | | /// Wall time is already covered by `execution.stage.duration`. This is the |
741 | | /// other half: an action pinning eight cores for a minute and one sleeping |
742 | | /// for a minute look identical by wall time and nothing alike here. |
743 | 1 | pub fn record_execution_cpu_time( |
744 | 1 | cpu_time_ms: u64, |
745 | 1 | instance_name: &str, |
746 | 1 | action_mnemonic: &str, |
747 | 1 | outcome: &str, |
748 | 1 | ) { |
749 | | #[expect(clippy::cast_precision_loss)] // Milliseconds; f64 is exact well past any real action. |
750 | 1 | let seconds = cpu_time_ms as f64 / 1000.0; |
751 | 1 | EXECUTION_METRICS.execution_cpu_time.record( |
752 | 1 | seconds, |
753 | 1 | &execution_sample_attributes(instance_name, action_mnemonic, outcome), |
754 | | ); |
755 | 1 | } |
756 | | |
757 | | /// Records the peak memory a worker observed while running an action. |
758 | | /// |
759 | | /// The worker samples this, so it is only present when sampling ran. #2614 |
760 | | /// listed `execution_memory_usage` as unimplementable because |
761 | | /// `ExecutionMetadata` carries no memory figure; `ActionResourceUsage` does, |
762 | | /// and this is that number. |
763 | 19 | pub fn record_execution_peak_memory( |
764 | 19 | peak_memory_kb: u64, |
765 | 19 | instance_name: &str, |
766 | 19 | action_mnemonic: &str, |
767 | 19 | outcome: &str, |
768 | 19 | ) { |
769 | 19 | EXECUTION_METRICS.execution_peak_memory.record( |
770 | 19 | peak_memory_sample(peak_memory_kb), |
771 | 19 | &execution_sample_attributes(instance_name, action_mnemonic, outcome), |
772 | | ); |
773 | 19 | } |
774 | | |
775 | | /// Attributes for a per-action resource sample. The mnemonic is attached only |
776 | | /// when known: an empty value on every sample from an unattributed client |
777 | | /// would read as a real mnemonic named "". |
778 | 20 | fn execution_sample_attributes( |
779 | 20 | instance_name: &str, |
780 | 20 | action_mnemonic: &str, |
781 | 20 | outcome: &str, |
782 | 20 | ) -> Vec<KeyValue> { |
783 | 20 | let mut attrs = vec![KeyValue::new(EXECUTION_INSTANCE, instance_name.to_string())]; |
784 | 20 | if !action_mnemonic.is_empty() { |
785 | 3 | attrs.push(KeyValue::new( |
786 | 3 | EXECUTION_ACTION_MNEMONIC, |
787 | 3 | action_mnemonic.to_string(), |
788 | 3 | )); |
789 | 17 | } |
790 | 20 | if !outcome.is_empty() { |
791 | 20 | attrs.push(KeyValue::new(EXECUTION_OUTCOME, outcome.to_string())); |
792 | 20 | }0 |
793 | 20 | attrs |
794 | 20 | } |
795 | | |
796 | | /// Ceiling on a recorded peak-memory sample. |
797 | | /// |
798 | | /// Well above any real action and far below the point where a histogram's |
799 | | /// `u64` sum can wrap, so it filters a garbage reading without touching a |
800 | | /// plausible one. Everything past the largest bucket already shares the same |
801 | | /// bucket, so this changes no bucket count. |
802 | | const PEAK_MEMORY_MAX_BYTES: u64 = 1 << 50; // 1 PiB. |
803 | | |
804 | | /// Converts a worker's peak-memory reading to bytes, bounded. |
805 | | /// |
806 | | /// The figure crosses a gRPC boundary from a worker, so a malfunctioning one |
807 | | /// could report something absurd. An unbounded value overflows the |
808 | | /// histogram's `u64` sum, which panics a debug build and takes the thread |
809 | | /// with it. Kept separate so it is directly testable: the overflow only |
810 | | /// happens once a meter provider is installed, which no unit test does. |
811 | | #[must_use] |
812 | 24 | pub fn peak_memory_sample(peak_memory_kb: u64) -> u64 { |
813 | 24 | peak_memory_kb |
814 | 24 | .saturating_mul(1024) |
815 | 24 | .min(PEAK_MEMORY_MAX_BYTES) |
816 | 24 | } |
817 | | |
818 | | /// Helper function to create attributes for execution metrics |
819 | | #[must_use] |
820 | 65.9k | pub fn make_execution_attributes( |
821 | 65.9k | instance_name: &str, |
822 | 65.9k | worker_id: Option<&str>, |
823 | 65.9k | priority: Option<i32>, |
824 | 65.9k | ) -> Vec<KeyValue> { |
825 | 65.9k | let mut attrs = vec![KeyValue::new(EXECUTION_INSTANCE, instance_name.to_string())]; |
826 | | |
827 | 65.9k | if let Some(worker_id31.2k ) = worker_id { |
828 | 31.2k | attrs.push(KeyValue::new(EXECUTION_WORKER_ID, worker_id.to_string())); |
829 | 34.6k | } |
830 | | |
831 | 65.9k | if let Some(priority65.9k ) = priority { |
832 | 65.9k | attrs.push(KeyValue::new(EXECUTION_PRIORITY, i64::from(priority))); |
833 | 65.9k | }1 |
834 | | |
835 | 65.9k | attrs |
836 | 65.9k | } |
837 | | |
838 | | /// Records the histogram metrics derivable from a completed action's result. |
839 | 10.1k | pub fn record_completed_execution_metrics( |
840 | 10.1k | action_result: &ActionResult, |
841 | 10.1k | instance_name: &str, |
842 | 10.1k | worker_id: Option<&str>, |
843 | 10.1k | priority: Option<i32>, |
844 | 10.1k | ) { |
845 | 10.1k | let m = &*EXECUTION_METRICS; |
846 | 10.1k | let md = &action_result.execution_metadata; |
847 | 10.1k | let base = make_execution_attributes(instance_name, worker_id, priority); |
848 | | |
849 | 10.1k | let record_secs = |
850 | 50.7k | |hist: &metrics::Histogram<f64>, start: SystemTime, end: SystemTime, attrs: &[KeyValue]| { |
851 | 50.7k | if start > SystemTime::UNIX_EPOCH |
852 | 50 | && let Ok(d) = end.duration_since(start) |
853 | 50 | { |
854 | 50 | hist.record(d.as_secs_f64(), attrs); |
855 | 50.6k | } |
856 | 50.7k | }; |
857 | | |
858 | | // Queue wait (queued -> worker picked it up) and end-to-end duration. |
859 | 10.1k | record_secs( |
860 | 10.1k | &m.execution_queue_time, |
861 | 10.1k | md.queued_timestamp, |
862 | 10.1k | md.worker_start_timestamp, |
863 | 10.1k | &base, |
864 | 10.1k | ); |
865 | 10.1k | record_secs( |
866 | 10.1k | &m.execution_total_duration, |
867 | 10.1k | md.queued_timestamp, |
868 | 10.1k | md.worker_completed_timestamp, |
869 | 10.1k | &base, |
870 | 10.1k | ); |
871 | | |
872 | | // Per-phase stage durations, labeled by phase on the stage attribute. |
873 | 30.4k | for (phase, start, end) in [ |
874 | 10.1k | ( |
875 | 10.1k | "input_fetch", |
876 | 10.1k | md.input_fetch_start_timestamp, |
877 | 10.1k | md.input_fetch_completed_timestamp, |
878 | 10.1k | ), |
879 | 10.1k | ( |
880 | 10.1k | "execution", |
881 | 10.1k | md.execution_start_timestamp, |
882 | 10.1k | md.execution_completed_timestamp, |
883 | 10.1k | ), |
884 | 10.1k | ( |
885 | 10.1k | "output_upload", |
886 | 10.1k | md.output_upload_start_timestamp, |
887 | 10.1k | md.output_upload_completed_timestamp, |
888 | 10.1k | ), |
889 | 30.4k | ] { |
890 | 30.4k | let mut attrs = base.clone(); |
891 | 30.4k | attrs.push(KeyValue::new(EXECUTION_STAGE, phase)); |
892 | 30.4k | record_secs(&m.execution_stage_duration, start, end, &attrs); |
893 | 30.4k | } |
894 | | |
895 | | // Total bytes produced: output files plus stdout/stderr. |
896 | 10.1k | m.execution_output_size |
897 | 10.1k | .record(execution_output_bytes(action_result), &base); |
898 | 10.1k | } |
899 | | |
900 | | /// Total output bytes produced by an action: the output file digests plus the |
901 | | /// stdout and stderr digests. |
902 | | #[must_use] |
903 | 10.1k | pub fn execution_output_bytes(action_result: &ActionResult) -> u64 { |
904 | 10.1k | action_result |
905 | 10.1k | .output_files |
906 | 10.1k | .iter() |
907 | 10.1k | .map(|f| f.digest4 .size_bytes4 ()) |
908 | 10.1k | .sum::<u64>() |
909 | 10.1k | + action_result.stdout_digest.size_bytes() |
910 | 10.1k | + action_result.stderr_digest.size_bytes() |
911 | 10.1k | } |
912 | | |
913 | | /// Input mount counters, emitted by workers rather than schedulers. |
914 | 3 | pub static INPUT_MOUNT_METRICS: LazyLock<InputMountMetrics> = LazyLock::new(|| { |
915 | 3 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
916 | 3 | InputMountMetrics { |
917 | 3 | cache_hits: meter |
918 | 3 | .u64_counter("worker.input_mount.cache_hits") |
919 | 3 | .with_description("Cached directories reused for input mount preparation") |
920 | 3 | .build(), |
921 | 3 | cache_misses: meter |
922 | 3 | .u64_counter("worker.input_mount.cache_misses") |
923 | 3 | .with_description("Input mount directories requiring cache construction") |
924 | 3 | .build(), |
925 | 3 | fallbacks: meter |
926 | 3 | .u64_counter("worker.input_mount.fallbacks") |
927 | 3 | .with_description( |
928 | 3 | "Actions using ordinary input preparation despite configured input mounts", |
929 | 3 | ) |
930 | 3 | .build(), |
931 | 3 | prepare_failures: meter |
932 | 3 | .u64_counter("worker.input_mount.prepare_failures") |
933 | 3 | .with_description("Failed input mount preparations retried without mounts") |
934 | 3 | .build(), |
935 | 3 | } |
936 | 3 | }); |
937 | | |
938 | | #[derive(Debug)] |
939 | | pub struct InputMountMetrics { |
940 | | /// Cached directories reused for input mount preparation. |
941 | | pub cache_hits: metrics::Counter<u64>, |
942 | | /// Input mount directories requiring cache construction. |
943 | | pub cache_misses: metrics::Counter<u64>, |
944 | | /// Actions using ordinary input preparation despite configured input mounts. |
945 | | pub fallbacks: metrics::Counter<u64>, |
946 | | /// Failed input mount preparations retried without mounts. |
947 | | pub prepare_failures: metrics::Counter<u64>, |
948 | | } |
949 | | |
950 | | /// Global worker fleet metrics instruments. |
951 | 24 | pub static WORKER_METRICS: LazyLock<WorkerMetrics> = LazyLock::new(|| { |
952 | 24 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
953 | | |
954 | 24 | WorkerMetrics { |
955 | 24 | worker_connected_count: meter |
956 | 24 | .i64_up_down_counter("worker.connected.count") |
957 | 24 | .with_description("Number of workers currently connected to this scheduler instance") |
958 | 24 | .with_unit("{worker}") |
959 | 24 | .build(), |
960 | 24 | |
961 | 24 | worker_connections: meter |
962 | 24 | .u64_counter("worker.connections") |
963 | 24 | .with_description("Total number of workers that have joined the pool") |
964 | 24 | .with_unit("{worker}") |
965 | 24 | .build(), |
966 | 24 | |
967 | 24 | worker_disconnections: meter |
968 | 24 | .u64_counter("worker.disconnections") |
969 | 24 | .with_description("Total number of workers that have left the pool, by reason") |
970 | 24 | .with_unit("{worker}") |
971 | 24 | .build(), |
972 | 24 | |
973 | 24 | input_relinks: meter |
974 | 24 | .u64_counter("worker.input.relinks") |
975 | 24 | .with_description("Input hardlinks made again because the fast store evicted their source while the tree was staged, by kind: plain, executable, or exhausted when the retries ran out") |
976 | 24 | .build(), |
977 | 24 | |
978 | 24 | prepare_retries: meter |
979 | 24 | .u64_counter("worker.prepare.retries") |
980 | 24 | .with_description("Input fetches tried again on the same worker after a transient store failure: retried per attempt made again, then one of recovered, exhausted when the download deadline ran out, failed when a later attempt failed for good, or abandoned when a kill or the download timeout ended the fetch mid-retry") |
981 | 24 | .build(), |
982 | 24 | |
983 | 24 | dispatch_requeues: meter |
984 | 24 | .u64_counter("scheduler.dispatch.requeues") |
985 | 24 | .with_description("Dispatches sent back to the queue untried, by reason: declined, channel_full, unacknowledged") |
986 | 24 | .build(), |
987 | 24 | |
988 | 24 | worker_keepalives: meter |
989 | 24 | .u64_counter("worker.keepalives") |
990 | 24 | .with_description("Total worker keepalives received") |
991 | 24 | .with_unit("{keepalive}") |
992 | 24 | .build(), |
993 | 24 | |
994 | 24 | worker_keepalive_gap: meter |
995 | 24 | .f64_histogram("worker.keepalive.gap") |
996 | 24 | .with_description( |
997 | 24 | "Seconds since the previous message from the same worker, at each message", |
998 | 24 | ) |
999 | 24 | .with_unit("s") |
1000 | 24 | .with_boundaries(vec![ |
1001 | 24 | 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 15.0, 20.0, 30.0, 60.0, |
1002 | 24 | ]) |
1003 | 24 | .build(), |
1004 | 24 | |
1005 | 24 | worker_state_count: meter |
1006 | 24 | .i64_up_down_counter("worker.state.count") |
1007 | 24 | .with_description("Number of connected workers in each non-default state") |
1008 | 24 | .with_unit("{worker}") |
1009 | 24 | .build(), |
1010 | 24 | |
1011 | 24 | worker_tripwire: meter |
1012 | 24 | .u64_counter("scheduler.worker.tripwire") |
1013 | 24 | .with_description("Workers the outcome tripwire acted on, by outcome: drained, or held when as many workers were already drained as it may drain") |
1014 | 24 | .with_unit("{worker}") |
1015 | 24 | .build(), |
1016 | 24 | } |
1017 | 24 | }); |
1018 | | |
1019 | | /// Worker fleet metrics. |
1020 | | /// |
1021 | | /// These are per-scheduler-instance. A worker only appears in the registry of |
1022 | | /// the instance holding its stream, so with several schedulers behind a load |
1023 | | /// balancer the fleet total is the sum across instances, not any one of them. |
1024 | | /// |
1025 | | /// Deliberately not attributed by worker id. Ids are per connection, so a |
1026 | | /// fleet that churns would grow the label set without bound, and the |
1027 | | /// autoscaling and fleet-health questions these answer are all aggregate. |
1028 | | #[derive(Debug)] |
1029 | | pub struct WorkerMetrics { |
1030 | | /// Workers currently connected. |
1031 | | pub worker_connected_count: metrics::UpDownCounter<i64>, |
1032 | | /// Workers that have joined, cumulative. |
1033 | | pub worker_connections: metrics::Counter<u64>, |
1034 | | /// Workers that have left, cumulative, by reason. |
1035 | | pub worker_disconnections: metrics::Counter<u64>, |
1036 | | /// Input hardlinks made again after their source was evicted mid-staging, by kind. |
1037 | | pub input_relinks: metrics::Counter<u64>, |
1038 | | /// Input fetches tried again on the worker after a transient store failure: `retried` per attempt made again, then one of `recovered`, `exhausted`, `failed` or `abandoned`. |
1039 | | pub prepare_retries: metrics::Counter<u64>, |
1040 | | /// Dispatches requeued untried, by reason. |
1041 | | pub dispatch_requeues: metrics::Counter<u64>, |
1042 | | /// Keepalives received, cumulative. |
1043 | | pub worker_keepalives: metrics::Counter<u64>, |
1044 | | /// Seconds between consecutive messages from a worker. |
1045 | | pub worker_keepalive_gap: metrics::Histogram<f64>, |
1046 | | /// Connected workers currently paused or draining. |
1047 | | pub worker_state_count: metrics::UpDownCounter<i64>, |
1048 | | /// Workers the outcome tripwire drained, or held back from draining. |
1049 | | pub worker_tripwire: metrics::Counter<u64>, |
1050 | | } |
1051 | | |
1052 | | /// Records a worker joining the pool. |
1053 | 2.24k | pub fn record_worker_connected() { |
1054 | 2.24k | WORKER_METRICS.worker_connected_count.add(1, &[]); |
1055 | 2.24k | WORKER_METRICS.worker_connections.add(1, &[]); |
1056 | 2.24k | } |
1057 | | |
1058 | | /// Records a worker leaving the pool. |
1059 | | /// |
1060 | | /// `was_draining` and `was_paused` unwind the state gauges, which would |
1061 | | /// otherwise keep counting a worker that is already gone. |
1062 | 934 | pub fn record_worker_disconnected( |
1063 | 934 | reason: WorkerDisconnectReason, |
1064 | 934 | was_draining: bool, |
1065 | 934 | was_paused: bool, |
1066 | 934 | ) { |
1067 | 934 | WORKER_METRICS.worker_connected_count.add(-1, &[]); |
1068 | 934 | WORKER_METRICS.worker_disconnections.add( |
1069 | | 1, |
1070 | 934 | &[KeyValue::new(WORKER_DISCONNECT_REASON, reason.as_str())], |
1071 | | ); |
1072 | 934 | if was_draining { |
1073 | 5 | record_worker_state("draining", false); |
1074 | 929 | } |
1075 | 934 | if was_paused { |
1076 | 1 | record_worker_state("paused", false); |
1077 | 933 | } |
1078 | 934 | } |
1079 | | |
1080 | | /// Records a worker entering or leaving `state`. |
1081 | 47 | pub fn record_worker_state(state: &'static str, entered: bool) { |
1082 | 47 | WORKER_METRICS.worker_state_count.add( |
1083 | 47 | if entered { 128 } else { -119 }, |
1084 | 47 | &[KeyValue::new(WORKER_STATE, state)], |
1085 | | ); |
1086 | 47 | } |
1087 | | |
1088 | | /// Records the outcome tripwire acting on a worker: `drained`, or `held` |
1089 | | /// when as many workers were already drained as it may drain at once. |
1090 | 8 | pub fn record_worker_tripwire(outcome: &'static str) { |
1091 | 8 | WORKER_METRICS |
1092 | 8 | .worker_tripwire |
1093 | 8 | .add(1, &[KeyValue::new("outcome", outcome)]); |
1094 | 8 | } |
1095 | | |
1096 | | /// Records a dispatch going back to the queue untried: the worker declined |
1097 | | /// it, its channel was full, or it never acknowledged it. |
1098 | 39 | pub fn record_dispatch_requeue(reason: &'static str) { |
1099 | 39 | WORKER_METRICS |
1100 | 39 | .dispatch_requeues |
1101 | 39 | .add(1, &[KeyValue::new("reason", reason)]); |
1102 | 39 | } |
1103 | | |
1104 | | /// Records an input hardlink made again because the fast store evicted its |
1105 | | /// source between resolving the tree and linking it, or `exhausted` when the |
1106 | | /// fetch retries ran out. A steady rate says the store runs at its ceiling. |
1107 | 3 | pub fn record_input_relink(kind: &'static str) { |
1108 | 3 | WORKER_METRICS |
1109 | 3 | .input_relinks |
1110 | 3 | .add(1, &[KeyValue::new(WORKER_RELINK_KIND, kind)]); |
1111 | 3 | } |
1112 | | |
1113 | | /// Records an input fetch the worker tried again after a transient store |
1114 | | /// failure: `retried` per attempt made again, then one terminal outcome, |
1115 | | /// `recovered`, `exhausted` (the download deadline ran out), `failed` (a |
1116 | | /// later attempt failed for good) or `abandoned` (a kill or the download |
1117 | | /// timeout ended it mid-retry). |
1118 | 32 | pub fn record_prepare_retry(outcome: &'static str) { |
1119 | 32 | WORKER_METRICS |
1120 | 32 | .prepare_retries |
1121 | 32 | .add(1, &[KeyValue::new(WORKER_PREPARE_OUTCOME, outcome)]); |
1122 | 32 | } |
1123 | | |
1124 | | /// Records a keepalive from a worker. |
1125 | 64.3k | pub fn record_worker_keepalive() { |
1126 | 64.3k | WORKER_METRICS.worker_keepalives.add(1, &[]); |
1127 | 64.3k | } |
1128 | | |
1129 | | /// Records the gap since the worker's previous message. The distribution |
1130 | | /// is what the eviction margin is judged against. |
1131 | 95.3k | pub fn record_worker_keepalive_gap(seconds: u64) { |
1132 | | #[expect( |
1133 | | clippy::cast_precision_loss, |
1134 | | reason = "seconds; exact well past any real gap" |
1135 | | )] |
1136 | 95.3k | WORKER_METRICS |
1137 | 95.3k | .worker_keepalive_gap |
1138 | 95.3k | .record(seconds as f64, &[]); |
1139 | 95.3k | } |
1140 | | |
1141 | | /// Global gRPC serving metrics. |
1142 | | /// |
1143 | | /// One duration histogram covers rate, errors and latency: the count gives |
1144 | | /// rate, the status attribute separates errors, and the buckets give latency. |
1145 | 3 | pub static RPC_METRICS: LazyLock<RpcMetrics> = LazyLock::new(|| { |
1146 | 3 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
1147 | | |
1148 | 3 | RpcMetrics { |
1149 | 3 | rpc_server_duration: meter |
1150 | 3 | .f64_histogram("rpc.server.duration") |
1151 | 3 | .with_description("Duration of inbound gRPC calls in seconds") |
1152 | 3 | .with_unit("s") |
1153 | 3 | .with_boundaries(vec![ |
1154 | 3 | 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, |
1155 | 3 | 300.0, |
1156 | 3 | ]) |
1157 | 3 | .build(), |
1158 | 3 | } |
1159 | 3 | }); |
1160 | | |
1161 | | /// gRPC serving metrics. |
1162 | | #[derive(Debug)] |
1163 | | pub struct RpcMetrics { |
1164 | | /// Duration of inbound gRPC calls, by service, method and status. |
1165 | | pub rpc_server_duration: metrics::Histogram<f64>, |
1166 | | } |
1167 | | |
1168 | | /// Records a served gRPC call. |
1169 | | /// |
1170 | | /// `full_path` is the HTTP path tonic routes on, `/package.Service/Method`. |
1171 | 8 | pub fn record_rpc_served(full_path: &str, grpc_status: i32, duration_secs: f64) { |
1172 | 8 | let (service, method) = split_grpc_path(full_path); |
1173 | 8 | RPC_METRICS.rpc_server_duration.record( |
1174 | 8 | duration_secs, |
1175 | 8 | &[ |
1176 | 8 | KeyValue::new(RPC_SERVICE, service.to_string()), |
1177 | 8 | KeyValue::new(RPC_METHOD, method.to_string()), |
1178 | 8 | KeyValue::new(RPC_STATUS_CODE, i64::from(grpc_status)), |
1179 | 8 | ], |
1180 | | ); |
1181 | 8 | } |
1182 | | |
1183 | | /// Splits `/package.Service/Method` into its service and method halves. |
1184 | | /// |
1185 | | /// Anything that does not look like a gRPC path is reported whole under an |
1186 | | /// `unknown` method, so an unexpected route still shows up rather than |
1187 | | /// silently vanishing. Both halves come from the route table, not user input, |
1188 | | /// so the label set stays bounded. |
1189 | | #[must_use] |
1190 | 12 | pub fn split_grpc_path(full_path: &str) -> (&str, &str) { |
1191 | 12 | let trimmed = full_path.strip_prefix('/').unwrap_or(full_path); |
1192 | 12 | trimmed |
1193 | 12 | .split_once('/') |
1194 | 12 | .map_or((trimmed, "unknown"), |(service, method)| (service7 , method7 )) |
1195 | 12 | } |
1196 | | |
1197 | | /// Global scheduler metrics. |
1198 | 21 | pub static SCHEDULER_METRICS: LazyLock<SchedulerOtlpMetrics> = LazyLock::new(|| { |
1199 | 21 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
1200 | | |
1201 | 21 | SchedulerOtlpMetrics { |
1202 | 21 | matching_duration: meter |
1203 | 21 | .f64_histogram("scheduler.matching.duration") |
1204 | 21 | .with_description("Duration of one queued-action to worker matching pass in seconds") |
1205 | 21 | .with_unit("s") |
1206 | 21 | .with_boundaries(vec![ |
1207 | 21 | 0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, |
1208 | 21 | ]) |
1209 | 21 | .build(), |
1210 | 21 | |
1211 | 21 | matching_passes: meter |
1212 | 21 | .u64_counter("scheduler.matching.passes") |
1213 | 21 | .with_description("Matching passes run, by result") |
1214 | 21 | .with_unit("{pass}") |
1215 | 21 | .build(), |
1216 | 21 | |
1217 | 21 | unsatisfiable_queued: meter |
1218 | 21 | .u64_gauge("scheduler.unsatisfiable.queued") |
1219 | 21 | .with_description( |
1220 | 21 | "Queued actions that no worker connected to this scheduler could run even when idle", |
1221 | 21 | ) |
1222 | 21 | .with_unit("{action}") |
1223 | 21 | .build(), |
1224 | 21 | |
1225 | 21 | unsatisfiable_failed: meter |
1226 | 21 | .u64_counter("scheduler.unsatisfiable.failed") |
1227 | 21 | .with_description("Queued actions failed because no worker could ever run them") |
1228 | 21 | .with_unit("{action}") |
1229 | 21 | .build(), |
1230 | 21 | |
1231 | 21 | queue_depth: meter |
1232 | 21 | .u64_gauge("scheduler.queue.depth") |
1233 | 21 | .with_description("Queued actions the last matching pass read") |
1234 | 21 | .with_unit("{action}") |
1235 | 21 | .build(), |
1236 | 21 | |
1237 | 21 | queue_age: meter |
1238 | 21 | .u64_gauge("scheduler.queue.age") |
1239 | 21 | .with_description( |
1240 | 21 | "Queued actions the last matching pass read, by how long ago their client submitted them", |
1241 | 21 | ) |
1242 | 21 | .with_unit("{action}") |
1243 | 21 | .build(), |
1244 | 21 | |
1245 | 21 | queue_oldest_age: meter |
1246 | 21 | .f64_gauge("scheduler.queue.oldest_age") |
1247 | 21 | .with_description( |
1248 | 21 | "Seconds since the client submitted the oldest action the last matching pass read", |
1249 | 21 | ) |
1250 | 21 | .with_unit("s") |
1251 | 21 | .build(), |
1252 | 21 | |
1253 | 21 | executing_age: meter |
1254 | 21 | .u64_gauge("scheduler.executing.age") |
1255 | 21 | .with_description( |
1256 | 21 | "Executing actions the last executing sweep read, by how long they have been executing", |
1257 | 21 | ) |
1258 | 21 | .with_unit("{action}") |
1259 | 21 | .build(), |
1260 | 21 | |
1261 | 21 | executing_oldest_age: meter |
1262 | 21 | .f64_gauge("scheduler.executing.oldest_age") |
1263 | 21 | .with_description( |
1264 | 21 | "Seconds the longest-executing action the last executing sweep read has been executing", |
1265 | 21 | ) |
1266 | 21 | .with_unit("s") |
1267 | 21 | .build(), |
1268 | 21 | |
1269 | 21 | queue_retired: meter |
1270 | 21 | .u64_counter("scheduler.queue.retired") |
1271 | 21 | .with_description("Queued actions retired because no client was listening any more") |
1272 | 21 | .with_unit("{action}") |
1273 | 21 | .build(), |
1274 | 21 | |
1275 | 21 | parked_dispatched: meter |
1276 | 21 | .u64_counter("scheduler.matching.parked_dispatched") |
1277 | 21 | .with_description( |
1278 | 21 | "Queued actions dispatched onto room that opened while the matching pass was past them, taken ahead of the newer actions the pass was reading", |
1279 | 21 | ) |
1280 | 21 | .with_unit("{action}") |
1281 | 21 | .build(), |
1282 | 21 | |
1283 | 21 | worker_holds: meter |
1284 | 21 | .u64_counter("scheduler.worker.holds") |
1285 | 21 | .with_description( |
1286 | 21 | "Workers held for a queued action that kept finding no room, by outcome: held when the hold began, placed when a worker took the action, released when the action or the worker left, declined when the held worker declined the action idle, expired when max_hold_s passed first", |
1287 | 21 | ) |
1288 | 21 | .with_unit("{hold}") |
1289 | 21 | .build(), |
1290 | 21 | |
1291 | 21 | held_workers: meter |
1292 | 21 | .u64_gauge("scheduler.worker.held") |
1293 | 21 | .with_description("Workers this scheduler holds for a queued action right now") |
1294 | 21 | .with_unit("{worker}") |
1295 | 21 | .build(), |
1296 | 21 | |
1297 | 21 | awaited_action_orphans: meter |
1298 | 21 | .u64_counter("scheduler.awaited_action.orphans") |
1299 | 21 | .with_description( |
1300 | 21 | "Operations the queue listed whose record was gone on read, by site; non-zero means the scheduler's store is losing records, usually to eviction", |
1301 | 21 | ) |
1302 | 21 | .with_unit("{operation}") |
1303 | 21 | .build(), |
1304 | 21 | |
1305 | 21 | sweep_failures: meter |
1306 | 21 | .u64_counter("scheduler.sweep.failures") |
1307 | 21 | .with_description("Abandoned-queue sweeps that ended in an error before finishing") |
1308 | 21 | .with_unit("{sweep}") |
1309 | 21 | .build(), |
1310 | 21 | |
1311 | 21 | hint_resolutions: meter |
1312 | 21 | .u64_counter("scheduler.hints.resolutions") |
1313 | 21 | .with_description( |
1314 | 21 | "Actions the historical resource scheduler sized, by where the numbers came from: a learned hint keyed by target, command digest or mnemonic, a seed, a file-backed hint, the cold start, or nothing", |
1315 | 21 | ) |
1316 | 21 | .with_unit("{action}") |
1317 | 21 | .build(), |
1318 | 21 | |
1319 | 21 | hints_entries: meter |
1320 | 21 | .u64_gauge("scheduler.hints.entries") |
1321 | 21 | .with_description("Keys the scheduler holds a measured resource profile for, by family") |
1322 | 21 | .with_unit("{key}") |
1323 | 21 | .build(), |
1324 | 21 | |
1325 | 21 | hints_capacity: meter |
1326 | 21 | .u64_gauge("scheduler.hints.capacity") |
1327 | 21 | .with_description("Keys a family can hold before it evicts the least recently seen") |
1328 | 21 | .with_unit("{key}") |
1329 | 21 | .build(), |
1330 | 21 | |
1331 | 21 | hints_evictions: meter |
1332 | 21 | .u64_counter("scheduler.hints.evictions") |
1333 | 21 | .with_description("Keys evicted from a full family; climbing means the budget is too small or a client puts something unbounded in target_id") |
1334 | 21 | .with_unit("{key}") |
1335 | 21 | .build(), |
1336 | 21 | |
1337 | 21 | hints_samples: meter |
1338 | 21 | .u64_counter("scheduler.hints.samples") |
1339 | 21 | .with_description("Sampled usage reports the sketch recorded, by family") |
1340 | 21 | .with_unit("{sample}") |
1341 | 21 | .build(), |
1342 | 21 | |
1343 | 21 | hints_snapshot_bytes: meter |
1344 | 21 | .u64_gauge("scheduler.hints.snapshot.bytes") |
1345 | 21 | .with_description("Size of the last sketch snapshot written") |
1346 | 21 | .with_unit("By") |
1347 | 21 | .build(), |
1348 | 21 | |
1349 | 21 | hints_snapshot_age: meter |
1350 | 21 | .u64_gauge("scheduler.hints.snapshot.age") |
1351 | 21 | .with_description("Seconds since the sketch was last snapshotted; zero before the first write") |
1352 | 21 | .with_unit("s") |
1353 | 21 | .build(), |
1354 | 21 | |
1355 | 21 | hints_snapshot_failures: meter |
1356 | 21 | .u64_counter("scheduler.hints.snapshot.failures") |
1357 | 21 | .with_description("Sketch snapshot writes that failed; the sketch in memory is unaffected") |
1358 | 21 | .with_unit("{write}") |
1359 | 21 | .build(), |
1360 | 21 | } |
1361 | 21 | }); |
1362 | | |
1363 | | /// Scheduler matching metrics. |
1364 | | /// |
1365 | | /// Named to avoid colliding with the existing `#[metric]` component struct of |
1366 | | /// the same idea in the scheduler crate. |
1367 | | #[derive(Debug)] |
1368 | | pub struct SchedulerOtlpMetrics { |
1369 | | /// How long a matching pass takes. The saturation signal: this climbing |
1370 | | /// while the queue is non-empty means matching is the bottleneck. |
1371 | | pub matching_duration: metrics::Histogram<f64>, |
1372 | | /// Matching passes, by result. |
1373 | | pub matching_passes: metrics::Counter<u64>, |
1374 | | /// Queued actions seen in the last matching pass that no worker |
1375 | | /// connected to this scheduler could run even when idle. Per scheduler |
1376 | | /// instance, judged against that instance's own workers. |
1377 | | pub unsatisfiable_queued: metrics::Gauge<u64>, |
1378 | | /// Queued actions failed because no worker could ever run them, by |
1379 | | /// the properties that could not be satisfied. |
1380 | | pub unsatisfiable_failed: metrics::Counter<u64>, |
1381 | | /// Queued actions the last matching pass read. |
1382 | | pub queue_depth: metrics::Gauge<u64>, |
1383 | | /// Queued actions the last matching pass read, by age bucket. |
1384 | | pub queue_age: metrics::Gauge<u64>, |
1385 | | /// Age of the oldest action the last matching pass read, in seconds. |
1386 | | pub queue_oldest_age: metrics::Gauge<f64>, |
1387 | | /// Executing actions the last executing sweep read, by age bucket. |
1388 | | pub executing_age: metrics::Gauge<u64>, |
1389 | | /// How long the longest-executing action the last sweep read has been |
1390 | | /// executing, in seconds. |
1391 | | pub executing_oldest_age: metrics::Gauge<f64>, |
1392 | | /// Queued actions retired for having no client. |
1393 | | pub queue_retired: metrics::Counter<u64>, |
1394 | | /// Actions a pass parked for want of room and dispatched when room |
1395 | | /// opened before the pass ended, ahead of the newer actions it was reading. |
1396 | | pub parked_dispatched: metrics::Counter<u64>, |
1397 | | /// Workers held for a queued action that kept finding no room, by what |
1398 | | /// began or ended the hold. |
1399 | | pub worker_holds: metrics::Counter<u64>, |
1400 | | /// Workers held for such an action right now. |
1401 | | pub held_workers: metrics::Gauge<u64>, |
1402 | | /// Listed operations whose record was gone on read, by site. |
1403 | | pub awaited_action_orphans: metrics::Counter<u64>, |
1404 | | /// Abandoned-queue sweeps that failed before finishing. |
1405 | | pub sweep_failures: metrics::Counter<u64>, |
1406 | | /// Actions sized by the historical resource scheduler, by source. |
1407 | | pub hint_resolutions: metrics::Counter<u64>, |
1408 | | /// Keys the scheduler's resource sketch holds, by family. |
1409 | | pub hints_entries: metrics::Gauge<u64>, |
1410 | | /// Keys a sketch family can hold, by family. |
1411 | | pub hints_capacity: metrics::Gauge<u64>, |
1412 | | /// Keys evicted from a full sketch family, by family. |
1413 | | pub hints_evictions: metrics::Counter<u64>, |
1414 | | /// Usage reports the sketch recorded, by family. |
1415 | | pub hints_samples: metrics::Counter<u64>, |
1416 | | /// Bytes of the last sketch snapshot. |
1417 | | pub hints_snapshot_bytes: metrics::Gauge<u64>, |
1418 | | /// Seconds since the last sketch snapshot. |
1419 | | pub hints_snapshot_age: metrics::Gauge<u64>, |
1420 | | /// Sketch snapshot writes that failed. |
1421 | | pub hints_snapshot_failures: metrics::Counter<u64>, |
1422 | | } |
1423 | | |
1424 | | /// Records the sketch's size for one family: what it holds, what it can |
1425 | | /// hold, and the running totals of evictions and samples since start |
1426 | | /// (counters take the delta from the last report). |
1427 | 0 | pub fn record_hints_family( |
1428 | 0 | family: &'static str, |
1429 | 0 | entries: u64, |
1430 | 0 | capacity: u64, |
1431 | 0 | evictions_delta: u64, |
1432 | 0 | samples_delta: u64, |
1433 | 0 | ) { |
1434 | 0 | let attrs = [KeyValue::new("family", family)]; |
1435 | 0 | SCHEDULER_METRICS.hints_entries.record(entries, &attrs); |
1436 | 0 | SCHEDULER_METRICS.hints_capacity.record(capacity, &attrs); |
1437 | 0 | if evictions_delta > 0 { |
1438 | 0 | SCHEDULER_METRICS |
1439 | 0 | .hints_evictions |
1440 | 0 | .add(evictions_delta, &attrs); |
1441 | 0 | } |
1442 | 0 | if samples_delta > 0 { |
1443 | 0 | SCHEDULER_METRICS.hints_samples.add(samples_delta, &attrs); |
1444 | 0 | } |
1445 | 0 | } |
1446 | | |
1447 | | /// Records a sketch snapshot write. |
1448 | 0 | pub fn record_hints_snapshot(bytes: u64, age_s: u64) { |
1449 | 0 | SCHEDULER_METRICS.hints_snapshot_bytes.record(bytes, &[]); |
1450 | 0 | SCHEDULER_METRICS.hints_snapshot_age.record(age_s, &[]); |
1451 | 0 | } |
1452 | | |
1453 | | /// Records a sketch snapshot write that failed. |
1454 | 0 | pub fn record_hints_snapshot_failure() { |
1455 | 0 | SCHEDULER_METRICS.hints_snapshot_failures.add(1, &[]); |
1456 | 0 | } |
1457 | | |
1458 | | /// Records a completed matching pass. |
1459 | 36.3k | pub fn record_matching_pass(duration_secs: f64, succeeded: bool) { |
1460 | 36.3k | SCHEDULER_METRICS |
1461 | 36.3k | .matching_duration |
1462 | 36.3k | .record(duration_secs, &[]); |
1463 | 36.3k | SCHEDULER_METRICS.matching_passes.add( |
1464 | | 1, |
1465 | 36.3k | &[KeyValue::new( |
1466 | | SCHEDULER_MATCH_RESULT, |
1467 | 36.3k | if succeeded { "ok"36.3k } else { "error"9 }, |
1468 | | )], |
1469 | | ); |
1470 | 36.3k | } |
1471 | | |
1472 | | /// Records how many unsatisfiable queued actions a matching pass saw. |
1473 | 36.3k | pub fn record_unsatisfiable_queued(count: u64) { |
1474 | 36.3k | SCHEDULER_METRICS.unsatisfiable_queued.record(count, &[]); |
1475 | 36.3k | } |
1476 | | |
1477 | | /// Records how many queued actions a matching pass read. |
1478 | 36.3k | pub fn record_queue_depth(count: u64) { |
1479 | 36.3k | SCHEDULER_METRICS.queue_depth.record(count, &[]); |
1480 | 36.3k | } |
1481 | | |
1482 | | /// Upper bounds of the action age buckets; anything older falls in the last, |
1483 | | /// open bucket. Matches how an operator asks about a backlog: how much has |
1484 | | /// waited over a minute, five, fifteen, thirty, an hour. |
1485 | | const AGE_BUCKET_BOUNDS: [core::time::Duration; 5] = [ |
1486 | | core::time::Duration::from_mins(1), |
1487 | | core::time::Duration::from_mins(5), |
1488 | | core::time::Duration::from_mins(15), |
1489 | | core::time::Duration::from_mins(30), |
1490 | | core::time::Duration::from_hours(1), |
1491 | | ]; |
1492 | | |
1493 | | /// The `age` attribute values, one per bucket, in bound order. |
1494 | | pub const AGE_BUCKET_LABELS: [&str; 6] = |
1495 | | ["lt_1m", "1m_5m", "5m_15m", "15m_30m", "30m_60m", "ge_60m"]; |
1496 | | |
1497 | | /// Counts actions into age buckets as a pass reads them, for one gauge |
1498 | | /// sample per bucket at the end of the pass. |
1499 | | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] |
1500 | | pub struct AgeBuckets { |
1501 | | counts: [u64; AGE_BUCKET_LABELS.len()], |
1502 | | oldest: core::time::Duration, |
1503 | | } |
1504 | | |
1505 | | impl AgeBuckets { |
1506 | | /// Counts one action of the given age. |
1507 | 6.66M | pub fn observe(&mut self, age: core::time::Duration) { |
1508 | 6.66M | let bucket = AGE_BUCKET_BOUNDS |
1509 | 6.66M | .iter() |
1510 | 6.67M | .position6.66M (|bound| age < *bound) |
1511 | 6.66M | .unwrap_or(AGE_BUCKET_BOUNDS.len()); |
1512 | 6.66M | self.counts[bucket] += 1; |
1513 | 6.66M | self.oldest = self.oldest.max(age); |
1514 | 6.66M | } |
1515 | | |
1516 | | /// Counts one action from the time its age is measured from. A time |
1517 | | /// after `now` (clock skew between replicas) counts as no age at all. |
1518 | 6.66M | pub fn observe_since(&mut self, since: SystemTime, now: SystemTime) { |
1519 | 6.66M | self.observe(now.duration_since(since).unwrap_or_default()); |
1520 | 6.66M | } |
1521 | | |
1522 | | /// The count in each bucket, in [`AGE_BUCKET_LABELS`] order. |
1523 | | #[must_use] |
1524 | 36.3k | pub const fn counts(&self) -> [u64; AGE_BUCKET_LABELS.len()] { |
1525 | 36.3k | self.counts |
1526 | 36.3k | } |
1527 | | |
1528 | | /// The oldest age counted, zero when nothing was. |
1529 | | #[must_use] |
1530 | 36.3k | pub const fn oldest(&self) -> core::time::Duration { |
1531 | 36.3k | self.oldest |
1532 | 36.3k | } |
1533 | | } |
1534 | | |
1535 | | /// Records the ages of the queued actions a matching pass read. Every bucket |
1536 | | /// is written, empty ones as zero, so a gauge never holds a count from a |
1537 | | /// backlog that has since drained. |
1538 | 36.3k | pub fn record_queue_ages(ages: &AgeBuckets) { |
1539 | 36.3k | record_age_buckets( |
1540 | 36.3k | &SCHEDULER_METRICS.queue_age, |
1541 | 36.3k | &SCHEDULER_METRICS.queue_oldest_age, |
1542 | 36.3k | ages, |
1543 | | ); |
1544 | 36.3k | } |
1545 | | |
1546 | | /// Records the ages of the executing actions an executing sweep read, the |
1547 | | /// same way as [`record_queue_ages`]. |
1548 | 1 | pub fn record_executing_ages(ages: &AgeBuckets) { |
1549 | 1 | record_age_buckets( |
1550 | 1 | &SCHEDULER_METRICS.executing_age, |
1551 | 1 | &SCHEDULER_METRICS.executing_oldest_age, |
1552 | 1 | ages, |
1553 | | ); |
1554 | 1 | } |
1555 | | |
1556 | 36.3k | fn record_age_buckets( |
1557 | 36.3k | by_bucket: &metrics::Gauge<u64>, |
1558 | 36.3k | oldest: &metrics::Gauge<f64>, |
1559 | 36.3k | ages: &AgeBuckets, |
1560 | 36.3k | ) { |
1561 | 217k | for (label, count) in AGE_BUCKET_LABELS36.3k .iter36.3k ().zip36.3k (ages36.3k .counts36.3k ()) { |
1562 | 217k | by_bucket.record(count, &[KeyValue::new(SCHEDULER_ACTION_AGE, *label)]); |
1563 | 217k | } |
1564 | 36.3k | oldest.record(ages.oldest().as_secs_f64(), &[]); |
1565 | 36.3k | } |
1566 | | |
1567 | | /// Records queued actions retired by the abandoned sweep. |
1568 | 116 | pub fn record_queue_retired(count: u64) { |
1569 | 116 | if count > 0 { |
1570 | 5 | SCHEDULER_METRICS.queue_retired.add(count, &[]); |
1571 | 111 | } |
1572 | 116 | } |
1573 | | |
1574 | | /// Records parked actions a pass sent to a worker onto room that opened |
1575 | | /// mid-pass: confirmed sends, not attempts. A dispatch that failed or lost |
1576 | | /// the assignment race to another scheduler is not counted. |
1577 | 36.3k | pub fn record_parked_dispatched(count: u64) { |
1578 | 36.3k | if count > 0 { |
1579 | 3 | SCHEDULER_METRICS.parked_dispatched.add(count, &[]); |
1580 | 36.3k | } |
1581 | 36.3k | } |
1582 | | |
1583 | | /// Records a worker hold beginning (`held`) or ending: `placed` when a |
1584 | | /// worker took the action, `released` when the action left the queue or |
1585 | | /// the worker left or drained, `declined` when the held worker declined |
1586 | | /// the action idle and another is held instead, `expired` when `max_hold_s` |
1587 | | /// passed first. |
1588 | 27 | pub fn record_worker_hold(outcome: &'static str) { |
1589 | 27 | SCHEDULER_METRICS |
1590 | 27 | .worker_holds |
1591 | 27 | .add(1, &[KeyValue::new(SCHEDULER_HOLD_OUTCOME, outcome)]); |
1592 | 27 | } |
1593 | | |
1594 | | /// Records how many workers are held right now. |
1595 | 27 | pub fn record_held_workers(count: usize) { |
1596 | 27 | SCHEDULER_METRICS |
1597 | 27 | .held_workers |
1598 | 27 | .record(u64::try_from(count).unwrap_or(u64::MAX), &[]); |
1599 | 27 | } |
1600 | | |
1601 | | /// Records an operation state re-sent on an idle `Execute`/`WaitExecution` |
1602 | | /// stream by the stream heartbeat. |
1603 | 2 | pub fn record_execute_stream_heartbeat() { |
1604 | 2 | EXECUTION_METRICS.execute_stream_heartbeats.add(1, &[]); |
1605 | 2 | } |
1606 | | |
1607 | | /// Records an `Execute`/`WaitExecution` response stream dropped before its |
1608 | | /// operation finished, whichever side tore the stream down. |
1609 | 6.43k | pub fn record_execute_stream_drop() { |
1610 | 6.43k | EXECUTION_METRICS.execute_stream_drops.add(1, &[]); |
1611 | 6.43k | } |
1612 | | |
1613 | | /// Records an operation the queue listed whose record was gone when read; |
1614 | | /// `site` is where it was met (`matching`, `sweep`). |
1615 | 8 | pub fn record_awaited_action_orphan(site: &'static str) { |
1616 | 8 | SCHEDULER_METRICS |
1617 | 8 | .awaited_action_orphans |
1618 | 8 | .add(1, &[KeyValue::new("site", site)]); |
1619 | 8 | } |
1620 | | |
1621 | | /// Records an abandoned-queue sweep that failed before finishing. |
1622 | 0 | pub fn record_sweep_failure() { |
1623 | 0 | SCHEDULER_METRICS.sweep_failures.add(1, &[]); |
1624 | 0 | } |
1625 | | |
1626 | | /// Records where an action's reservation came from: `target`, |
1627 | | /// `action_digest`, `command_digest`, `mnemonic`, `cold_start` or `none`. |
1628 | | /// The cold-start share of a run is `cold_start` over the total. |
1629 | 75 | pub fn record_hint_resolution(source: &'static str) { |
1630 | 75 | SCHEDULER_METRICS |
1631 | 75 | .hint_resolutions |
1632 | 75 | .add(1, &[KeyValue::new("source", source)]); |
1633 | 75 | } |
1634 | | |
1635 | | /// Records a queued action failed for being unsatisfiable. `properties` |
1636 | | /// names the ones no worker could satisfy, sorted and comma separated, so |
1637 | | /// the label set is bounded by the declared platform properties. |
1638 | 29 | pub fn record_unsatisfiable_failed(properties: String) { |
1639 | 29 | SCHEDULER_METRICS.unsatisfiable_failed.add( |
1640 | | 1, |
1641 | 29 | &[KeyValue::new( |
1642 | 29 | SCHEDULER_UNSATISFIABLE_PROPERTIES, |
1643 | 29 | properties, |
1644 | 29 | )], |
1645 | | ); |
1646 | 29 | } |
1647 | | |
1648 | | /// Global object-store metrics. |
1649 | 2 | pub static STORE_METRICS: LazyLock<StoreMetrics> = LazyLock::new(|| { |
1650 | 2 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
1651 | | |
1652 | 2 | StoreMetrics { |
1653 | 2 | object_throttles: meter |
1654 | 2 | .u64_counter("store.object.throttles") |
1655 | 2 | .with_description("Object store responses that asked the client to slow down (HTTP 503 ServerBusy or 429), by provider and operation") |
1656 | 2 | .with_unit("{response}") |
1657 | 2 | .build(), |
1658 | 2 | |
1659 | 2 | object_read_limit: meter |
1660 | 2 | .u64_gauge("store.object.read_limit") |
1661 | 2 | .with_description("Reads this process keeps in flight against an object store at most, as the adaptive permit pool stands: halved on a throttled response, raised by one after a run of successes") |
1662 | 2 | .with_unit("{request}") |
1663 | 2 | .build(), |
1664 | 2 | |
1665 | 2 | object_read_waits: meter |
1666 | 2 | .u64_counter("store.object.read_waits") |
1667 | 2 | .with_description("Object store reads that found every read permit taken and waited for one, by provider") |
1668 | 2 | .with_unit("{request}") |
1669 | 2 | .build(), |
1670 | 2 | |
1671 | 2 | object_request_waits: meter |
1672 | 2 | .u64_counter("store.object.request_waits") |
1673 | 2 | .with_description("Object store requests of any kind that found every request permit taken (max_concurrent_requests) and waited for one, by provider") |
1674 | 2 | .with_unit("{request}") |
1675 | 2 | .build(), |
1676 | 2 | } |
1677 | 2 | }); |
1678 | | |
1679 | | /// Object-store metrics shared by the cloud backends. |
1680 | | #[derive(Debug)] |
1681 | | pub struct StoreMetrics { |
1682 | | /// Throttled responses, by provider and operation. |
1683 | | pub object_throttles: metrics::Counter<u64>, |
1684 | | /// The adaptive read limit as it stands, by provider. |
1685 | | pub object_read_limit: metrics::Gauge<u64>, |
1686 | | /// Reads that waited for a read permit, by provider. |
1687 | | pub object_read_waits: metrics::Counter<u64>, |
1688 | | /// Requests that waited for a request permit, by provider. |
1689 | | pub object_request_waits: metrics::Counter<u64>, |
1690 | | } |
1691 | | |
1692 | | /// Records the read limit an object store's permit pool settled on. |
1693 | 35 | pub fn record_object_store_read_limit(provider: &'static str, limit: usize) { |
1694 | 35 | STORE_METRICS.object_read_limit.record( |
1695 | 35 | u64::try_from(limit).unwrap_or(u64::MAX), |
1696 | 35 | &[KeyValue::new(STORE_OBJECT_PROVIDER, provider)], |
1697 | | ); |
1698 | 35 | } |
1699 | | |
1700 | | /// Records a read that found no permit free and waited for one. |
1701 | 7 | pub fn record_object_store_read_wait(provider: &'static str) { |
1702 | 7 | STORE_METRICS |
1703 | 7 | .object_read_waits |
1704 | 7 | .add(1, &[KeyValue::new(STORE_OBJECT_PROVIDER, provider)]); |
1705 | 7 | } |
1706 | | |
1707 | | /// Records a request that found no request permit free and waited for one. |
1708 | 77 | pub fn record_object_store_request_wait(provider: &'static str) { |
1709 | 77 | STORE_METRICS |
1710 | 77 | .object_request_waits |
1711 | 77 | .add(1, &[KeyValue::new(STORE_OBJECT_PROVIDER, provider)]); |
1712 | 77 | } |
1713 | | |
1714 | | /// Records a response from an object store that asked the client to slow down. |
1715 | 17 | pub fn record_object_store_throttle(provider: &'static str, op: &'static str) { |
1716 | 17 | STORE_METRICS.object_throttles.add( |
1717 | | 1, |
1718 | 17 | &[ |
1719 | 17 | KeyValue::new(STORE_OBJECT_PROVIDER, provider), |
1720 | 17 | KeyValue::new(STORE_OBJECT_OPERATION, op), |
1721 | 17 | ], |
1722 | | ); |
1723 | 17 | } |
1724 | | |
1725 | | /// Global tiered-store metrics. |
1726 | 10 | pub static STORE_TIER_METRICS: LazyLock<StoreTierMetrics> = LazyLock::new(|| { |
1727 | 10 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
1728 | | |
1729 | 10 | StoreTierMetrics { |
1730 | 10 | tier_operations: meter |
1731 | 10 | .u64_counter("store.tier.operations") |
1732 | 10 | .with_description("Reads served by each tier of a tiered store, by result") |
1733 | 10 | .with_unit("{operation}") |
1734 | 10 | .build(), |
1735 | 10 | |
1736 | 10 | tier_io: meter |
1737 | 10 | .u64_counter("store.tier.io") |
1738 | 10 | .with_description("Bytes moved through each tier of a tiered store") |
1739 | 10 | .with_unit("By") |
1740 | 10 | .build(), |
1741 | 10 | } |
1742 | 10 | }); |
1743 | | |
1744 | | /// Tiered-store metrics. The hit ratio is `tier_operations{result="hit"}` over |
1745 | | /// the sum, which is the number worth alerting on for a fast/slow store. |
1746 | | #[derive(Debug)] |
1747 | | pub struct StoreTierMetrics { |
1748 | | /// Reads served per tier, by result. |
1749 | | pub tier_operations: metrics::Counter<u64>, |
1750 | | /// Bytes moved per tier and direction. |
1751 | | pub tier_io: metrics::Counter<u64>, |
1752 | | } |
1753 | | |
1754 | | /// Records a read served, or not served, by a tier. |
1755 | 2.78k | pub fn record_store_tier_read(tier: &'static str, result: &'static str) { |
1756 | 2.78k | STORE_TIER_METRICS.tier_operations.add( |
1757 | | 1, |
1758 | 2.78k | &[ |
1759 | 2.78k | KeyValue::new(STORE_TIER, tier), |
1760 | 2.78k | KeyValue::new(STORE_RESULT, result), |
1761 | 2.78k | ], |
1762 | | ); |
1763 | 2.78k | } |
1764 | | |
1765 | | /// Records bytes moved through a tier. |
1766 | 2.24k | pub fn record_store_tier_io(tier: &'static str, direction: &'static str, bytes: u64) { |
1767 | 2.24k | STORE_TIER_METRICS.tier_io.add( |
1768 | 2.24k | bytes, |
1769 | 2.24k | &[ |
1770 | 2.24k | KeyValue::new(STORE_TIER, tier), |
1771 | 2.24k | KeyValue::new(STORE_DIRECTION, direction), |
1772 | 2.24k | ], |
1773 | | ); |
1774 | 2.24k | } |
1775 | | |
1776 | | /// Global health-check metrics. |
1777 | 3 | pub static HEALTH_METRICS: LazyLock<HealthMetrics> = LazyLock::new(|| { |
1778 | 3 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
1779 | | |
1780 | 3 | HealthMetrics { |
1781 | 3 | health_checks: meter |
1782 | 3 | .u64_counter("health.checks") |
1783 | 3 | .with_description("Health check results, by component and status") |
1784 | 3 | .with_unit("{check}") |
1785 | 3 | .build(), |
1786 | 3 | } |
1787 | 3 | }); |
1788 | | |
1789 | | /// Health-check metrics. |
1790 | | #[derive(Debug)] |
1791 | | pub struct HealthMetrics { |
1792 | | /// Health check results, by namespace and status. |
1793 | | pub health_checks: metrics::Counter<u64>, |
1794 | | } |
1795 | | |
1796 | | /// Records one component's health check result. |
1797 | 20 | pub fn record_health_check(namespace: &str, status: &'static str) { |
1798 | 20 | HEALTH_METRICS.health_checks.add( |
1799 | | 1, |
1800 | 20 | &[ |
1801 | 20 | KeyValue::new(HEALTH_NAMESPACE, namespace.to_string()), |
1802 | 20 | KeyValue::new(HEALTH_STATUS, status), |
1803 | 20 | ], |
1804 | | ); |
1805 | 20 | } |
1806 | | |
1807 | | /// Global connection-pool metrics. |
1808 | 19 | pub static CONNECTION_METRICS: LazyLock<ConnectionMetrics> = LazyLock::new(|| { |
1809 | 19 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
1810 | | |
1811 | 19 | ConnectionMetrics { |
1812 | 19 | pool_available: meter |
1813 | 19 | .u64_histogram("connection.pool.available") |
1814 | 19 | .with_description("Free slots in a connection pool, sampled when one is taken") |
1815 | 19 | .with_unit("{connection}") |
1816 | 19 | .with_boundaries(vec![ |
1817 | 19 | 0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, |
1818 | 19 | ]) |
1819 | 19 | .build(), |
1820 | 19 | |
1821 | 19 | pool_acquisitions: meter |
1822 | 19 | .u64_counter("connection.pool.acquisitions") |
1823 | 19 | .with_description("Connection acquisitions, by pool and whether one was free") |
1824 | 19 | .with_unit("{acquisition}") |
1825 | 19 | .build(), |
1826 | 19 | |
1827 | 19 | reconnects: meter |
1828 | 19 | .u64_counter("connection.reconnects") |
1829 | 19 | .with_description("Reconnects performed, by pool") |
1830 | 19 | .with_unit("{reconnect}") |
1831 | 19 | .build(), |
1832 | 19 | } |
1833 | 19 | }); |
1834 | | |
1835 | | /// Connection-pool metrics. |
1836 | | /// |
1837 | | /// `pool_available` is the saturation signal: a pool that keeps reporting zero |
1838 | | /// free slots is the bottleneck, whatever the latency elsewhere says. Counting |
1839 | | /// queued acquisitions gives the same answer from the other direction. |
1840 | | #[derive(Debug)] |
1841 | | pub struct ConnectionMetrics { |
1842 | | /// Free slots at the moment a connection was taken. |
1843 | | pub pool_available: metrics::Histogram<u64>, |
1844 | | /// Acquisitions, by pool and result. |
1845 | | pub pool_acquisitions: metrics::Counter<u64>, |
1846 | | /// Reconnects, by pool. |
1847 | | pub reconnects: metrics::Counter<u64>, |
1848 | | } |
1849 | | |
1850 | | /// Ceiling on a recorded free-slot count. |
1851 | | /// |
1852 | | /// An unbounded pool is a semaphore holding `Semaphore::MAX_PERMITS`, around |
1853 | | /// 2.3e18. Recording that overflows the histogram's `u64` sum after a handful |
1854 | | /// of samples, which panics a debug build. Callers pass `None` for an |
1855 | | /// unbounded pool, and this clamp is the backstop if one ever does not: it |
1856 | | /// sits above the largest bucket boundary, so bucket counts are unchanged. |
1857 | | const POOL_AVAILABLE_MAX: u64 = 1024; |
1858 | | |
1859 | | /// Records a connection being taken from `pool`. |
1860 | | /// |
1861 | | /// `available` is the free-slot count, or `None` when the pool is unbounded |
1862 | | /// and the figure would be meaningless. `queued` means nothing was free and |
1863 | | /// the caller had to wait, which is the case worth alerting on. |
1864 | | /// Clamps a free-slot count to something a histogram can accumulate. |
1865 | | /// |
1866 | | /// Kept separate so it is directly testable: the failure this guards against |
1867 | | /// only appears once a meter provider is installed, so a test that merely |
1868 | | /// calls `record_connection_acquired` cannot catch it. |
1869 | | #[must_use] |
1870 | 690k | pub fn pool_available_sample(available: usize) -> u64 { |
1871 | 690k | u64::try_from(available) |
1872 | 690k | .unwrap_or(POOL_AVAILABLE_MAX) |
1873 | 690k | .min(POOL_AVAILABLE_MAX) |
1874 | 690k | } |
1875 | | |
1876 | 690k | pub fn record_connection_acquired(pool: &'static str, available: Option<usize>, queued: bool) { |
1877 | 690k | if let Some(available690k ) = available { |
1878 | 690k | CONNECTION_METRICS.pool_available.record( |
1879 | 690k | pool_available_sample(available), |
1880 | 690k | &[KeyValue::new(CONNECTION_POOL, pool)], |
1881 | 690k | ); |
1882 | 690k | }57 |
1883 | 690k | CONNECTION_METRICS.pool_acquisitions.add( |
1884 | | 1, |
1885 | | &[ |
1886 | 690k | KeyValue::new(CONNECTION_POOL, pool), |
1887 | 690k | KeyValue::new( |
1888 | | CONNECTION_RESULT, |
1889 | 690k | if queued { "queued"143k } else { "immediate"546k }, |
1890 | | ), |
1891 | | ], |
1892 | | ); |
1893 | 690k | } |
1894 | | |
1895 | | /// Records a reconnect. A rising count means the backend is flapping, which |
1896 | | /// on Redis usually means a failover. |
1897 | 248 | pub fn record_connection_reconnect(pool: &'static str) { |
1898 | 248 | CONNECTION_METRICS |
1899 | 248 | .reconnects |
1900 | 248 | .add(1, &[KeyValue::new(CONNECTION_POOL, pool)]); |
1901 | 248 | } |
1902 | | |
1903 | | // Metric attribute keys for retried operations. |
1904 | | pub const RETRY_CLIENT: &str = "retry.client"; |
1905 | | pub const RETRY_OUTCOME: &str = "retry.outcome"; |
1906 | | |
1907 | | /// Global metrics for operations run under a [`crate::retry::Retrier`]. |
1908 | 8 | pub static RETRY_METRICS: LazyLock<RetryMetrics> = LazyLock::new(|| { |
1909 | 8 | let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build()); |
1910 | | |
1911 | 8 | RetryMetrics { |
1912 | 8 | outcomes: meter |
1913 | 8 | .u64_counter("retry.outcomes") |
1914 | 8 | .with_description( |
1915 | 8 | "Retried operations by client and outcome: each retry (retried), a success after at least one retry (recovered), retries run out (exhausted), or an error that is never retried, NotFound aside (permanent)", |
1916 | 8 | ) |
1917 | 8 | .with_unit("{event}") |
1918 | 8 | .build(), |
1919 | 8 | } |
1920 | 8 | }); |
1921 | | |
1922 | | /// Metrics for operations run under a [`crate::retry::Retrier`]. |
1923 | | #[derive(Debug)] |
1924 | | pub struct RetryMetrics { |
1925 | | /// Retry events, by client and outcome. |
1926 | | pub outcomes: metrics::Counter<u64>, |
1927 | | } |
1928 | | |
1929 | | /// Records one retry event. `client` names what is retrying (`azure`, `s3`, |
1930 | | /// `grpc_store`, ...), `outcome` is one of `retried`, `recovered`, |
1931 | | /// `exhausted` or `permanent`. An `exhausted` count rising is a backend that |
1932 | | /// stayed down, or throttled, for longer than the retry policy waits. |
1933 | 128 | pub fn record_retry_outcome(client: &'static str, outcome: &'static str) { |
1934 | 128 | RETRY_METRICS.outcomes.add( |
1935 | | 1, |
1936 | 128 | &[ |
1937 | 128 | KeyValue::new(RETRY_CLIENT, client), |
1938 | 128 | KeyValue::new(RETRY_OUTCOME, outcome), |
1939 | 128 | ], |
1940 | | ); |
1941 | 128 | } |