flare_core/common/message/
middleware.rs1use super::pipeline::{MessageContext, MessageMiddleware};
6use crate::common::error::Result;
7use crate::common::platform::wall_clock_ms;
8use crate::common::protocol::Frame;
9use async_trait::async_trait;
10use std::sync::Arc;
11use tracing::{debug, info, warn};
12
13pub struct LoggingMiddleware {
17 name: String,
18 log_level: LogLevel,
19}
20
21#[derive(Clone, Copy)]
22pub enum LogLevel {
23 Debug,
24 Info,
25 Warn,
26}
27
28impl LoggingMiddleware {
29 pub fn new(name: impl Into<String>) -> Self {
31 Self {
32 name: name.into(),
33 log_level: LogLevel::Info,
34 }
35 }
36
37 pub fn with_level(mut self, level: LogLevel) -> Self {
39 self.log_level = level;
40 self
41 }
42}
43
44#[async_trait]
45impl MessageMiddleware for LoggingMiddleware {
46 async fn before(&self, ctx: &MessageContext) -> Result<Option<Frame>> {
47 match self.log_level {
48 LogLevel::Debug => {
49 debug!(
50 connection_id = ?ctx.connection_id,
51 message_id = %ctx.frame.message_id,
52 "Processing message"
53 );
54 }
55 LogLevel::Info => {
56 info!(
57 connection_id = ?ctx.connection_id,
58 message_id = %ctx.frame.message_id,
59 "Processing message"
60 );
61 }
62 LogLevel::Warn => {
63 warn!(
64 connection_id = ?ctx.connection_id,
65 message_id = %ctx.frame.message_id,
66 "Processing message"
67 );
68 }
69 }
70 Ok(None)
71 }
72
73 fn name(&self) -> &str {
74 &self.name
75 }
76
77 fn priority(&self) -> u32 {
78 10 }
80}
81
82pub struct MetricsMiddleware {
86 name: String,
87}
88
89impl MetricsMiddleware {
90 pub fn new(name: impl Into<String>) -> Self {
92 Self { name: name.into() }
93 }
94}
95
96#[async_trait]
97impl MessageMiddleware for MetricsMiddleware {
98 async fn before(&self, ctx: &MessageContext) -> Result<Option<Frame>> {
99 ctx.set_metadata(
101 "start_time".to_string(),
102 wall_clock_ms().to_le_bytes().to_vec(),
103 )
104 .await;
105 Ok(None)
106 }
107
108 async fn after(&self, ctx: &MessageContext, response: Option<Frame>) -> Result<Option<Frame>> {
109 if let Some(start_bytes) = ctx.get_metadata("start_time").await {
111 let start_ms = u64::from_le_bytes(start_bytes.try_into().unwrap_or([0; 8]));
112 let duration_ms = wall_clock_ms().saturating_sub(start_ms);
113
114 debug!(
115 connection_id = ?ctx.connection_id,
116 message_id = %ctx.frame.message_id,
117 duration_ms,
118 "Message processed"
119 );
120 }
121 Ok(response)
122 }
123
124 fn name(&self) -> &str {
125 &self.name
126 }
127
128 fn priority(&self) -> u32 {
129 20
130 }
131}
132
133pub struct ValidationMiddleware {
137 name: String,
138 #[allow(clippy::type_complexity)]
139 validator: Arc<dyn Fn(&Frame) -> Result<()> + Send + Sync>,
140}
141
142impl ValidationMiddleware {
143 pub fn new<F>(name: impl Into<String>, validator: F) -> Self
145 where
146 F: Fn(&Frame) -> Result<()> + Send + Sync + 'static,
147 {
148 Self {
149 name: name.into(),
150 validator: Arc::new(validator),
151 }
152 }
153}
154
155#[async_trait]
156impl MessageMiddleware for ValidationMiddleware {
157 async fn before(&self, ctx: &MessageContext) -> Result<Option<Frame>> {
158 (self.validator)(&ctx.frame)?;
159 Ok(None)
160 }
161
162 fn name(&self) -> &str {
163 &self.name
164 }
165
166 fn priority(&self) -> u32 {
167 5 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::common::MessageParser;
175 use crate::common::platform::wall_clock_ms;
176 use crate::common::protocol::{FrameBuilder, Reliability, ping};
177
178 #[tokio::test]
179 async fn metrics_middleware_records_absolute_start_time_millis() {
180 let frame = FrameBuilder::new()
181 .with_command(crate::common::protocol::Command {
182 r#type: Some(
183 crate::common::protocol::flare::core::commands::command::Type::System(ping()),
184 ),
185 })
186 .with_reliability(Reliability::BestEffort)
187 .build();
188 let ctx = MessageContext::new(frame, Some("conn-1".to_string()), MessageParser::json());
189 let metrics = MetricsMiddleware::new("metrics");
190
191 let before = wall_clock_ms();
192 metrics.before(&ctx).await.expect("before should succeed");
193 let after = wall_clock_ms();
194
195 let start_bytes = ctx
196 .get_metadata("start_time")
197 .await
198 .expect("metrics should store start_time");
199 let start_ms = u64::from_le_bytes(
200 start_bytes
201 .try_into()
202 .expect("start_time should be a u64 millis value"),
203 );
204
205 assert!(
206 (before..=after).contains(&start_ms),
207 "start_time should be an absolute wall-clock millis timestamp, got {start_ms}, expected within {before}..={after}"
208 );
209 }
210}