diff --git a/echoircd.conf.example b/echoircd.conf.example index f0d4d70..1c286a0 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -136,6 +136,9 @@ amu_target = both # syslog_facility = daemon # kern user mail daemon auth ... local0..local7 # syslog_tag = echoircd +# --- log_json: append the server-notice / log stream to a file as JSONL --- +# log_json = /var/log/echoircd/events.jsonl + # --- PROXY protocol: trust the HAProxy/nginx PROXY header (v1 or v2) from these # sources (glob or CIDR, repeatable), so the real client IP is used instead of # the proxy's. A connection from a trusted proxy MUST lead with a PROXY header. diff --git a/src/modules/jsonlog.rs b/src/modules/jsonlog.rs index 7ceee76..5ec04ae 100644 --- a/src/modules/jsonlog.rs +++ b/src/modules/jsonlog.rs @@ -63,27 +63,30 @@ fn escape_tag(v: &str) -> String { out } -/// The escaped `draft/json-log` tag *value* for a server notice `msg`. Place after -/// `draft/json-log=` in the tag block. echoIRCd snotices are untyped, so `subsystem` -/// / `event_id` are derived from the leading word and `snomask` is the generic `s`. -pub fn tag_value(s: &Server, msg: &str) -> String { +/// The structured JSON object for a server notice `msg` (unescaped). echoIRCd +/// snotices are untyped, so `subsystem` / `event_id` are derived from the leading +/// word and `snomask` is the generic `s`. Shared by the tag value and `log_json`. +pub fn json_line(s: &Server, msg: &str) -> String { let head = msg .split_whitespace() .next() .unwrap_or("general") .trim_end_matches(':'); - let subsystem = head.to_ascii_lowercase(); - let event_id = head.to_ascii_uppercase(); - let json = obj(&[ + obj(&[ ("timestamp", qstr(&iso_time(now()))), ("level", qstr("info")), - ("subsystem", qstr(&subsystem)), - ("event_id", qstr(&event_id)), + ("subsystem", qstr(&head.to_ascii_lowercase())), + ("event_id", qstr(&head.to_ascii_uppercase())), ("log_source", qstr(&s.name)), ("msg", qstr(&strip_formatting(msg))), ("snomask", qstr("s")), - ]); - escape_tag(&json) + ]) +} + +/// The escaped `draft/json-log` tag *value* for a server notice `msg`. Place after +/// `draft/json-log=` in the tag block. +pub fn tag_value(s: &Server, msg: &str) -> String { + escape_tag(&json_line(s, msg)) } #[cfg(test)] diff --git a/src/modules/log_json.rs b/src/modules/log_json.rs new file mode 100644 index 0000000..cd4d7ad --- /dev/null +++ b/src/modules/log_json.rs @@ -0,0 +1,46 @@ +//! log_json — append the server-notice / log stream to a file as JSON, one object +//! per line (JSONL). Off unless `log_json = ` is set. Reuses the +//! `draft/json-log` object builder. The file handle is cached on the core thread +//! (snotice is single-threaded) and reopened if the path changes or a write fails — +//! so an external logrotate that renames the file is picked up on the next line. + +use std::cell::RefCell; +use std::fs::{File, OpenOptions}; +use std::io::Write; + +use crate::server::Server; + +thread_local! { + /// (configured path, open append handle) cached for reuse. + static SINK: RefCell> = const { RefCell::new(None) }; +} + +/// Append `msg` as a JSON line to the configured log file. Called at the tail of +/// [`Server::snotice`]. +pub fn tee(s: &Server, msg: &str) { + let Some(path) = s.conf("log_json") else { + SINK.with(|c| *c.borrow_mut() = None); // disabled: drop any handle + return; + }; + let line = crate::modules::jsonlog::json_line(s, msg); + SINK.with(|cell| { + let mut slot = cell.borrow_mut(); + let need_open = match slot.as_ref() { + Some((p, _)) => p != path, + None => true, + }; + if need_open { + *slot = OpenOptions::new() + .create(true) + .append(true) + .open(path) + .ok() + .map(|f| (path.to_string(), f)); + } + if let Some((_, f)) = slot.as_mut() { + if writeln!(f, "{line}").is_err() { + *slot = None; // reopen next time + } + } + }); +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 5a2910b..b3247bc 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -41,6 +41,7 @@ pub mod ident; pub mod irccloudtags; pub mod jsonlog; pub mod jwt; +pub mod log_json; pub mod maphide; pub mod markread; pub mod metadata; diff --git a/src/server.rs b/src/server.rs index ca9cc7a..c771e83 100644 --- a/src/server.rs +++ b/src/server.rs @@ -758,6 +758,7 @@ impl Server { } crate::modules::chanlog::tee(self, msg); crate::modules::syslog::tee(self, msg); + crate::modules::log_json::tee(self, msg); } /// Broadcast a `*** msg` server NOTICE to *every* registered local user — for