Skip to main content

dk_engine/
metrics.rs

1//! In-process counters and gauges for workspace lifecycle (Epic B).
2//!
3//! Follows the same pattern as `dk-protocol/src/metrics.rs`: per-label counters
4//! backed by `tracing::info!` events with a `metric` field so log-based
5//! aggregators can surface them.  Each labeled counter family is stored in a
6//! `DashMap<String, AtomicU64>` so a future Prometheus exporter can iterate
7//! over all observed label values.  The gauge remains a plain `AtomicI64`
8//! because it has no label dimension.
9
10use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
11use std::sync::LazyLock;
12
13use dashmap::DashMap;
14
15// ── Labeled counter families ──────────────────────────────────────────────────
16
17/// Workspaces skipped by GC because the pin rule applied, keyed by `reason`.
18static WORKSPACE_PINNED: LazyLock<DashMap<String, AtomicU64>> =
19    LazyLock::new(DashMap::new);
20
21/// Workspaces transitioned to stranded state, keyed by `reason`.
22static WORKSPACE_STRANDED: LazyLock<DashMap<String, AtomicU64>> =
23    LazyLock::new(DashMap::new);
24
25/// Resume attempts, keyed by `outcome`.
26static WORKSPACE_RESUMED: LazyLock<DashMap<String, AtomicU64>> =
27    LazyLock::new(DashMap::new);
28
29/// Workspaces permanently abandoned, keyed by `reason`.
30static WORKSPACE_ABANDONED: LazyLock<DashMap<String, AtomicU64>> =
31    LazyLock::new(DashMap::new);
32
33// ── Gauge ─────────────────────────────────────────────────────────────────────
34
35/// Rows where `stranded_at IS NOT NULL AND abandoned_at IS NULL`.
36static WORKSPACE_STRANDED_ACTIVE: AtomicI64 = AtomicI64::new(0);
37
38// ── Private helper ────────────────────────────────────────────────────────────
39
40#[inline]
41fn incr_labeled(map: &DashMap<String, AtomicU64>, label: &str) {
42    map.entry(label.to_string())
43        .or_insert_with(|| AtomicU64::new(0))
44        .fetch_add(1, Ordering::Relaxed);
45}
46
47// ── Public helpers ────────────────────────────────────────────────────────────
48
49/// Increment "workspace pinned" counter for the given `reason` label.
50pub fn incr_workspace_pinned(reason: &str) {
51    incr_labeled(&WORKSPACE_PINNED, reason);
52    tracing::info!(
53        metric = "dkod_workspace_pinned_total",
54        reason,
55        increment = 1,
56        "metrics counter"
57    );
58}
59
60/// Increment "workspace stranded" counter for the given `reason` label.
61pub fn incr_workspace_stranded(reason: &str) {
62    incr_labeled(&WORKSPACE_STRANDED, reason);
63    tracing::info!(
64        metric = "dkod_workspace_stranded_total",
65        reason,
66        increment = 1,
67        "metrics counter"
68    );
69}
70
71/// Increment "workspace resumed" counter for the given `outcome` label.
72pub fn incr_workspace_resumed(outcome: &str) {
73    incr_labeled(&WORKSPACE_RESUMED, outcome);
74    tracing::info!(
75        metric = "dkod_workspace_resumed_total",
76        outcome,
77        increment = 1,
78        "metrics counter"
79    );
80}
81
82/// Increment "workspace abandoned" counter for the given `reason` label.
83pub fn incr_workspace_abandoned(reason: &str) {
84    incr_labeled(&WORKSPACE_ABANDONED, reason);
85    tracing::info!(
86        metric = "dkod_workspace_abandoned_total",
87        reason,
88        increment = 1,
89        "metrics counter"
90    );
91}
92
93/// Set the stranded-active gauge to `n`
94/// (`COUNT(*) WHERE stranded_at IS NOT NULL AND abandoned_at IS NULL`).
95pub fn set_workspace_stranded_active(n: i64) {
96    WORKSPACE_STRANDED_ACTIVE.store(n, Ordering::Relaxed);
97    tracing::info!(
98        metric = "dkod_workspace_stranded_active",
99        value = n,
100        "metrics gauge"
101    );
102}
103
104// ── Snapshot helpers (tests + future scrape) ──────────────────────────────────
105
106/// Total pinned events across all reason labels.
107pub fn workspace_pinned_total() -> u64 {
108    WORKSPACE_PINNED
109        .iter()
110        .map(|e| e.value().load(Ordering::Relaxed))
111        .sum()
112}
113
114/// Total stranded events across all reason labels.
115pub fn workspace_stranded_total() -> u64 {
116    WORKSPACE_STRANDED
117        .iter()
118        .map(|e| e.value().load(Ordering::Relaxed))
119        .sum()
120}
121
122/// Total resumed events across all outcome labels.
123pub fn workspace_resumed_total() -> u64 {
124    WORKSPACE_RESUMED
125        .iter()
126        .map(|e| e.value().load(Ordering::Relaxed))
127        .sum()
128}
129
130/// Total abandoned events across all reason labels.
131pub fn workspace_abandoned_total() -> u64 {
132    WORKSPACE_ABANDONED
133        .iter()
134        .map(|e| e.value().load(Ordering::Relaxed))
135        .sum()
136}
137
138pub fn workspace_stranded_active() -> i64 {
139    WORKSPACE_STRANDED_ACTIVE.load(Ordering::Relaxed)
140}