1use crate::lsn::Lsn;
4use crate::state::JobState;
5use serde::Serialize;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::Instant;
8
9#[derive(Default)]
10pub struct Metrics {
11 pub events_received: AtomicU64,
12 pub bytes_received: AtomicU64,
13 pub txns_applied: AtomicU64,
14 pub txns_skipped: AtomicU64,
15 pub rows_applied: AtomicU64,
16 pub rows_copied: AtomicU64,
17 pub bytes_copied: AtomicU64,
18 pub feedback_sent: AtomicU64,
19 pub reconnects: AtomicU64,
20 pub last_received_lsn: AtomicU64,
21 pub server_wal_end: AtomicU64,
22 pub committed_lsn: AtomicU64,
23 pub acked_lsn: AtomicU64,
24 pub inflight_items: AtomicU64,
25 pub inflight_bytes: AtomicU64,
26 pub last_apply_unix_micros: AtomicU64,
28 state: AtomicU64,
29 connection: AtomicU64,
30 started: Option<Instant>,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
35#[serde(rename_all = "snake_case")]
36pub enum ConnectionState {
37 Disconnected,
39 Connected,
40 Reconnecting,
42}
43
44const CONNECTIONS: [ConnectionState; 3] = [
45 ConnectionState::Disconnected,
46 ConnectionState::Connected,
47 ConnectionState::Reconnecting,
48];
49
50#[derive(Debug, Clone, Serialize)]
51pub struct MetricsSnapshot {
52 pub state: JobState,
53 pub uptime_secs: u64,
54 pub events_received: u64,
55 pub bytes_received: u64,
56 pub txns_applied: u64,
57 pub txns_skipped: u64,
58 pub rows_applied: u64,
59 pub rows_copied: u64,
60 pub bytes_copied: u64,
61 pub feedback_sent: u64,
62 pub reconnects: u64,
63 pub last_received_lsn: Lsn,
64 pub server_wal_end: Lsn,
65 pub committed_lsn: Lsn,
66 pub acked_lsn: Lsn,
67 pub apply_lag_bytes: u64,
70 pub inflight_items: u64,
71 pub inflight_bytes: u64,
72 pub rss_bytes: Option<u64>,
73 pub last_apply_unix_micros: Option<u64>,
74 pub connection: ConnectionState,
75}
76
77const STATES: [JobState; 7] = [
78 JobState::Initializing,
79 JobState::Snapshotting,
80 JobState::CatchingUp,
81 JobState::Streaming,
82 JobState::Paused,
83 JobState::Error,
84 JobState::ResyncRequired,
85];
86
87impl Metrics {
88 pub fn new() -> Self {
89 Metrics {
90 started: Some(Instant::now()),
91 ..Default::default()
92 }
93 }
94
95 pub fn set_state(&self, s: JobState) {
96 let idx = STATES.iter().position(|x| *x == s).unwrap_or(0) as u64;
97 self.state.store(idx, Ordering::Relaxed);
98 }
99
100 pub fn state(&self) -> JobState {
101 STATES[self.state.load(Ordering::Relaxed) as usize % STATES.len()]
102 }
103
104 pub fn set_connection(&self, c: ConnectionState) {
105 let idx = CONNECTIONS.iter().position(|x| *x == c).unwrap_or(0) as u64;
106 self.connection.store(idx, Ordering::Relaxed);
107 }
108
109 pub fn connection(&self) -> ConnectionState {
110 CONNECTIONS[self.connection.load(Ordering::Relaxed) as usize % CONNECTIONS.len()]
111 }
112
113 pub fn mark_applied_now(&self) {
114 let micros = std::time::SystemTime::now()
115 .duration_since(std::time::UNIX_EPOCH)
116 .map(|d| d.as_micros() as u64)
117 .unwrap_or(0);
118 self.last_apply_unix_micros.store(micros, Ordering::Relaxed);
119 }
120
121 pub fn add(&self, counter: &AtomicU64, n: u64) {
122 counter.fetch_add(n, Ordering::Relaxed);
123 }
124
125 pub fn set_lsn(&self, slot: &AtomicU64, lsn: Lsn) {
126 slot.fetch_max(lsn.0, Ordering::Relaxed);
127 }
128
129 pub fn snapshot(&self) -> MetricsSnapshot {
130 let g = |a: &AtomicU64| a.load(Ordering::Relaxed);
131 let wal_end = g(&self.server_wal_end);
132 let committed = g(&self.committed_lsn);
133 let acked = g(&self.acked_lsn);
134 MetricsSnapshot {
135 state: self.state(),
136 uptime_secs: self.started.map(|s| s.elapsed().as_secs()).unwrap_or(0),
137 events_received: g(&self.events_received),
138 bytes_received: g(&self.bytes_received),
139 txns_applied: g(&self.txns_applied),
140 txns_skipped: g(&self.txns_skipped),
141 rows_applied: g(&self.rows_applied),
142 rows_copied: g(&self.rows_copied),
143 bytes_copied: g(&self.bytes_copied),
144 feedback_sent: g(&self.feedback_sent),
145 reconnects: g(&self.reconnects),
146 last_received_lsn: Lsn(g(&self.last_received_lsn)),
147 server_wal_end: Lsn(wal_end),
148 committed_lsn: Lsn(committed),
149 acked_lsn: Lsn(acked),
150 apply_lag_bytes: wal_end.saturating_sub(acked.max(committed)),
151 inflight_items: g(&self.inflight_items),
152 inflight_bytes: g(&self.inflight_bytes),
153 rss_bytes: process_rss_bytes(),
154 last_apply_unix_micros: match g(&self.last_apply_unix_micros) {
155 0 => None,
156 v => Some(v),
157 },
158 connection: self.connection(),
159 }
160 }
161}
162
163pub fn process_rss_bytes() -> Option<u64> {
165 #[cfg(target_os = "linux")]
166 {
167 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
168 let pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
169 return Some(pages * 4096);
170 }
171 #[cfg(target_os = "macos")]
172 {
173 let out = std::process::Command::new("ps")
175 .args(["-o", "rss=", "-p", &std::process::id().to_string()])
176 .output()
177 .ok()?;
178 let kb: u64 = String::from_utf8_lossy(&out.stdout).trim().parse().ok()?;
179 return Some(kb * 1024);
180 }
181 #[allow(unreachable_code)]
182 None
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn snapshot_reflects_counters_and_lag() {
191 let m = Metrics::new();
192 m.set_state(JobState::Streaming);
193 m.add(&m.txns_applied, 3);
194 m.set_lsn(&m.server_wal_end, Lsn(1000));
195 m.set_lsn(&m.committed_lsn, Lsn(400));
196 m.set_lsn(&m.committed_lsn, Lsn(300)); let s = m.snapshot();
198 assert_eq!(s.state, JobState::Streaming);
199 assert_eq!(s.txns_applied, 3);
200 assert_eq!(s.committed_lsn, Lsn(400));
201 assert_eq!(s.apply_lag_bytes, 600);
202 m.set_lsn(&m.acked_lsn, Lsn(900));
204 assert_eq!(m.snapshot().apply_lag_bytes, 100);
205 let json = serde_json::to_value(&s).unwrap();
206 assert_eq!(json["state"], "STREAMING");
207 }
208
209 #[test]
210 fn rss_is_reported_on_supported_platforms() {
211 if cfg!(any(target_os = "linux", target_os = "macos")) {
212 assert!(process_rss_bytes().unwrap() > 0);
213 }
214 }
215}