1#[cfg(feature = "runtime_support")]
5use std::marker::Unpin;
6#[cfg(feature = "runtime_support")]
7use std::panic::AssertUnwindSafe;
8use std::time::Duration;
9#[cfg(feature = "runtime_support")]
10use std::time::SystemTime;
11
12#[cfg(feature = "runtime_support")]
13use dfir_rs::Never;
14#[cfg(feature = "runtime_support")]
15use dfir_rs::scheduled::metrics::{DfirMetrics, DfirMetricsIntervals};
16#[cfg(feature = "runtime_support")]
17use futures::FutureExt;
18use quote::quote;
19#[cfg(feature = "runtime_support")]
20use serde_json::json;
21use syn::parse_quote;
22#[cfg(feature = "runtime_support")]
23use tokio::io::{AsyncWrite, AsyncWriteExt};
24#[cfg(feature = "runtime_support")]
25use tokio_metrics::RuntimeMetrics;
26
27use crate::location::{LocationKey, LocationType};
28use crate::staging_util::get_this_crate;
29use crate::telemetry::Sidecar;
30
31pub const DEFAULT_FILE_PATH: &str = "/var/log/hydro/metrics.log";
33pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(30);
35
36pub struct RecordMetricsSidecar {
38 file_path: String,
39 interval: Duration,
40}
41
42#[buildstructor::buildstructor]
43impl RecordMetricsSidecar {
44 #[builder]
46 pub fn new(file_path: Option<String>, interval: Option<Duration>) -> Self {
47 Self {
48 file_path: file_path.unwrap_or_else(|| DEFAULT_FILE_PATH.to_owned()),
49 interval: interval.unwrap_or(DEFAULT_INTERVAL),
50 }
51 }
52}
53
54impl Sidecar for RecordMetricsSidecar {
55 fn to_expr(
56 &self,
57 flow_name: &str,
58 _location_key: LocationKey,
59 _location_type: LocationType,
60 location_name: &str,
61 dfir_ident: &syn::Ident,
62 ) -> syn::Expr {
63 let Self {
64 file_path,
65 interval,
66 } = self;
67
68 let root = get_this_crate();
69 let namespace = flow_name.replace(char::is_whitespace, "_");
70 let interval: proc_macro2::TokenStream = {
71 let secs = interval.as_secs();
72 let nanos = interval.subsec_nanos();
73 quote!(::std::time::Duration::new(#secs, #nanos))
74 };
75
76 parse_quote! {
77 #root::telemetry::emf::record_metrics_sidecar(#dfir_ident.metrics_intervals(), #namespace, #location_name, #file_path, #interval)
78 }
79 }
80
81 #[cfg(feature = "build")]
83 fn edit_as_code_options(&self, options: &mut dfir_lang::graph::AsCodeOptions) {
84 options.include_metrics_tracking = true;
85 }
86}
87
88#[cfg(feature = "runtime_support")]
90#[doc(hidden)]
91pub fn record_metrics_sidecar(
92 mut dfir_intervals: DfirMetricsIntervals,
93 namespace: &'static str,
94 location_name: &'static str,
95 file_path: &'static str,
96 interval: Duration,
97) -> impl 'static + Future<Output = Never> {
98 assert!(!namespace.contains(char::is_whitespace));
99
100 async move {
101 if let Some(parent_dir) = std::path::Path::new(file_path).parent()
103 && let Err(e) = tokio::fs::create_dir_all(parent_dir).await
104 {
105 eprintln!("Failed to create log file directory for EMF metrics: {}", e);
107 }
108
109 let rt_monitor = tokio_metrics::RuntimeMonitor::new(&tokio::runtime::Handle::current());
111 let mut rt_intervals = rt_monitor.intervals();
112
113 loop {
114 let _ = tokio::time::sleep(interval).await;
115
116 let dfir_metrics = dfir_intervals.take_interval();
117 let rt_metrics = rt_intervals.next().unwrap();
118
119 let unwind_result = AssertUnwindSafe(async {
120 let timestamp = SystemTime::now();
121
122 let file = tokio::fs::OpenOptions::new()
123 .write(true)
124 .create(true)
125 .truncate(false)
126 .append(true)
127 .open(file_path)
128 .await
129 .expect("Failed to open log file for EMF metrics.");
130 let mut writer = tokio::io::BufWriter::new(file);
131
132 record_metrics_dfir(
133 namespace,
134 location_name,
135 timestamp,
136 dfir_metrics,
137 &mut writer,
138 )
139 .await
140 .unwrap();
141
142 record_metrics_tokio(namespace, location_name, timestamp, rt_metrics, &mut writer)
143 .await
144 .unwrap();
145
146 writer.shutdown().await.unwrap();
147 })
148 .catch_unwind()
149 .await;
150
151 if let Err(panic_reason) = unwind_result {
152 eprintln!("Panic in metrics sidecar: {panic_reason:?}");
154 }
155 }
156 }
157}
158
159#[cfg(feature = "runtime_support")]
160async fn record_metrics_dfir<W>(
162 namespace: &str,
163 location_name: &str,
164 timestamp: SystemTime,
165 metrics: DfirMetrics,
166 writer: &mut W,
167) -> Result<(), std::io::Error>
168where
169 W: AsyncWrite + Unpin,
170{
171 let ts_millis = timestamp
172 .duration_since(SystemTime::UNIX_EPOCH)
173 .unwrap()
174 .as_millis();
175
176 for (hoff_id, hoff_metrics) in metrics.handoffs.iter() {
178 let emf = json!({
179 "_aws": {
180 "Timestamp": ts_millis,
181 "CloudWatchMetrics": [
182 {
183 "Namespace": namespace,
184 "Dimensions": [["LocationName"], ["LocationName", "HandoffId"]],
185 "Metrics": [
186 {"Name": "CurrItemsCount", "Unit": Unit::Count},
187 {"Name": "TotalItemsCount", "Unit": Unit::Count},
188 ]
189 }
190 ]
191 },
192 "LocationName": location_name,
193 "HandoffId": format!("{:?}", hoff_id),
194 "CurrItemsCount": hoff_metrics.curr_items_count(),
195 "TotalItemsCount": hoff_metrics.total_items_count(),
196 })
197 .to_string();
198 writer.write_all(emf.as_bytes()).await?;
199 writer.write_u8(b'\n').await?;
200 }
201
202 for (sg_id, sg_metrics) in metrics.subgraphs.iter() {
204 let emf = json!({
205 "_aws": {
206 "Timestamp": ts_millis,
207 "CloudWatchMetrics": [
208 {
209 "Namespace": namespace,
210 "Dimensions": [["LocationName"], ["LocationName", "SubgraphId"]],
211 "Metrics": [
212 {"Name": "TotalRunCount", "Unit": Unit::Count},
213 {"Name": "TotalPollDuration", "Unit": Unit::Microseconds},
214 {"Name": "TotalPollCount", "Unit": Unit::Count},
215 {"Name": "TotalIdleDuration", "Unit": Unit::Microseconds},
216 {"Name": "TotalIdleCount", "Unit": Unit::Count},
217 ]
218 }
219 ]
220 },
221 "LocationName": location_name,
222 "SubgraphId": format!("{:?}", sg_id),
223 "TotalRunCount": sg_metrics.total_run_count(),
224 "TotalPollDuration": sg_metrics.total_poll_duration().as_micros(),
225 "TotalPollCount": sg_metrics.total_poll_count(),
226 "TotalIdleDuration": sg_metrics.total_idle_duration().as_micros(),
227 "TotalIdleCount": sg_metrics.total_idle_count(),
228 })
229 .to_string();
230 writer.write_all(emf.as_bytes()).await?;
231 writer.write_u8(b'\n').await?;
232 }
233
234 Ok(())
235}
236
237#[cfg(feature = "runtime_support")]
238async fn record_metrics_tokio<W>(
240 namespace: &str,
241 location_name: &str,
242 timestamp: SystemTime,
243 rt_metrics: RuntimeMetrics,
244 writer: &mut W,
245) -> Result<(), std::io::Error>
246where
247 W: AsyncWrite + Unpin,
248{
249 let ts_millis = timestamp
250 .duration_since(SystemTime::UNIX_EPOCH)
251 .unwrap()
252 .as_millis();
253
254 let emf = json!({
256 "_aws": {
257 "Timestamp": ts_millis,
258 "CloudWatchMetrics": [
259 {
260 "Namespace": namespace,
261 "Dimensions": [["LocationName"]],
262 "Metrics": [
263 {"Name": "TotalBusyDuration", "Unit": Unit::Microseconds},
265 {"Name": "GlobalQueueDepth", "Unit": Unit::Count},
266 ]
267 }
268 ]
269 },
270 "LocationName": location_name,
271 "TotalBusyDuration": rt_metrics.total_busy_duration.as_micros(),
273 "GlobalQueueDepth": rt_metrics.global_queue_depth,
274 })
276 .to_string();
277 writer.write_all(emf.as_bytes()).await?;
278 writer.write_u8(b'\n').await?;
279
280 Ok(())
281}
282
283#[expect(missing_docs, reason = "self-explanatory")]
287#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
288pub enum Unit {
289 #[default]
291 None,
292 Seconds,
293 Microseconds,
294 Milliseconds,
295 Bytes,
296 Kilobytes,
297 Megabytes,
298 Gigabytes,
299 Terabytes,
300 Bits,
301 Kilobits,
302 Megabits,
303 Gigabits,
304 Terabits,
305 Percent,
306 Count,
307 #[serde(rename = "Bytes/Second")]
308 BytesPerSecond,
309 #[serde(rename = "Kilobytes/Second")]
310 KilobytesPerSecond,
311 #[serde(rename = "Megabytes/Second")]
312 MegabytesPerSecond,
313 #[serde(rename = "Gigabytes/Second")]
314 GigabytesPerSecond,
315 #[serde(rename = "Terabytes/Second")]
316 TerabytesPerSecond,
317 #[serde(rename = "Bits/Second")]
318 BitsPerSecond,
319 #[serde(rename = "Kilobits/Second")]
320 KilobitsPerSecond,
321 #[serde(rename = "Megabits/Second")]
322 MegabitsPerSecond,
323 #[serde(rename = "Gigabits/Second")]
324 GigabitsPerSecond,
325 #[serde(rename = "Terabits/Second")]
326 TerabitsPerSecond,
327 #[serde(rename = "Count/Second")]
328 CountPerSecond,
329}