use crate::{
Result,
client::{Client, ClientPreparedCommand},
commands::ConnectionCommands,
network::PushReceiver,
};
use futures_util::{Stream, StreamExt};
use serde::{Deserialize, Deserializer, de};
use std::{
net::SocketAddr,
pin::Pin,
task::{Context, Poll},
};
use tracing::warn;
pub struct MonitorStream {
closed: bool,
receiver: PushReceiver,
client: Client,
}
impl MonitorStream {
pub(crate) fn new(receiver: PushReceiver, client: Client) -> Self {
Self {
closed: false,
receiver,
client,
}
}
pub async fn close(&mut self) -> Result<()> {
self.client.reset().await?;
self.closed = true;
Ok(())
}
pub fn dropped_messages(&self) -> usize {
self.receiver.dropped_messages()
}
}
impl Stream for MonitorStream {
type Item = MonitoredCommandInfo;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
if self.closed {
return Poll::Ready(None);
}
let this = self.get_mut();
loop {
let Poll::Ready(event) = this.receiver.poll_next_unpin(cx) else {
return Poll::Pending;
};
let Some(event) = event else {
return Poll::Ready(None);
};
match event {
Ok(resp_buf) => match resp_buf.to() {
Ok(info) => return Poll::Ready(Some(info)),
Err(e) => warn!("Cannot decode a monitor event: {e}"),
},
Err(e) => warn!("Error while receiving a monitor event: {e}"),
}
}
}
}
impl Drop for MonitorStream {
fn drop(&mut self) {
if self.closed {
return;
}
let _result = self.client.reset().forget();
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct MonitoredCommandInfo {
pub unix_timestamp_millis: f64,
pub database: usize,
pub server_addr: SocketAddr,
pub command: String,
pub command_args: Vec<String>,
}
impl<'de> Deserialize<'de> for MonitoredCommandInfo {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let line = <&str>::deserialize(deserializer)?;
parse_monitor_line(line).ok_or_else(|| {
de::Error::custom(format!("Cannot parse result from MONITOR event: {line}"))
})
}
}
fn parse_monitor_line(line: &str) -> Option<MonitoredCommandInfo> {
let (timestamp, rest) = line.split_once(' ')?;
let unix_timestamp_millis = timestamp.parse::<f64>().ok()?;
let rest = rest.strip_prefix('[')?;
let (bracket, rest) = rest.split_once(']')?;
let (database, server_addr) = bracket.split_once(' ')?;
let database = database.parse::<usize>().ok()?;
let server_addr = server_addr.parse::<SocketAddr>().ok()?;
let mut quoted = parse_quoted_args(rest.trim_start())?;
if quoted.is_empty() {
return None;
}
let command = quoted.remove(0);
Some(MonitoredCommandInfo {
unix_timestamp_millis,
database,
server_addr,
command,
command_args: quoted,
})
}
fn parse_quoted_args(mut s: &str) -> Option<Vec<String>> {
let mut args = Vec::new();
loop {
s = s.trim_start_matches(' ');
if s.is_empty() {
return Some(args);
}
let after_open = s.strip_prefix('"')?;
let (value, rest) = decode_quoted(after_open)?;
args.push(value);
s = rest;
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "`i` only advances over bytes the `get` calls found, so it stays an \
offset into the slice, and `hi * 16 + lo` is at most 255 for two hex \
digits."
)]
fn decode_quoted(s: &str) -> Option<(String, &str)> {
let bytes = s.as_bytes();
let mut out: Vec<u8> = Vec::new();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'"' => {
let value = String::from_utf8_lossy(&out).into_owned();
return Some((value, &s[i + 1..]));
}
b'\\' => {
let esc = *bytes.get(i + 1)?;
match esc {
b'"' => out.push(b'"'),
b'\\' => out.push(b'\\'),
b'n' => out.push(b'\n'),
b'r' => out.push(b'\r'),
b't' => out.push(b'\t'),
b'b' => out.push(0x08),
b'a' => out.push(0x07),
b'x' => {
let hi = (*bytes.get(i + 2)? as char).to_digit(16)?;
let lo = (*bytes.get(i + 3)? as char).to_digit(16)?;
out.push((hi * 16 + lo) as u8);
i += 2; }
other => out.push(other),
}
i += 2;
}
other => {
out.push(other);
i += 1;
}
}
}
None
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::indexing_slicing,
reason = "test code: a panic is how a test reports failure"
)]
use super::parse_monitor_line;
#[test]
fn plain_command() {
let info = parse_monitor_line("1339518083.107412 [0 127.0.0.1:60866] \"keys\" \"*\"")
.expect("should parse");
assert_eq!(0, info.database);
assert_eq!("keys", info.command);
assert_eq!(vec!["*".to_string()], info.command_args);
}
#[test]
fn argument_with_spaces_is_not_split() {
let info = parse_monitor_line("1.0 [0 127.0.0.1:6379] \"SET\" \"k\" \"a b c\"")
.expect("should parse");
assert_eq!("SET", info.command);
assert_eq!(
vec!["k".to_string(), "a b c".to_string()],
info.command_args
);
}
#[test]
fn double_space_argument_does_not_panic() {
let info = parse_monitor_line("1.0 [0 127.0.0.1:6379] \"SET\" \"k\" \"a b\"")
.expect("should parse");
assert_eq!(vec!["k".to_string(), "a b".to_string()], info.command_args);
}
#[test]
fn escapes_are_decoded() {
let info =
parse_monitor_line("1.0 [0 127.0.0.1:6379] \"SET\" \"k\" \"a\\tb\\n\\x41\\\"c\"")
.expect("should parse");
assert_eq!(
vec!["k".to_string(), "a\tb\nA\"c".to_string()],
info.command_args
);
}
#[test]
fn malformed_returns_none_not_panic() {
assert!(parse_monitor_line("").is_none());
assert!(parse_monitor_line("notanumber [0 127.0.0.1:6379] \"PING\"").is_none());
assert!(parse_monitor_line("1.0 [0 127.0.0.1:6379] \"unterminated").is_none());
}
}