log_json: append the server-notice/log stream to a file as JSONL (log_json = <path>)
This commit is contained in:
parent
6ffd6a57bc
commit
72ca89af26
5 changed files with 65 additions and 11 deletions
|
|
@ -136,6 +136,9 @@ amu_target = both
|
||||||
# syslog_facility = daemon # kern user mail daemon auth ... local0..local7
|
# syslog_facility = daemon # kern user mail daemon auth ... local0..local7
|
||||||
# syslog_tag = echoircd
|
# 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
|
# --- 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
|
# 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.
|
# the proxy's. A connection from a trusted proxy MUST lead with a PROXY header.
|
||||||
|
|
|
||||||
|
|
@ -63,27 +63,30 @@ fn escape_tag(v: &str) -> String {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The escaped `draft/json-log` tag *value* for a server notice `msg`. Place after
|
/// The structured JSON object for a server notice `msg` (unescaped). echoIRCd
|
||||||
/// `draft/json-log=` in the tag block. echoIRCd snotices are untyped, so `subsystem`
|
/// snotices are untyped, so `subsystem` / `event_id` are derived from the leading
|
||||||
/// / `event_id` are derived from the leading word and `snomask` is the generic `s`.
|
/// word and `snomask` is the generic `s`. Shared by the tag value and `log_json`.
|
||||||
pub fn tag_value(s: &Server, msg: &str) -> String {
|
pub fn json_line(s: &Server, msg: &str) -> String {
|
||||||
let head = msg
|
let head = msg
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or("general")
|
.unwrap_or("general")
|
||||||
.trim_end_matches(':');
|
.trim_end_matches(':');
|
||||||
let subsystem = head.to_ascii_lowercase();
|
obj(&[
|
||||||
let event_id = head.to_ascii_uppercase();
|
|
||||||
let json = obj(&[
|
|
||||||
("timestamp", qstr(&iso_time(now()))),
|
("timestamp", qstr(&iso_time(now()))),
|
||||||
("level", qstr("info")),
|
("level", qstr("info")),
|
||||||
("subsystem", qstr(&subsystem)),
|
("subsystem", qstr(&head.to_ascii_lowercase())),
|
||||||
("event_id", qstr(&event_id)),
|
("event_id", qstr(&head.to_ascii_uppercase())),
|
||||||
("log_source", qstr(&s.name)),
|
("log_source", qstr(&s.name)),
|
||||||
("msg", qstr(&strip_formatting(msg))),
|
("msg", qstr(&strip_formatting(msg))),
|
||||||
("snomask", qstr("s")),
|
("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)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
46
src/modules/log_json.rs
Normal file
46
src/modules/log_json.rs
Normal file
|
|
@ -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 = <path>` 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<Option<(String, File)>> = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,7 @@ pub mod ident;
|
||||||
pub mod irccloudtags;
|
pub mod irccloudtags;
|
||||||
pub mod jsonlog;
|
pub mod jsonlog;
|
||||||
pub mod jwt;
|
pub mod jwt;
|
||||||
|
pub mod log_json;
|
||||||
pub mod maphide;
|
pub mod maphide;
|
||||||
pub mod markread;
|
pub mod markread;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
|
|
|
||||||
|
|
@ -758,6 +758,7 @@ impl Server {
|
||||||
}
|
}
|
||||||
crate::modules::chanlog::tee(self, msg);
|
crate::modules::chanlog::tee(self, msg);
|
||||||
crate::modules::syslog::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
|
/// Broadcast a `*** msg` server NOTICE to *every* registered local user — for
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue