oxur_cli/repl/
metrics.rs

1//! Client-side metrics for REPL operations
2//!
3//! Provides [`MetricsClientAdapter`] wrapper that adds metrics collection
4//! around any [`ReplClientAdapter`] implementation.
5
6#[cfg(feature = "binary")]
7use crate::repl::runner::ReplClientAdapter;
8#[cfg(feature = "binary")]
9use anyhow::Result;
10#[cfg(feature = "binary")]
11use async_trait::async_trait;
12#[cfg(feature = "binary")]
13use metrics::{counter, histogram};
14#[cfg(feature = "binary")]
15use oxur_repl::protocol::{OperationResult, Request, Response};
16#[cfg(feature = "binary")]
17use std::time::Instant;
18
19/// Wrapper that adds metrics collection to any [`ReplClientAdapter`].
20///
21/// Records:
22/// - `repl.client.requests_total` - Requests sent (labeled by operation)
23/// - `repl.client.responses_total` - Responses received (labeled by status)
24/// - `repl.client.response_latency_ms` - Round-trip latency histogram
25///
26/// # Example
27///
28/// ```no_run
29/// use oxur_cli::repl::metrics::MetricsClientAdapter;
30///
31/// // Wrap an existing adapter
32/// // let inner_adapter = TcpAdapter::new(...);
33/// // let metrics_adapter = MetricsClientAdapter::new(inner_adapter);
34/// // runner.run(&mut metrics_adapter).await?;
35/// ```
36#[cfg(feature = "binary")]
37#[derive(Debug)]
38#[allow(dead_code)] // Optional infrastructure - not yet wired into connect.rs
39pub struct MetricsClientAdapter<C: ReplClientAdapter> {
40    inner: C,
41    request_start: Option<Instant>,
42}
43
44#[cfg(feature = "binary")]
45#[allow(dead_code)] // Optional infrastructure - not yet wired into connect.rs
46impl<C: ReplClientAdapter> MetricsClientAdapter<C> {
47    /// Create a new metrics-enabled adapter wrapping an inner adapter.
48    pub fn new(inner: C) -> Self {
49        Self { inner, request_start: None }
50    }
51
52    /// Unwrap and return the inner adapter.
53    pub fn into_inner(self) -> C {
54        self.inner
55    }
56}
57
58#[cfg(feature = "binary")]
59#[async_trait]
60impl<C: ReplClientAdapter> ReplClientAdapter for MetricsClientAdapter<C> {
61    async fn send_eval(&mut self, request: Request) -> Result<()> {
62        // Record request start time
63        self.request_start = Some(Instant::now());
64
65        // Extract operation name for metrics label
66        let operation = match &request.operation {
67            oxur_repl::protocol::Operation::CreateSession { .. } => "create_session",
68            oxur_repl::protocol::Operation::Clone { .. } => "clone",
69            oxur_repl::protocol::Operation::Eval { .. } => "eval",
70            oxur_repl::protocol::Operation::Close => "close",
71            oxur_repl::protocol::Operation::LsSessions => "ls_sessions",
72            oxur_repl::protocol::Operation::LoadFile { .. } => "load_file",
73            oxur_repl::protocol::Operation::Interrupt => "interrupt",
74            oxur_repl::protocol::Operation::Describe { .. } => "describe",
75            oxur_repl::protocol::Operation::History { .. } => "history",
76            oxur_repl::protocol::Operation::ClearOutput => "clear_output",
77            _ => "unknown",
78        };
79
80        counter!("repl.client.requests_total", "operation" => operation).increment(1);
81
82        // Delegate to inner adapter
83        self.inner.send_eval(request).await
84    }
85
86    async fn recv_response(&mut self) -> Result<Response> {
87        let response = self.inner.recv_response().await?;
88
89        // Record latency if we have a start time
90        if let Some(start) = self.request_start.take() {
91            let latency_ms = start.elapsed().as_millis() as f64;
92            histogram!("repl.client.response_latency_ms").record(latency_ms);
93        }
94
95        // Record response status
96        let status = match &response.result {
97            OperationResult::Success { .. } => "success",
98            OperationResult::Error { .. } => "error",
99            OperationResult::Sessions { .. } => "success",
100            OperationResult::HistoryEntries { .. } => "success",
101            _ => "unknown",
102        };
103        counter!("repl.client.responses_total", "status" => status).increment(1);
104
105        Ok(response)
106    }
107
108    async fn close(&mut self) -> Result<()> {
109        self.inner.close().await
110    }
111
112    fn current_session(&self) -> &oxur_repl::protocol::SessionId {
113        self.inner.current_session()
114    }
115
116    async fn handle_special_command(&mut self, input: &str, color_enabled: bool) -> Option<String> {
117        self.inner.handle_special_command(input, color_enabled).await
118    }
119}
120
121#[cfg(test)]
122#[cfg(feature = "binary")]
123mod tests {
124    // Unit tests would require mocking ReplClientAdapter
125    // Integration tests are more appropriate for this wrapper
126}