Coverage Report

Created: 2026-09-18 20:40

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 The NativeLink Authors. 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 requested 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_ACTION_MNEMONIC: &str = "execution.action_mnemonic";
36
pub const EXECUTION_EXIT_CODE: &str = "execution.exit_code";
37
pub const EXECUTION_ACTION_DIGEST: &str = "execution.action_digest";
38
39
// Metric attribute keys for gRPC serving, following OTel rpc semconv.
40
pub const RPC_SERVICE: &str = "rpc.service";
41
pub const RPC_METHOD: &str = "rpc.method";
42
pub const RPC_STATUS_CODE: &str = "rpc.grpc.status_code";
43
44
// Metric attribute keys for the scheduler.
45
pub const SCHEDULER_MATCH_RESULT: &str = "scheduler.match.result";
46
47
// Metric attribute keys for tiered stores.
48
pub const STORE_TIER: &str = "store.tier";
49
pub const STORE_RESULT: &str = "store.result";
50
pub const STORE_DIRECTION: &str = "store.direction";
51
52
// Metric attribute keys for connection pools.
53
pub const CONNECTION_POOL: &str = "connection.pool";
54
pub const CONNECTION_RESULT: &str = "connection.result";
55
56
// Metric attribute keys for health checks.
57
pub const HEALTH_NAMESPACE: &str = "health.namespace";
58
pub const HEALTH_STATUS: &str = "health.status";
59
60
// Metric attribute keys for the worker fleet.
61
pub const WORKER_STATE: &str = "worker.state";
62
pub const WORKER_DISCONNECT_REASON: &str = "worker.disconnect.reason";
63
64
/// Why a worker left the pool.
65
#[derive(Debug, Clone, Copy)]
66
pub enum WorkerDisconnectReason {
67
    /// The worker's connection ended.
68
    Disconnected,
69
    /// The scheduler evicted it, usually after a timeout or an error.
70
    Evicted,
71
}
72
73
impl WorkerDisconnectReason {
74
    #[must_use]
75
16
    pub const fn as_str(self) -> &'static str {
76
16
        match self {
77
3
            Self::Disconnected => "disconnected",
78
13
            Self::Evicted => "evicted",
79
        }
80
16
    }
81
}
82
83
/// Cache operation types for metrics classification.
84
#[derive(Debug, Clone, Copy)]
85
pub enum CacheOperationName {
86
    /// Data retrieval operations (get, peek, contains, etc.)
87
    Read,
88
    /// Data storage operations (insert, update, replace, etc.)
89
    Write,
90
    /// Explicit data removal operations
91
    Delete,
92
    /// Automatic cache maintenance (evictions, TTL cleanup, etc.)
93
    Evict,
94
}
95
96
impl From<CacheOperationName> for Value {
97
33
    fn from(op: CacheOperationName) -> Self {
98
33
        match op {
99
12
            CacheOperationName::Read => Self::from("read"),
100
6
            CacheOperationName::Write => Self::from("write"),
101
9
            CacheOperationName::Delete => Self::from("delete"),
102
6
            CacheOperationName::Evict => Self::from("evict"),
103
        }
104
33
    }
105
}
106
107
/// Results of cache operations.
108
///
109
/// Result semantics vary by operation type:
110
/// - Read: Hit/Miss/Expired indicate data availability
111
/// - Write/Delete/Evict: Success/Error indicate completion status
112
#[derive(Debug, Clone, Copy)]
113
pub enum CacheOperationResult {
114
    /// Data found and valid (Read operations)
115
    Hit,
116
    /// Data not found (Read operations)
117
    Miss,
118
    /// Data found but invalid/expired (Read operations)
119
    Expired,
120
    /// Operation completed successfully (Write/Delete/Evict operations)
121
    Success,
122
    /// Operation failed (any operation type)
123
    Error,
124
}
125
126
impl From<CacheOperationResult> for Value {
127
33
    fn from(result: CacheOperationResult) -> Self {
128
33
        match result {
129
3
            CacheOperationResult::Hit => Self::from("hit"),
130
6
            CacheOperationResult::Miss => Self::from("miss"),
131
6
            CacheOperationResult::Expired => Self::from("expired"),
132
9
            CacheOperationResult::Success => Self::from("success"),
133
9
            CacheOperationResult::Error => Self::from("error"),
134
        }
135
33
    }
136
}
137
138
/// Remote execution stages for metrics classification.
139
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140
pub enum ExecutionStage {
141
    /// Unknown stage
142
    Unknown,
143
    /// Checking cache for existing results
144
    CacheCheck,
145
    /// Action is queued waiting for execution
146
    Queued,
147
    /// Action is being executed by a worker
148
    Executing,
149
    /// Action execution completed
150
    Completed,
151
}
152
153
impl From<ExecutionStage> for Value {
154
246
    fn from(stage: ExecutionStage) -> Self {
155
246
        match stage {
156
1
            ExecutionStage::Unknown => Self::from("unknown"),
157
2
            ExecutionStage::CacheCheck => Self::from("cache_check"),
158
106
            ExecutionStage::Queued => Self::from("queued"),
159
107
            ExecutionStage::Executing => Self::from("executing"),
160
30
            ExecutionStage::Completed => Self::from("completed"),
161
        }
162
246
    }
163
}
164
165
impl From<ActionStage> for ExecutionStage {
166
5
    fn from(stage: ActionStage) -> Self {
167
5
        match stage {
168
1
            ActionStage::Unknown => Self::Unknown,
169
1
            ActionStage::CacheCheck => Self::CacheCheck,
170
1
            ActionStage::Queued => Self::Queued,
171
1
            ActionStage::Executing => Self::Executing,
172
1
            ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => Self::Completed,
173
        }
174
5
    }
175
}
176
177
impl From<&ActionStage> for ExecutionStage {
178
0
    fn from(stage: &ActionStage) -> Self {
179
0
        match stage {
180
0
            ActionStage::Unknown => Self::Unknown,
181
0
            ActionStage::CacheCheck => Self::CacheCheck,
182
0
            ActionStage::Queued => Self::Queued,
183
0
            ActionStage::Executing => Self::Executing,
184
0
            ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => Self::Completed,
185
        }
186
0
    }
187
}
188
189
/// Results of remote execution operations.
190
#[derive(Debug, Clone, Copy)]
191
pub enum ExecutionResult {
192
    /// Execution completed successfully
193
    Success,
194
    /// Execution failed
195
    Failure,
196
    /// Execution was cancelled
197
    Cancelled,
198
    /// Execution timed out
199
    Timeout,
200
    /// Result was found in cache
201
    CacheHit,
202
}
203
204
impl From<ExecutionResult> for Value {
205
28
    fn from(result: ExecutionResult) -> Self {
206
28
        match result {
207
11
            ExecutionResult::Success => Self::from("success"),
208
14
            ExecutionResult::Failure => Self::from("failure"),
209
1
            ExecutionResult::Cancelled => Self::from("cancelled"),
210
1
            ExecutionResult::Timeout => Self::from("timeout"),
211
1
            ExecutionResult::CacheHit => Self::from("cache_hit"),
212
        }
213
28
    }
214
}
215
216
/// Pre-allocated attribute combinations for efficient cache metrics collection.
217
///
218
/// Avoids runtime allocation by pre-computing common attribute combinations
219
/// for cache operations and results.
220
#[derive(Debug)]
221
pub struct CacheMetricAttrs {
222
    // Read operation attributes
223
    read_hit: Vec<KeyValue>,
224
    read_miss: Vec<KeyValue>,
225
    read_expired: Vec<KeyValue>,
226
    read_error: Vec<KeyValue>,
227
228
    // Write operation attributes
229
    write_success: Vec<KeyValue>,
230
    write_error: Vec<KeyValue>,
231
232
    // Delete operation attributes
233
    delete_success: Vec<KeyValue>,
234
    delete_miss: Vec<KeyValue>,
235
    delete_error: Vec<KeyValue>,
236
237
    // Evict operation attributes
238
    evict_success: Vec<KeyValue>,
239
    evict_expired: Vec<KeyValue>,
240
}
241
242
impl CacheMetricAttrs {
243
    /// Creates a new set of pre-computed attributes.
244
    ///
245
    /// The `base_attrs` are included in all attribute combinations (e.g., cache
246
    /// type, instance ID).
247
    #[must_use]
248
3
    pub fn new(base_attrs: &[KeyValue]) -> Self {
249
33
        let 
make_attrs3
= |op: CacheOperationName, result: CacheOperationResult| {
250
33
            let mut attrs = base_attrs.to_vec();
251
33
            attrs.push(KeyValue::new(CACHE_OPERATION, op));
252
33
            attrs.push(KeyValue::new(CACHE_RESULT, result));
253
33
            attrs
254
33
        };
255
256
3
        Self {
257
3
            read_hit: make_attrs(CacheOperationName::Read, CacheOperationResult::Hit),
258
3
            read_miss: make_attrs(CacheOperationName::Read, CacheOperationResult::Miss),
259
3
            read_expired: make_attrs(CacheOperationName::Read, CacheOperationResult::Expired),
260
3
            read_error: make_attrs(CacheOperationName::Read, CacheOperationResult::Error),
261
3
262
3
            write_success: make_attrs(CacheOperationName::Write, CacheOperationResult::Success),
263
3
            write_error: make_attrs(CacheOperationName::Write, CacheOperationResult::Error),
264
3
265
3
            delete_success: make_attrs(CacheOperationName::Delete, CacheOperationResult::Success),
266
3
            delete_miss: make_attrs(CacheOperationName::Delete, CacheOperationResult::Miss),
267
3
            delete_error: make_attrs(CacheOperationName::Delete, CacheOperationResult::Error),
268
3
269
3
            evict_success: make_attrs(CacheOperationName::Evict, CacheOperationResult::Success),
270
3
            evict_expired: make_attrs(CacheOperationName::Evict, CacheOperationResult::Expired),
271
3
        }
272
3
    }
273
274
    // Attribute accessors
275
    #[must_use]
276
0
    pub fn read_hit(&self) -> &[KeyValue] {
277
0
        &self.read_hit
278
0
    }
279
    #[must_use]
280
0
    pub fn read_miss(&self) -> &[KeyValue] {
281
0
        &self.read_miss
282
0
    }
283
    #[must_use]
284
0
    pub fn read_expired(&self) -> &[KeyValue] {
285
0
        &self.read_expired
286
0
    }
287
    #[must_use]
288
0
    pub fn read_error(&self) -> &[KeyValue] {
289
0
        &self.read_error
290
0
    }
291
    #[must_use]
292
4
    pub fn write_success(&self) -> &[KeyValue] {
293
4
        &self.write_success
294
4
    }
295
    #[must_use]
296
0
    pub fn write_error(&self) -> &[KeyValue] {
297
0
        &self.write_error
298
0
    }
299
    #[must_use]
300
0
    pub fn delete_success(&self) -> &[KeyValue] {
301
0
        &self.delete_success
302
0
    }
303
    #[must_use]
304
0
    pub fn delete_miss(&self) -> &[KeyValue] {
305
0
        &self.delete_miss
306
0
    }
307
    #[must_use]
308
0
    pub fn delete_error(&self) -> &[KeyValue] {
309
0
        &self.delete_error
310
0
    }
311
    #[must_use]
312
0
    pub fn evict_success(&self) -> &[KeyValue] {
313
0
        &self.evict_success
314
0
    }
315
    #[must_use]
316
0
    pub fn evict_expired(&self) -> &[KeyValue] {
317
0
        &self.evict_expired
318
0
    }
319
}
320
321
/// Pre-allocated attribute combinations for efficient remote execution metrics collection.
322
#[derive(Debug)]
323
pub struct ExecutionMetricAttrs {
324
    // Stage transition attributes
325
    unknown: Vec<KeyValue>,
326
    cache_check: Vec<KeyValue>,
327
    queued: Vec<KeyValue>,
328
    executing: Vec<KeyValue>,
329
    completed_success: Vec<KeyValue>,
330
    completed_failure: Vec<KeyValue>,
331
    completed_cancelled: Vec<KeyValue>,
332
    completed_timeout: Vec<KeyValue>,
333
    completed_cache_hit: Vec<KeyValue>,
334
}
335
336
impl ExecutionMetricAttrs {
337
    /// Creates a new set of pre-computed attributes.
338
    ///
339
    /// The `base_attrs` are included in all attribute combinations (e.g., instance
340
    /// name, worker ID).
341
    #[must_use]
342
1
    pub fn new(base_attrs: &[KeyValue]) -> Self {
343
9
        let 
make_attrs1
= |stage: ExecutionStage, result: Option<ExecutionResult>| {
344
9
            let mut attrs = base_attrs.to_vec();
345
9
            attrs.push(KeyValue::new(EXECUTION_STAGE, stage));
346
9
            if let Some(
result5
) = result {
347
5
                attrs.push(KeyValue::new(EXECUTION_RESULT, result));
348
5
            
}4
349
9
            attrs
350
9
        };
351
352
1
        Self {
353
1
            unknown: make_attrs(ExecutionStage::Unknown, None),
354
1
            cache_check: make_attrs(ExecutionStage::CacheCheck, None),
355
1
            queued: make_attrs(ExecutionStage::Queued, None),
356
1
            executing: make_attrs(ExecutionStage::Executing, None),
357
1
            completed_success: make_attrs(
358
1
                ExecutionStage::Completed,
359
1
                Some(ExecutionResult::Success),
360
1
            ),
361
1
            completed_failure: make_attrs(
362
1
                ExecutionStage::Completed,
363
1
                Some(ExecutionResult::Failure),
364
1
            ),
365
1
            completed_cancelled: make_attrs(
366
1
                ExecutionStage::Completed,
367
1
                Some(ExecutionResult::Cancelled),
368
1
            ),
369
1
            completed_timeout: make_attrs(
370
1
                ExecutionStage::Completed,
371
1
                Some(ExecutionResult::Timeout),
372
1
            ),
373
1
            completed_cache_hit: make_attrs(
374
1
                ExecutionStage::Completed,
375
1
                Some(ExecutionResult::CacheHit),
376
1
            ),
377
1
        }
378
1
    }
379
380
    // Attribute accessors
381
    #[must_use]
382
0
    pub fn unknown(&self) -> &[KeyValue] {
383
0
        &self.unknown
384
0
    }
385
    #[must_use]
386
0
    pub fn cache_check(&self) -> &[KeyValue] {
387
0
        &self.cache_check
388
0
    }
389
    #[must_use]
390
1
    pub fn queued(&self) -> &[KeyValue] {
391
1
        &self.queued
392
1
    }
393
    #[must_use]
394
0
    pub fn executing(&self) -> &[KeyValue] {
395
0
        &self.executing
396
0
    }
397
    #[must_use]
398
1
    pub fn completed_success(&self) -> &[KeyValue] {
399
1
        &self.completed_success
400
1
    }
401
    #[must_use]
402
0
    pub fn completed_failure(&self) -> &[KeyValue] {
403
0
        &self.completed_failure
404
0
    }
405
    #[must_use]
406
0
    pub fn completed_cancelled(&self) -> &[KeyValue] {
407
0
        &self.completed_cancelled
408
0
    }
409
    #[must_use]
410
0
    pub fn completed_timeout(&self) -> &[KeyValue] {
411
0
        &self.completed_timeout
412
0
    }
413
    #[must_use]
414
0
    pub fn completed_cache_hit(&self) -> &[KeyValue] {
415
0
        &self.completed_cache_hit
416
0
    }
417
}
418
419
/// Global cache metrics instruments.
420
3
pub static CACHE_METRICS: LazyLock<CacheMetrics> = LazyLock::new(|| {
421
3
    let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build());
422
423
3
    CacheMetrics {
424
3
        cache_operation_duration: meter
425
3
            .f64_histogram("cache.operation.duration")
426
3
            .with_description("Duration of cache operations in milliseconds")
427
3
            .with_unit("ms")
428
3
            // The range of these is quite large as a cache might be backed by
429
3
            // memory, a filesystem, or network storage. The current values were
430
3
            // determined empirically and might need adjustment.
431
3
            .with_boundaries(vec![
432
3
                // Microsecond range
433
3
                0.001, // 1μs
434
3
                0.005, // 5μs
435
3
                0.01,  // 10μs
436
3
                0.05,  // 50μs
437
3
                0.1,   // 100μs
438
3
                // Sub-millisecond range
439
3
                0.2, // 200μs
440
3
                0.5, // 500μs
441
3
                1.0, // 1ms
442
3
                // Low millisecond range
443
3
                2.0,   // 2ms
444
3
                5.0,   // 5ms
445
3
                10.0,  // 10ms
446
3
                20.0,  // 20ms
447
3
                50.0,  // 50ms
448
3
                100.0, // 100ms
449
3
                // Higher latency range
450
3
                200.0,  // 200ms
451
3
                500.0,  // 500ms
452
3
                1000.0, // 1 second
453
3
                2000.0, // 2 seconds
454
3
                5000.0, // 5 seconds
455
3
            ])
456
3
            .build(),
457
3
458
3
        cache_operations: meter
459
3
            .u64_counter("cache.operations")
460
3
            .with_description("Total cache operations by type and result")
461
3
            .build(),
462
3
463
3
        cache_io: meter
464
3
            .u64_counter("cache.io")
465
3
            .with_description("Total bytes processed by cache operations")
466
3
            .with_unit("By")
467
3
            .build(),
468
3
469
3
        cache_size: meter
470
3
            .i64_up_down_counter("cache.size")
471
3
            .with_description("Current total size of cached data")
472
3
            .with_unit("By")
473
3
            .build(),
474
3
475
3
        cache_entries: meter
476
3
            .i64_up_down_counter("cache.entries")
477
3
            .with_description("Current number of cached entries")
478
3
            .with_unit("{entry}")
479
3
            .build(),
480
3
481
3
        cache_entry_size: meter
482
3
            .u64_histogram("cache.item.size")
483
3
            .with_description("Size distribution of cached entries")
484
3
            .with_unit("By")
485
3
            .build(),
486
3
    }
487
3
});
488
489
/// OpenTelemetry metrics instruments for cache monitoring.
490
#[derive(Debug)]
491
pub struct CacheMetrics {
492
    /// Histogram of cache operation durations in milliseconds
493
    pub cache_operation_duration: metrics::Histogram<f64>,
494
    /// Counter of cache operations by type and result
495
    pub cache_operations: metrics::Counter<u64>,
496
    /// Counter of bytes read/written during cache operations
497
    pub cache_io: metrics::Counter<u64>,
498
    /// Current total size of all cached data in bytes
499
    pub cache_size: metrics::UpDownCounter<i64>,
500
    /// Current number of entries in cache
501
    pub cache_entries: metrics::UpDownCounter<i64>,
502
    /// Histogram of individual cache entry sizes in bytes
503
    pub cache_entry_size: metrics::Histogram<u64>,
504
}
505
506
/// Records a net change in a cache's contents, for `cache.size` and
507
/// `cache.entries`. Deltas are signed: entries leaving a cache, whether
508
/// evicted, replaced or deleted, pass negative values.
509
11
pub fn record_cache_entries_delta(size_delta: i64, entries_delta: i64, attrs: &[KeyValue]) {
510
11
    CACHE_METRICS.cache_size.add(size_delta, attrs);
511
11
    CACHE_METRICS.cache_entries.add(entries_delta, attrs);
512
11
}
513
514
/// Converts a size to the signed type the cache instruments take, clamping
515
/// rather than wrapping on a value too large to represent.
516
10.1k
pub fn saturating_i64(value: u64) -> i64 {
517
10.1k
    i64::try_from(value).unwrap_or(i64::MAX)
518
10.1k
}
519
520
/// Global remote execution metrics instruments.
521
6
pub static EXECUTION_METRICS: LazyLock<ExecutionMetrics> = LazyLock::new(|| {
522
6
    let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build());
523
524
6
    ExecutionMetrics {
525
6
        execution_stage_duration: meter
526
6
            .f64_histogram("execution.stage.duration")
527
6
            .with_description("Duration of each execution stage in seconds")
528
6
            .with_unit("s")
529
6
            .with_boundaries(vec![
530
6
                // Sub-second range
531
6
                0.001, // 1ms
532
6
                0.01,  // 10ms
533
6
                0.1,   // 100ms
534
6
                0.5,   // 500ms
535
6
                1.0,   // 1s
536
6
                // Multi-second range
537
6
                2.0,    // 2s
538
6
                5.0,    // 5s
539
6
                10.0,   // 10s
540
6
                30.0,   // 30s
541
6
                60.0,   // 1 minute
542
6
                120.0,  // 2 minutes
543
6
                300.0,  // 5 minutes
544
6
                600.0,  // 10 minutes
545
6
                1800.0, // 30 minutes
546
6
                3600.0, // 1 hour
547
6
            ])
548
6
            .build(),
549
6
550
6
        execution_total_duration: meter
551
6
            .f64_histogram("execution.total.duration")
552
6
            .with_description(
553
6
                "Total duration of action execution from submission to completion in seconds",
554
6
            )
555
6
            .with_unit("s")
556
6
            .with_boundaries(vec![
557
6
                // Sub-second range
558
6
                0.01, // 10ms
559
6
                0.1,  // 100ms
560
6
                0.5,  // 500ms
561
6
                1.0,  // 1s
562
6
                // Multi-second range
563
6
                5.0,    // 5s
564
6
                10.0,   // 10s
565
6
                30.0,   // 30s
566
6
                60.0,   // 1 minute
567
6
                300.0,  // 5 minutes
568
6
                600.0,  // 10 minutes
569
6
                1800.0, // 30 minutes
570
6
                3600.0, // 1 hour
571
6
                7200.0, // 2 hours
572
6
            ])
573
6
            .build(),
574
6
575
6
        execution_queue_time: meter
576
6
            .f64_histogram("execution.queue.time")
577
6
            .with_description("Time spent waiting in queue before execution in seconds")
578
6
            .with_unit("s")
579
6
            .with_boundaries(vec![
580
6
                0.001, // 1ms
581
6
                0.01,  // 10ms
582
6
                0.1,   // 100ms
583
6
                0.5,   // 500ms
584
6
                1.0,   // 1s
585
6
                2.0,   // 2s
586
6
                5.0,   // 5s
587
6
                10.0,  // 10s
588
6
                30.0,  // 30s
589
6
                60.0,  // 1 minute
590
6
                300.0, // 5 minutes
591
6
                600.0, // 10 minutes
592
6
            ])
593
6
            .build(),
594
6
595
6
        execution_active_count: meter
596
6
            .i64_up_down_counter("execution.active.count")
597
6
            .with_description("Number of actions currently in each stage")
598
6
            .with_unit("{action}")
599
6
            .build(),
600
6
601
6
        execution_completed_count: meter
602
6
            .u64_counter("execution.completed.count")
603
6
            .with_description("Total number of completed executions by result")
604
6
            .with_unit("{action}")
605
6
            .build(),
606
6
607
6
        execution_stage_transitions: meter
608
6
            .u64_counter("execution.stage.transitions")
609
6
            .with_description("Number of stage transitions")
610
6
            .with_unit("{transition}")
611
6
            .build(),
612
6
613
6
        execution_cpu_time: meter
614
6
            .f64_histogram("execution.cpu.time")
615
6
            .with_description("CPU time consumed by an action in seconds")
616
6
            .with_unit("s")
617
6
            .with_boundaries(vec![
618
6
                0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 300.0, 600.0, 1800.0, 3600.0, 7200.0,
619
6
            ])
620
6
            .build(),
621
6
622
6
        execution_peak_memory: meter
623
6
            .u64_histogram("execution.peak.memory")
624
6
            .with_description("Peak resident memory observed while running an action in bytes")
625
6
            .with_unit("By")
626
6
            .with_boundaries(vec![
627
6
                1_048_576.0,      // 1MiB
628
6
                16_777_216.0,     // 16MiB
629
6
                67_108_864.0,     // 64MiB
630
6
                268_435_456.0,    // 256MiB
631
6
                1_073_741_824.0,  // 1GiB
632
6
                4_294_967_296.0,  // 4GiB
633
6
                17_179_869_184.0, // 16GiB
634
6
                68_719_476_736.0, // 64GiB
635
6
            ])
636
6
            .build(),
637
6
638
6
        execution_output_size: meter
639
6
            .u64_histogram("execution.output.size")
640
6
            .with_description("Size of execution outputs in bytes")
641
6
            .with_unit("By")
642
6
            .with_boundaries(vec![
643
6
                1_024.0,          // 1KB
644
6
                10_240.0,         // 10KB
645
6
                102_400.0,        // 100KB
646
6
                1_048_576.0,      // 1MB
647
6
                10_485_760.0,     // 10MB
648
6
                104_857_600.0,    // 100MB
649
6
                1_073_741_824.0,  // 1GB
650
6
                10_737_418_240.0, // 10GB
651
6
            ])
652
6
            .build(),
653
6
654
6
        execution_retry_count: meter
655
6
            .u64_counter("execution.retry.count")
656
6
            .with_description("Number of execution retries")
657
6
            .with_unit("{retry}")
658
6
            .build(),
659
6
    }
660
6
});
661
662
/// OpenTelemetry metrics instruments for remote execution monitoring.
663
#[derive(Debug)]
664
pub struct ExecutionMetrics {
665
    /// Histogram of stage durations in seconds
666
    pub execution_stage_duration: metrics::Histogram<f64>,
667
    /// Histogram of total execution durations in seconds
668
    pub execution_total_duration: metrics::Histogram<f64>,
669
    /// Histogram of queue wait times in seconds
670
    pub execution_queue_time: metrics::Histogram<f64>,
671
    /// Current number of actions in each stage
672
    pub execution_active_count: metrics::UpDownCounter<i64>,
673
    /// Total number of completed executions
674
    pub execution_completed_count: metrics::Counter<u64>,
675
    /// Number of stage transitions
676
    pub execution_stage_transitions: metrics::Counter<u64>,
677
    /// Histogram of output sizes in bytes
678
    pub execution_output_size: metrics::Histogram<u64>,
679
    /// Counter for execution retries
680
    pub execution_retry_count: metrics::Counter<u64>,
681
    /// Peak memory an action used, as sampled by the worker.
682
    pub execution_peak_memory: metrics::Histogram<u64>,
683
    /// CPU time an action used, as sampled by the worker.
684
    pub execution_cpu_time: metrics::Histogram<f64>,
685
}
686
687
/// Records the CPU time a worker observed for an action.
688
///
689
/// Wall time is already covered by `execution.stage.duration`. This is the
690
/// other half: an action pinning eight cores for a minute and one sleeping
691
/// for a minute look identical by wall time and nothing alike here.
692
1
pub fn record_execution_cpu_time(cpu_time_ms: u64, instance_name: &str, action_mnemonic: &str) {
693
    #[expect(clippy::cast_precision_loss)] // Milliseconds; f64 is exact well past any real action.
694
1
    let seconds = cpu_time_ms as f64 / 1000.0;
695
1
    EXECUTION_METRICS.execution_cpu_time.record(
696
1
        seconds,
697
1
        &execution_sample_attributes(instance_name, action_mnemonic),
698
    );
699
1
}
700
701
/// Records the peak memory a worker observed while running an action.
702
///
703
/// The worker samples this, so it is only present when sampling ran. #2614
704
/// listed `execution_memory_usage` as unimplementable because
705
/// `ExecutionMetadata` carries no memory figure; `ActionResourceUsage` does,
706
/// and this is that number.
707
2
pub fn record_execution_peak_memory(
708
2
    peak_memory_kb: u64,
709
2
    instance_name: &str,
710
2
    action_mnemonic: &str,
711
2
) {
712
2
    EXECUTION_METRICS.execution_peak_memory.record(
713
2
        peak_memory_sample(peak_memory_kb),
714
2
        &execution_sample_attributes(instance_name, action_mnemonic),
715
    );
716
2
}
717
718
/// Attributes for a per-action resource sample. The mnemonic is attached only
719
/// when known: an empty value on every sample from an unattributed client
720
/// would read as a real mnemonic named "".
721
3
fn execution_sample_attributes(instance_name: &str, action_mnemonic: &str) -> Vec<KeyValue> {
722
3
    let mut attrs = vec![KeyValue::new(EXECUTION_INSTANCE, instance_name.to_string())];
723
3
    if !action_mnemonic.is_empty() {
724
3
        attrs.push(KeyValue::new(
725
3
            EXECUTION_ACTION_MNEMONIC,
726
3
            action_mnemonic.to_string(),
727
3
        ));
728
3
    
}0
729
3
    attrs
730
3
}
731
732
/// Ceiling on a recorded peak-memory sample.
733
///
734
/// Well above any real action and far below the point where a histogram's
735
/// `u64` sum can wrap, so it filters a garbage reading without touching a
736
/// plausible one. Everything past the largest bucket already shares the same
737
/// bucket, so this changes no bucket count.
738
const PEAK_MEMORY_MAX_BYTES: u64 = 1 << 50; // 1 PiB.
739
740
/// Converts a worker's peak-memory reading to bytes, bounded.
741
///
742
/// The figure crosses a gRPC boundary from a worker, so a malfunctioning one
743
/// could report something absurd. An unbounded value overflows the
744
/// histogram's `u64` sum, which panics a debug build and takes the thread
745
/// with it. Kept separate so it is directly testable: the overflow only
746
/// happens once a meter provider is installed, which no unit test does.
747
#[must_use]
748
7
pub fn peak_memory_sample(peak_memory_kb: u64) -> u64 {
749
7
    peak_memory_kb
750
7
        .saturating_mul(1024)
751
7
        .min(PEAK_MEMORY_MAX_BYTES)
752
7
}
753
754
/// Helper function to create attributes for execution metrics
755
#[must_use]
756
192
pub fn make_execution_attributes(
757
192
    instance_name: &str,
758
192
    worker_id: Option<&str>,
759
192
    priority: Option<i32>,
760
192
) -> Vec<KeyValue> {
761
192
    let mut attrs = vec![KeyValue::new(EXECUTION_INSTANCE, instance_name.to_string())];
762
763
192
    if let Some(
worker_id63
) = worker_id {
764
63
        attrs.push(KeyValue::new(EXECUTION_WORKER_ID, worker_id.to_string()));
765
129
    }
766
767
192
    if let Some(
priority191
) = priority {
768
191
        attrs.push(KeyValue::new(EXECUTION_PRIORITY, i64::from(priority)));
769
191
    
}1
770
771
192
    attrs
772
192
}
773
774
/// Records the histogram metrics derivable from a completed action's result.
775
11
pub fn record_completed_execution_metrics(
776
11
    action_result: &ActionResult,
777
11
    instance_name: &str,
778
11
    worker_id: Option<&str>,
779
11
    priority: Option<i32>,
780
11
) {
781
11
    let m = &*EXECUTION_METRICS;
782
11
    let md = &action_result.execution_metadata;
783
11
    let base = make_execution_attributes(instance_name, worker_id, priority);
784
785
11
    let record_secs =
786
55
        |hist: &metrics::Histogram<f64>, start: SystemTime, end: SystemTime, attrs: &[KeyValue]| {
787
55
            if start > SystemTime::UNIX_EPOCH
788
25
                && let Ok(d) = end.duration_since(start)
789
25
            {
790
25
                hist.record(d.as_secs_f64(), attrs);
791
30
            }
792
55
        };
793
794
    // Queue wait (queued -> worker picked it up) and end-to-end duration.
795
11
    record_secs(
796
11
        &m.execution_queue_time,
797
11
        md.queued_timestamp,
798
11
        md.worker_start_timestamp,
799
11
        &base,
800
11
    );
801
11
    record_secs(
802
11
        &m.execution_total_duration,
803
11
        md.queued_timestamp,
804
11
        md.worker_completed_timestamp,
805
11
        &base,
806
11
    );
807
808
    // Per-phase stage durations, labeled by phase on the stage attribute.
809
33
    for (phase, start, end) in [
810
11
        (
811
11
            "input_fetch",
812
11
            md.input_fetch_start_timestamp,
813
11
            md.input_fetch_completed_timestamp,
814
11
        ),
815
11
        (
816
11
            "execution",
817
11
            md.execution_start_timestamp,
818
11
            md.execution_completed_timestamp,
819
11
        ),
820
11
        (
821
11
            "output_upload",
822
11
            md.output_upload_start_timestamp,
823
11
            md.output_upload_completed_timestamp,
824
11
        ),
825
33
    ] {
826
33
        let mut attrs = base.clone();
827
33
        attrs.push(KeyValue::new(EXECUTION_STAGE, phase));
828
33
        record_secs(&m.execution_stage_duration, start, end, &attrs);
829
33
    }
830
831
    // Total bytes produced: output files plus stdout/stderr.
832
11
    m.execution_output_size
833
11
        .record(execution_output_bytes(action_result), &base);
834
11
}
835
836
/// Total output bytes produced by an action: the output file digests plus the
837
/// stdout and stderr digests.
838
#[must_use]
839
12
pub fn execution_output_bytes(action_result: &ActionResult) -> u64 {
840
12
    action_result
841
12
        .output_files
842
12
        .iter()
843
12
        .map(|f| 
f.digest4
.
size_bytes4
())
844
12
        .sum::<u64>()
845
12
        + action_result.stdout_digest.size_bytes()
846
12
        + action_result.stderr_digest.size_bytes()
847
12
}
848
849
/// Global worker fleet metrics instruments.
850
4
pub static WORKER_METRICS: LazyLock<WorkerMetrics> = LazyLock::new(|| {
851
4
    let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build());
852
853
4
    WorkerMetrics {
854
4
        worker_connected_count: meter
855
4
            .i64_up_down_counter("worker.connected.count")
856
4
            .with_description("Number of workers currently connected to this scheduler instance")
857
4
            .with_unit("{worker}")
858
4
            .build(),
859
4
860
4
        worker_connections: meter
861
4
            .u64_counter("worker.connections")
862
4
            .with_description("Total number of workers that have joined the pool")
863
4
            .with_unit("{worker}")
864
4
            .build(),
865
4
866
4
        worker_disconnections: meter
867
4
            .u64_counter("worker.disconnections")
868
4
            .with_description("Total number of workers that have left the pool, by reason")
869
4
            .with_unit("{worker}")
870
4
            .build(),
871
4
872
4
        worker_keepalives: meter
873
4
            .u64_counter("worker.keepalives")
874
4
            .with_description("Total worker keepalives received")
875
4
            .with_unit("{keepalive}")
876
4
            .build(),
877
4
878
4
        worker_state_count: meter
879
4
            .i64_up_down_counter("worker.state.count")
880
4
            .with_description("Number of connected workers in each non-default state")
881
4
            .with_unit("{worker}")
882
4
            .build(),
883
4
    }
884
4
});
885
886
/// Worker fleet metrics.
887
///
888
/// These are per-scheduler-instance. A worker only appears in the registry of
889
/// the instance holding its stream, so with several schedulers behind a load
890
/// balancer the fleet total is the sum across instances, not any one of them.
891
///
892
/// Deliberately not attributed by worker id. Ids are per connection, so a
893
/// fleet that churns would grow the label set without bound, and the
894
/// autoscaling and fleet-health questions these answer are all aggregate.
895
#[derive(Debug)]
896
pub struct WorkerMetrics {
897
    /// Workers currently connected.
898
    pub worker_connected_count: metrics::UpDownCounter<i64>,
899
    /// Workers that have joined, cumulative.
900
    pub worker_connections: metrics::Counter<u64>,
901
    /// Workers that have left, cumulative, by reason.
902
    pub worker_disconnections: metrics::Counter<u64>,
903
    /// Keepalives received, cumulative.
904
    pub worker_keepalives: metrics::Counter<u64>,
905
    /// Connected workers currently paused or draining.
906
    pub worker_state_count: metrics::UpDownCounter<i64>,
907
}
908
909
/// Records a worker joining the pool.
910
46
pub fn record_worker_connected() {
911
46
    WORKER_METRICS.worker_connected_count.add(1, &[]);
912
46
    WORKER_METRICS.worker_connections.add(1, &[]);
913
46
}
914
915
/// Records a worker leaving the pool.
916
///
917
/// `was_draining` and `was_paused` unwind the state gauges, which would
918
/// otherwise keep counting a worker that is already gone.
919
14
pub fn record_worker_disconnected(
920
14
    reason: WorkerDisconnectReason,
921
14
    was_draining: bool,
922
14
    was_paused: bool,
923
14
) {
924
14
    WORKER_METRICS.worker_connected_count.add(-1, &[]);
925
14
    WORKER_METRICS.worker_disconnections.add(
926
        1,
927
14
        &[KeyValue::new(WORKER_DISCONNECT_REASON, reason.as_str())],
928
    );
929
14
    if was_draining {
930
1
        record_worker_state("draining", false);
931
13
    }
932
14
    if was_paused {
933
1
        record_worker_state("paused", false);
934
13
    }
935
14
}
936
937
/// Records a worker entering or leaving `state`.
938
8
pub fn record_worker_state(state: &'static str, entered: bool) {
939
8
    WORKER_METRICS.worker_state_count.add(
940
8
        if entered { 
14
} else {
-14
},
941
8
        &[KeyValue::new(WORKER_STATE, state)],
942
    );
943
8
}
944
945
/// Records a keepalive from a worker.
946
5
pub fn record_worker_keepalive() {
947
5
    WORKER_METRICS.worker_keepalives.add(1, &[]);
948
5
}
949
950
/// Global gRPC serving metrics.
951
///
952
/// One duration histogram covers rate, errors and latency: the count gives
953
/// rate, the status attribute separates errors, and the buckets give latency.
954
2
pub static RPC_METRICS: LazyLock<RpcMetrics> = LazyLock::new(|| {
955
2
    let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build());
956
957
2
    RpcMetrics {
958
2
        rpc_server_duration: meter
959
2
            .f64_histogram("rpc.server.duration")
960
2
            .with_description("Duration of inbound gRPC calls in seconds")
961
2
            .with_unit("s")
962
2
            .with_boundaries(vec![
963
2
                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,
964
2
                300.0,
965
2
            ])
966
2
            .build(),
967
2
    }
968
2
});
969
970
/// gRPC serving metrics.
971
#[derive(Debug)]
972
pub struct RpcMetrics {
973
    /// Duration of inbound gRPC calls, by service, method and status.
974
    pub rpc_server_duration: metrics::Histogram<f64>,
975
}
976
977
/// Records a served gRPC call.
978
///
979
/// `full_path` is the HTTP path tonic routes on, `/package.Service/Method`.
980
5
pub fn record_rpc_served(full_path: &str, grpc_status: i32, duration_secs: f64) {
981
5
    let (service, method) = split_grpc_path(full_path);
982
5
    RPC_METRICS.rpc_server_duration.record(
983
5
        duration_secs,
984
5
        &[
985
5
            KeyValue::new(RPC_SERVICE, service.to_string()),
986
5
            KeyValue::new(RPC_METHOD, method.to_string()),
987
5
            KeyValue::new(RPC_STATUS_CODE, i64::from(grpc_status)),
988
5
        ],
989
    );
990
5
}
991
992
/// Splits `/package.Service/Method` into its service and method halves.
993
///
994
/// Anything that does not look like a gRPC path is reported whole under an
995
/// `unknown` method, so an unexpected route still shows up rather than
996
/// silently vanishing. Both halves come from the route table, not user input,
997
/// so the label set stays bounded.
998
#[must_use]
999
9
pub fn split_grpc_path(full_path: &str) -> (&str, &str) {
1000
9
    let trimmed = full_path.strip_prefix('/').unwrap_or(full_path);
1001
9
    trimmed
1002
9
        .split_once('/')
1003
9
        .map_or((trimmed, "unknown"), |(service, method)| (
service4
,
method4
))
1004
9
}
1005
1006
/// Global scheduler metrics.
1007
3
pub static SCHEDULER_METRICS: LazyLock<SchedulerOtlpMetrics> = LazyLock::new(|| {
1008
3
    let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build());
1009
1010
3
    SchedulerOtlpMetrics {
1011
3
        matching_duration: meter
1012
3
            .f64_histogram("scheduler.matching.duration")
1013
3
            .with_description("Duration of one queued-action to worker matching pass in seconds")
1014
3
            .with_unit("s")
1015
3
            .with_boundaries(vec![
1016
3
                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,
1017
3
            ])
1018
3
            .build(),
1019
3
1020
3
        matching_passes: meter
1021
3
            .u64_counter("scheduler.matching.passes")
1022
3
            .with_description("Matching passes run, by result")
1023
3
            .with_unit("{pass}")
1024
3
            .build(),
1025
3
    }
1026
3
});
1027
1028
/// Scheduler matching metrics.
1029
///
1030
/// Named to avoid colliding with the existing `#[metric]` component struct of
1031
/// the same idea in the scheduler crate.
1032
#[derive(Debug)]
1033
pub struct SchedulerOtlpMetrics {
1034
    /// How long a matching pass takes. The saturation signal: this climbing
1035
    /// while the queue is non-empty means matching is the bottleneck.
1036
    pub matching_duration: metrics::Histogram<f64>,
1037
    /// Matching passes, by result.
1038
    pub matching_passes: metrics::Counter<u64>,
1039
}
1040
1041
/// Records a completed matching pass.
1042
152
pub fn record_matching_pass(duration_secs: f64, succeeded: bool) {
1043
152
    SCHEDULER_METRICS
1044
152
        .matching_duration
1045
152
        .record(duration_secs, &[]);
1046
152
    SCHEDULER_METRICS.matching_passes.add(
1047
        1,
1048
152
        &[KeyValue::new(
1049
            SCHEDULER_MATCH_RESULT,
1050
152
            if succeeded { 
"ok"149
} else {
"error"3
},
1051
        )],
1052
    );
1053
152
}
1054
1055
/// Global tiered-store metrics.
1056
6
pub static STORE_TIER_METRICS: LazyLock<StoreTierMetrics> = LazyLock::new(|| {
1057
6
    let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build());
1058
1059
6
    StoreTierMetrics {
1060
6
        tier_operations: meter
1061
6
            .u64_counter("store.tier.operations")
1062
6
            .with_description("Reads served by each tier of a tiered store, by result")
1063
6
            .with_unit("{operation}")
1064
6
            .build(),
1065
6
1066
6
        tier_io: meter
1067
6
            .u64_counter("store.tier.io")
1068
6
            .with_description("Bytes moved through each tier of a tiered store")
1069
6
            .with_unit("By")
1070
6
            .build(),
1071
6
    }
1072
6
});
1073
1074
/// Tiered-store metrics. The hit ratio is `tier_operations{result="hit"}` over
1075
/// the sum, which is the number worth alerting on for a fast/slow store.
1076
#[derive(Debug)]
1077
pub struct StoreTierMetrics {
1078
    /// Reads served per tier, by result.
1079
    pub tier_operations: metrics::Counter<u64>,
1080
    /// Bytes moved per tier and direction.
1081
    pub tier_io: metrics::Counter<u64>,
1082
}
1083
1084
/// Records a read served, or not served, by a tier.
1085
2.22k
pub fn record_store_tier_read(tier: &'static str, result: &'static str) {
1086
2.22k
    STORE_TIER_METRICS.tier_operations.add(
1087
        1,
1088
2.22k
        &[
1089
2.22k
            KeyValue::new(STORE_TIER, tier),
1090
2.22k
            KeyValue::new(STORE_RESULT, result),
1091
2.22k
        ],
1092
    );
1093
2.22k
}
1094
1095
/// Records bytes moved through a tier.
1096
1.81k
pub fn record_store_tier_io(tier: &'static str, direction: &'static str, bytes: u64) {
1097
1.81k
    STORE_TIER_METRICS.tier_io.add(
1098
1.81k
        bytes,
1099
1.81k
        &[
1100
1.81k
            KeyValue::new(STORE_TIER, tier),
1101
1.81k
            KeyValue::new(STORE_DIRECTION, direction),
1102
1.81k
        ],
1103
    );
1104
1.81k
}
1105
1106
/// Global health-check metrics.
1107
3
pub static HEALTH_METRICS: LazyLock<HealthMetrics> = LazyLock::new(|| {
1108
3
    let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build());
1109
1110
3
    HealthMetrics {
1111
3
        health_checks: meter
1112
3
            .u64_counter("health.checks")
1113
3
            .with_description("Health check results, by component and status")
1114
3
            .with_unit("{check}")
1115
3
            .build(),
1116
3
    }
1117
3
});
1118
1119
/// Health-check metrics.
1120
#[derive(Debug)]
1121
pub struct HealthMetrics {
1122
    /// Health check results, by namespace and status.
1123
    pub health_checks: metrics::Counter<u64>,
1124
}
1125
1126
/// Records one component's health check result.
1127
18
pub fn record_health_check(namespace: &str, status: &'static str) {
1128
18
    HEALTH_METRICS.health_checks.add(
1129
        1,
1130
18
        &[
1131
18
            KeyValue::new(HEALTH_NAMESPACE, namespace.to_string()),
1132
18
            KeyValue::new(HEALTH_STATUS, status),
1133
18
        ],
1134
    );
1135
18
}
1136
1137
/// Global connection-pool metrics.
1138
8
pub static CONNECTION_METRICS: LazyLock<ConnectionMetrics> = LazyLock::new(|| {
1139
8
    let meter = global::meter_with_scope(InstrumentationScope::builder("nativelink").build());
1140
1141
8
    ConnectionMetrics {
1142
8
        pool_available: meter
1143
8
            .u64_histogram("connection.pool.available")
1144
8
            .with_description("Free slots in a connection pool, sampled when one is taken")
1145
8
            .with_unit("{connection}")
1146
8
            .with_boundaries(vec![
1147
8
                0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0,
1148
8
            ])
1149
8
            .build(),
1150
8
1151
8
        pool_acquisitions: meter
1152
8
            .u64_counter("connection.pool.acquisitions")
1153
8
            .with_description("Connection acquisitions, by pool and whether one was free")
1154
8
            .with_unit("{acquisition}")
1155
8
            .build(),
1156
8
1157
8
        reconnects: meter
1158
8
            .u64_counter("connection.reconnects")
1159
8
            .with_description("Reconnects performed, by pool")
1160
8
            .with_unit("{reconnect}")
1161
8
            .build(),
1162
8
    }
1163
8
});
1164
1165
/// Connection-pool metrics.
1166
///
1167
/// `pool_available` is the saturation signal: a pool that keeps reporting zero
1168
/// free slots is the bottleneck, whatever the latency elsewhere says. Counting
1169
/// queued acquisitions gives the same answer from the other direction.
1170
#[derive(Debug)]
1171
pub struct ConnectionMetrics {
1172
    /// Free slots at the moment a connection was taken.
1173
    pub pool_available: metrics::Histogram<u64>,
1174
    /// Acquisitions, by pool and result.
1175
    pub pool_acquisitions: metrics::Counter<u64>,
1176
    /// Reconnects, by pool.
1177
    pub reconnects: metrics::Counter<u64>,
1178
}
1179
1180
/// Ceiling on a recorded free-slot count.
1181
///
1182
/// An unbounded pool is a semaphore holding `Semaphore::MAX_PERMITS`, around
1183
/// 2.3e18. Recording that overflows the histogram's `u64` sum after a handful
1184
/// of samples, which panics a debug build. Callers pass `None` for an
1185
/// unbounded pool, and this clamp is the backstop if one ever does not: it
1186
/// sits above the largest bucket boundary, so bucket counts are unchanged.
1187
const POOL_AVAILABLE_MAX: u64 = 1024;
1188
1189
/// Records a connection being taken from `pool`.
1190
///
1191
/// `available` is the free-slot count, or `None` when the pool is unbounded
1192
/// and the figure would be meaningless. `queued` means nothing was free and
1193
/// the caller had to wait, which is the case worth alerting on.
1194
/// Clamps a free-slot count to something a histogram can accumulate.
1195
///
1196
/// Kept separate so it is directly testable: the failure this guards against
1197
/// only appears once a meter provider is installed, so a test that merely
1198
/// calls `record_connection_acquired` cannot catch it.
1199
#[must_use]
1200
391
pub fn pool_available_sample(available: usize) -> u64 {
1201
391
    u64::try_from(available)
1202
391
        .unwrap_or(POOL_AVAILABLE_MAX)
1203
391
        .min(POOL_AVAILABLE_MAX)
1204
391
}
1205
1206
426
pub fn record_connection_acquired(pool: &'static str, available: Option<usize>, queued: bool) {
1207
426
    if let Some(
available386
) = available {
1208
386
        CONNECTION_METRICS.pool_available.record(
1209
386
            pool_available_sample(available),
1210
386
            &[KeyValue::new(CONNECTION_POOL, pool)],
1211
386
        );
1212
386
    
}40
1213
426
    CONNECTION_METRICS.pool_acquisitions.add(
1214
        1,
1215
        &[
1216
426
            KeyValue::new(CONNECTION_POOL, pool),
1217
426
            KeyValue::new(
1218
                CONNECTION_RESULT,
1219
426
                if queued { 
"queued"102
} else {
"immediate"324
},
1220
            ),
1221
        ],
1222
    );
1223
426
}
1224
1225
/// Records a reconnect. A rising count means the backend is flapping, which
1226
/// on Redis usually means a failover.
1227
4
pub fn record_connection_reconnect(pool: &'static str) {
1228
4
    CONNECTION_METRICS
1229
4
        .reconnects
1230
4
        .add(1, &[KeyValue::new(CONNECTION_POOL, pool)]);
1231
4
}