-
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