-
Notifications
You must be signed in to change notification settings - Fork 0
Adding a New Protocol
lex edited this page May 28, 2026
·
1 revision
This guide walks you through adding support for a new wire protocol to Ocular.
Adding a protocol requires changes in 4 files (all in crates/ocular-protocol/) plus tests. No changes needed in the proxy, capture, or TUI crates — the ProtocolHandler trait abstracts everything.
Create crates/ocular-protocol/src/yourprotocol.rs:
/// Parse a request buffer into a human-readable command string. /// Return None if the buffer doesn't contain a complete parseable request. pub fn parse_request(buf: &[u8]) -> Option<String> { // Example: parse a text-based protocol let text = std::str::from_utf8(buf).ok()?; let line = text.lines().next()?; Some(line.to_string()) } /// Parse a response buffer into a human-readable summary. /// Return None if unparseable. pub fn parse_response(buf: &[u8]) -> Option<String> { let text = std::str::from_utf8(buf).ok()?; Some(text.lines().next()?.to_string()) } /// Format the full response for the detail pane. /// Return None to use the default (raw bytes as UTF-8). pub fn format_response_detail(buf: &[u8]) -> Option<String> { Some(String::from_utf8_lossy(buf).to_string()) } /// Extract the full command string for display and filtering. /// For SQL-like protocols, this is the full query text. pub fn extract_full_command(buf: &[u8]) -> Option<String> { parse_request(buf) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_simple_request() { let buf = b"GET key1\r\n"; assert_eq!(parse_request(buf), Some("GET key1".to_string())); } #[test] fn test_parse_incomplete_request() { let buf = b"GET ke"; assert_eq!(parse_request(buf), None); } }
In crates/ocular-protocol/src/handlers.rs, add:
use crate::yourprotocol; pub struct YourProtocolHandler; impl ProtocolHandler for YourProtocolHandler { fn parse_request(&self, buf: &[u8]) -> Option<String> { yourprotocol::parse_request(buf) } fn parse_response(&self, buf: &[u8]) -> Option<String> { yourprotocol::parse_response(buf) } fn format_response_detail(&self, buf: &[u8]) -> Option<String> { yourprotocol::format_response_detail(buf) } fn extract_full_command(&self, buf: &[u8]) -> Option<String> { yourprotocol::extract_full_command(buf) } fn default_port(&self) -> u16 { 12345 // The well-known port for your protocol } // If your protocol needs multi-packet buffering: // fn needs_request_buffering(&self) -> bool { true } // fn needs_response_buffering(&self) -> bool { true } // fn request_complete(&self, buf: &[u8]) -> bool { /* check delimiter */ } // fn response_complete(&self, buf: &[u8]) -> bool { /* check length header */ } // If your protocol has a handshake (like MySQL auth): // fn capture_handshake(&self, buf: &[u8]) -> Option<HandshakeAction> { // Some(HandshakeAction::Done) // } }
In crates/ocular-protocol/src/lib.rs:
pub mod yourprotocol;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Protocol { Redis, Mysql, Postgres, Amqp, Mongodb, Memcached, Kafka, Http, YourProtocol, // ← new }
impl Protocol { pub fn from_str(s: &str) -> Option<Self> { match s.to_lowercase().as_str() { "redis" | "resp" => Some(Self::Redis), "mysql" => Some(Self::Mysql), // ... "yourprotocol" | "yp" => Some(Self::YourProtocol), // ← new (add alias) _ => None, } } }
pub fn get_handler(protocol: Protocol) -> Box<dyn ProtocolHandler> { match protocol { Protocol::Redis => Box::new(handlers::RedisHandler), // ... Protocol::YourProtocol => Box::new(handlers::YourProtocolHandler), // ← new } }
Most text-based protocols (line-delimited) don't need special buffering. But if your protocol:
-
Uses length-prefixed messages (like Kafka, MySQL): implement
message_length()so capture mode knows where one message ends and the next begins -
Has multi-packet responses (large result sets): implement
needs_response_buffering() = trueandresponse_complete() -
Has a handshake phase (like MySQL auth): implement
capture_handshake()to tell capture mode when to start parsing data
Example for a length-prefixed protocol:
fn message_length(&self, buf: &[u8]) -> Option<usize> { if buf.len() < 4 { return None; } let len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize; Some(4 + len) // header + body }
cargo test -p ocular-protocol# Start the service docker run -d --rm -p 12345:12345 yourservice/yourimage # Test with Ocular ocular proxy yourprotocol 127.0.0.1:12345
- Parser module created with
parse_request,parse_response,format_response_detail,extract_full_command -
ProtocolHandlertrait implemented - Protocol registered in
lib.rs(module, enum, from_str, get_handler) - Unit tests for parser edge cases (empty buffer, incomplete message, malformed data)
-
default_port()returns the well-known port - Buffering methods implemented if needed
-
cargo testpasses - Manual test with real service via
ocular proxy