Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}