listen on udp and tcp for json logs as well
This commit is contained in:
@@ -61,10 +61,12 @@ impl MessageHandler for AdminHttpHandler {
|
|||||||
.map_err(|err| (502u16, format!("HTTP request failed: {err}").into()))?;
|
.map_err(|err| (502u16, format!("HTTP request failed: {err}").into()))?;
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let body = response
|
let body = response.text().await.map_err(|err| {
|
||||||
.text()
|
(
|
||||||
.await
|
502u16,
|
||||||
.map_err(|err| (502u16, format!("Failed to read response body: {err}").into()))?;
|
format!("Failed to read response body: {err}").into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err((status.as_u16(), body.into()));
|
return Err((status.as_u16(), body.into()));
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ impl MessageHandler for AdminNetcatHandler {
|
|||||||
_context: &dyn Context,
|
_context: &dyn Context,
|
||||||
) -> MessageHandlerResult {
|
) -> MessageHandlerResult {
|
||||||
// Connect to server
|
// Connect to server
|
||||||
let mut stream =
|
let mut stream = timeout(
|
||||||
timeout(self.config.timeout, TcpStream::connect(&self.config.tcp_addr))
|
self.config.timeout,
|
||||||
|
TcpStream::connect(&self.config.tcp_addr),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| (504u16, "connecting: timeout".into()))?
|
.map_err(|_| (504u16, "connecting: timeout".into()))?
|
||||||
.map_err(|err| (502u16, format!("connecting: {err}").into()))?;
|
.map_err(|err| (502u16, format!("connecting: {err}").into()))?;
|
||||||
|
|||||||
+1
-3
@@ -17,9 +17,7 @@ use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
|
|||||||
/// This parser tracks brace and bracket depth to allow newlines within
|
/// This parser tracks brace and bracket depth to allow newlines within
|
||||||
/// JSON objects and arrays. A newline only ends the value when we're at
|
/// JSON objects and arrays. A newline only ends the value when we're at
|
||||||
/// depth 0 (outside any object or array).
|
/// depth 0 (outside any object or array).
|
||||||
pub async fn read_json_lines_value<R>(
|
pub async fn read_json_lines_value<R>(reader: &mut BufReader<R>) -> std::io::Result<Option<Vec<u8>>>
|
||||||
reader: &mut BufReader<R>,
|
|
||||||
) -> std::io::Result<Option<Vec<u8>>>
|
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
{
|
{
|
||||||
@@ -20,17 +20,37 @@ use tracing::{error, info, trace};
|
|||||||
mod json_lines;
|
mod json_lines;
|
||||||
use json_lines::read_json_lines_value;
|
use json_lines::read_json_lines_value;
|
||||||
|
|
||||||
/// Configuration for the JSON listener (UDP and TCP).
|
/// Configuration for the JSON log message listener.
|
||||||
|
///
|
||||||
|
/// Listens for JSON log messages on both UDP and TCP using the same address.
|
||||||
|
///
|
||||||
|
/// See [`JsonLogMessage`] for the JSON schema.
|
||||||
#[derive(Clone, Conf, Debug)]
|
#[derive(Clone, Conf, Debug)]
|
||||||
pub struct UdpJsonConfig {
|
pub struct JsonConfig {
|
||||||
/// Socket to listen for JSON log messages.
|
/// Socket address to listen for JSON log messages.
|
||||||
/// Both UDP and TCP listeners are started on this address.
|
/// Both UDP and TCP listeners are started on this address.
|
||||||
/// TCP uses relaxed JSON Lines format (newlines allowed within objects).
|
///
|
||||||
|
/// - **UDP**: Each datagram should contain a single JSON object.
|
||||||
|
/// - **TCP**: Uses a relaxed JSON Lines format where each JSON object is
|
||||||
|
/// separated by newlines.
|
||||||
|
///
|
||||||
|
/// See [`JsonLogMessage`] for the JSON schema. It is roughly compatible
|
||||||
|
/// with logstash, graylog, etc.
|
||||||
|
///
|
||||||
|
/// * "message", "Message", "msg" for the log mesage string itself
|
||||||
|
/// * "timestamp", "@timestamp", "time" for the timestamp, which can be a numeric unix typestamp,
|
||||||
|
/// or a string in RFC3339 form
|
||||||
|
/// * "level", "severity" for the log level string
|
||||||
|
/// * "host" or "hostname" for the originating host
|
||||||
|
/// * "app" or "appname" or "application" or "service" for the originating program
|
||||||
|
/// * "file" or "filename" or "source_file" for the source file that wrote the log line
|
||||||
|
/// * "line" or "lineno" for the source file line number that wrote the log line
|
||||||
|
/// * "module" or "module_path" or "logger" or "logger_name" for the source module that wrote the log line
|
||||||
#[conf(long, env)]
|
#[conf(long, env)]
|
||||||
pub listen_addr: SocketAddr,
|
pub listen_addr: SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UdpJsonConfig {
|
impl JsonConfig {
|
||||||
/// Bind UDP and TCP sockets and start background tasks to handle incoming JSON log messages.
|
/// Bind UDP and TCP sockets and start background tasks to handle incoming JSON log messages.
|
||||||
///
|
///
|
||||||
/// Returns join handles for the background tasks.
|
/// Returns join handles for the background tasks.
|
||||||
@@ -51,7 +71,7 @@ impl UdpJsonConfig {
|
|||||||
info!("Listening for JSON UDP on {}", self.listen_addr);
|
info!("Listening for JSON UDP on {}", self.listen_addr);
|
||||||
|
|
||||||
Ok(tokio::task::spawn(async move {
|
Ok(tokio::task::spawn(async move {
|
||||||
let mut buf = vec![0u8; 65536]; // Larger buffer for JSON
|
let mut buf = vec![0u8; 8192];
|
||||||
loop {
|
loop {
|
||||||
let Ok((len, _addr)) = udp_socket
|
let Ok((len, _addr)) = udp_socket
|
||||||
.recv_from(&mut buf)
|
.recv_from(&mut buf)
|
||||||
@@ -122,13 +142,11 @@ async fn handle_tcp_connection(stream: TcpStream, gateway: &Gateway) -> std::io:
|
|||||||
return Ok(()); // Clean EOF
|
return Ok(()); // Clean EOF
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = std::str::from_utf8(&msg_bytes).map_err(|err| {
|
let text = std::str::from_utf8(&msg_bytes)
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, err)
|
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let json_msg: JsonLogMessage = serde_json::from_str(text).map_err(|err| {
|
let json_msg: JsonLogMessage = serde_json::from_str(text)
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, err)
|
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let log_msg = json_msg.into_log_message();
|
let log_msg = json_msg.into_log_message();
|
||||||
gateway.handle_log_message(log_msg).await;
|
gateway.handle_log_message(log_msg).await;
|
||||||
@@ -183,7 +201,7 @@ impl JsonLogMessage {
|
|||||||
///
|
///
|
||||||
/// TODO: Allow this to take configuration options to customize how fields are mapped
|
/// TODO: Allow this to take configuration options to customize how fields are mapped
|
||||||
pub fn into_log_message(self) -> LogMessage {
|
pub fn into_log_message(self) -> LogMessage {
|
||||||
let level = self.level.unwrap_or(Level::INFO);
|
let level = self.level.unwrap_or(Level::ERROR);
|
||||||
let mut builder = LogMessage::builder(level, self.message);
|
let mut builder = LogMessage::builder(level, self.message);
|
||||||
|
|
||||||
if let Some(ts) = self.timestamp {
|
if let Some(ts) = self.timestamp {
|
||||||
@@ -309,7 +327,7 @@ mod tests {
|
|||||||
assert_eq!(msg.message, "Hello, world!");
|
assert_eq!(msg.message, "Hello, world!");
|
||||||
let log_msg = msg.into_log_message();
|
let log_msg = msg.into_log_message();
|
||||||
assert_eq!(&*log_msg.msg, "Hello, world!");
|
assert_eq!(&*log_msg.msg, "Hello, world!");
|
||||||
assert_eq!(log_msg.level, Level::INFO); // default
|
assert_eq!(log_msg.level, Level::ERROR); // default
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -524,7 +542,7 @@ mod tests {
|
|||||||
let log_msg = msg.into_log_message();
|
let log_msg = msg.into_log_message();
|
||||||
|
|
||||||
assert_eq!(&*log_msg.msg, "simple log");
|
assert_eq!(&*log_msg.msg, "simple log");
|
||||||
assert_eq!(log_msg.level, Level::INFO);
|
assert_eq!(log_msg.level, Level::ERROR);
|
||||||
assert!(log_msg.hostname.is_none());
|
assert!(log_msg.hostname.is_none());
|
||||||
assert!(log_msg.appname.is_none());
|
assert!(log_msg.appname.is_none());
|
||||||
assert!(log_msg.timestamp.is_none());
|
assert!(log_msg.timestamp.is_none());
|
||||||
@@ -535,14 +553,15 @@ mod tests {
|
|||||||
let json = r#"{"message": "test", "level": "unknown_level"}"#;
|
let json = r#"{"message": "test", "level": "unknown_level"}"#;
|
||||||
let msg: JsonLogMessage = serde_json::from_str(json).unwrap();
|
let msg: JsonLogMessage = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(msg.level, None);
|
assert_eq!(msg.level, None);
|
||||||
// Should default to INFO when converted
|
// Should default to ERROR when converted
|
||||||
let log_msg = msg.into_log_message();
|
let log_msg = msg.into_log_message();
|
||||||
assert_eq!(log_msg.level, Level::INFO);
|
assert_eq!(log_msg.level, Level::ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_extra_fields_are_ignored() {
|
fn test_extra_fields_are_ignored() {
|
||||||
let json = r#"{"message": "test", "extra_field": "ignored", "nested": {"also": "ignored"}}"#;
|
let json =
|
||||||
|
r#"{"message": "test", "extra_field": "ignored", "nested": {"also": "ignored"}}"#;
|
||||||
let msg: JsonLogMessage = serde_json::from_str(json).unwrap();
|
let msg: JsonLogMessage = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(msg.message, "test");
|
assert_eq!(msg.message, "test");
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,8 @@ use admin_netcat::AdminNetcatConfig;
|
|||||||
mod syslog;
|
mod syslog;
|
||||||
use syslog::SyslogConfig;
|
use syslog::SyslogConfig;
|
||||||
|
|
||||||
mod udp_json;
|
mod json;
|
||||||
use udp_json::UdpJsonConfig;
|
use json::JsonConfig;
|
||||||
|
|
||||||
/// Admin message handler configuration - select how non-command messages are handled
|
/// Admin message handler configuration - select how non-command messages are handled
|
||||||
#[derive(Clone, Debug, Subcommands)]
|
#[derive(Clone, Debug, Subcommands)]
|
||||||
@@ -53,7 +53,7 @@ struct Config {
|
|||||||
#[conf(flatten, prefix)]
|
#[conf(flatten, prefix)]
|
||||||
syslog: Option<SyslogConfig>,
|
syslog: Option<SyslogConfig>,
|
||||||
#[conf(flatten, prefix)]
|
#[conf(flatten, prefix)]
|
||||||
udp_json: Option<UdpJsonConfig>,
|
json: Option<JsonConfig>,
|
||||||
/// Optional admin message handler (netcat or http)
|
/// Optional admin message handler (netcat or http)
|
||||||
#[conf(subcommands)]
|
#[conf(subcommands)]
|
||||||
admin_handler: Option<AdminHandlerCommand>,
|
admin_handler: Option<AdminHandlerCommand>,
|
||||||
@@ -124,8 +124,8 @@ async fn main() {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let _udp_json_tasks = if let Some(udp_json) = &config.udp_json {
|
let _json_tasks = if let Some(json) = &config.json {
|
||||||
Some(udp_json.start_tasks(gateway.clone()).await.unwrap())
|
Some(json.start_tasks(gateway.clone()).await.unwrap())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ impl SyslogConfig {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(syslog_msg) = SyslogMessage::from_str(text)
|
let Ok(syslog_msg) = SyslogMessage::from_str(text).inspect_err(|err| {
|
||||||
.inspect_err(|err| error!("Syslog UDP packet was not valid syslog: {err}:\n{text}"))
|
error!("Syslog UDP packet was not valid syslog: {err}:\n{text}")
|
||||||
else {
|
}) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,7 +128,9 @@ async fn handle_tcp_connection(
|
|||||||
let mut reader = BufReader::new(stream);
|
let mut reader = BufReader::new(stream);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let Some(msg_bytes) = read_framed_syslog_bytes(&mut reader, config.tcp_cr_is_delimiter).await? else {
|
let Some(msg_bytes) =
|
||||||
|
read_framed_syslog_bytes(&mut reader, config.tcp_cr_is_delimiter).await?
|
||||||
|
else {
|
||||||
return Ok(()); // Clean EOF
|
return Ok(()); // Clean EOF
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -178,14 +180,16 @@ where
|
|||||||
|
|
||||||
// Octet counting: starts with non-zero digit
|
// Octet counting: starts with non-zero digit
|
||||||
b'1'..=b'9' => {
|
b'1'..=b'9' => {
|
||||||
return read_octet_counted_message(reader, first_byte).await.map(Some)
|
return read_octet_counted_message(reader, first_byte)
|
||||||
|
.await
|
||||||
|
.map(Some);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-transparent framing: starts with '<' (beginning of syslog PRI)
|
// Non-transparent framing: starts with '<' (beginning of syslog PRI)
|
||||||
b'<' => {
|
b'<' => {
|
||||||
return read_non_transparent_message(reader, first_byte, cr_is_delimiter)
|
return read_non_transparent_message(reader, first_byte, cr_is_delimiter)
|
||||||
.await
|
.await
|
||||||
.map(Some)
|
.map(Some);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid first byte
|
// Invalid first byte
|
||||||
@@ -197,7 +201,7 @@ where
|
|||||||
first_byte,
|
first_byte,
|
||||||
char::from(first_byte)
|
char::from(first_byte)
|
||||||
),
|
),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +227,10 @@ where
|
|||||||
.checked_mul(10)
|
.checked_mul(10)
|
||||||
.and_then(|l| l.checked_add((b - b'0') as usize))
|
.and_then(|l| l.checked_add((b - b'0') as usize))
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, "message length overflow")
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"message length overflow",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
b' ' => break,
|
b' ' => break,
|
||||||
|
|||||||
@@ -485,7 +485,9 @@ impl Gateway {
|
|||||||
sender_uuid: msg.source_uuid.clone(),
|
sender_uuid: msg.source_uuid.clone(),
|
||||||
group_id: data.group_info.as_ref().map(|g| g.group_id.clone()),
|
group_id: data.group_info.as_ref().map(|g| g.group_id.clone()),
|
||||||
};
|
};
|
||||||
handler.handle_verified_signal_message(msg, &GatewayContext).await
|
handler
|
||||||
|
.handle_verified_signal_message(msg, &GatewayContext)
|
||||||
|
.await
|
||||||
} else {
|
} else {
|
||||||
Err((501u16, "No message handler configured".into()))
|
Err((501u16, "No message handler configured".into()))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user