#![deny(warnings, missing_docs, clippy::all)]
#![forbid(unsafe_code)]
use std::io::prelude::*;
use std::os::fd::AsFd;
use client::JournalClient;
use log::kv::{Error, Key, Value, VisitSource};
use log::{Level, Log, Metadata, Record, SetLoggerError};
mod client;
mod fields;
use fields::*;
pub fn connected_to_journal() -> bool {
rustix::fs::fstat(std::io::stderr().as_fd())
.map(|stat| format!("{}:{}", stat.st_dev, stat.st_ino))
.ok()
.and_then(|stderr| {
std::env::var_os("JOURNAL_STREAM").map(|s| s.to_string_lossy() == stderr.as_str())
})
.unwrap_or(false)
}
pub fn current_exe_identifier() -> Option<String> {
let executable = std::env::current_exe().ok()?;
Some(executable.file_name()?.to_string_lossy().into_owned())
}
struct WriteKeyValues<'a>(&'a mut Vec<u8>);
impl<'kvs> VisitSource<'kvs> for WriteKeyValues<'_> {
fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), Error> {
put_field_length_encoded(self.0, FieldName::WriteEscaped(key.as_str()), value);
Ok(())
}
}
pub struct JournalLog {
client: JournalClient,
extra_fields: Vec<u8>,
syslog_identifier: String,
}
fn record_payload(syslog_identifier: &str, record: &Record) -> Vec<u8> {
use FieldName::*;
let mut buffer = Vec::with_capacity(1024);
let priority = match record.level() {
Level::Error => b"3",
Level::Warn => b"4",
Level::Info => b"5",
Level::Debug => b"6",
Level::Trace => b"7",
};
put_field_bytes(&mut buffer, WellFormed("PRIORITY"), priority);
put_field_length_encoded(&mut buffer, WellFormed("MESSAGE"), record.args());
writeln!(&mut buffer, "SYSLOG_PID={}", std::process::id()).unwrap();
if !syslog_identifier.is_empty() {
put_field_bytes(
&mut buffer,
WellFormed("SYSLOG_IDENTIFIER"),
syslog_identifier.as_bytes(),
);
}
if let Some(file) = record.file() {
put_field_bytes(&mut buffer, WellFormed("CODE_FILE"), file.as_bytes());
}
if let Some(module) = record.module_path() {
put_field_bytes(&mut buffer, WellFormed("CODE_MODULE"), module.as_bytes());
}
if let Some(line) = record.line() {
writeln!(&mut buffer, "CODE_LINE={}", line).unwrap();
}
put_field_bytes(
&mut buffer,
WellFormed("TARGET"),
record.target().as_bytes(),
);
record
.key_values()
.visit(&mut WriteKeyValues(&mut buffer))
.unwrap();
buffer
}
impl JournalLog {
pub fn new() -> std::io::Result<Self> {
let logger = Self::empty()?;
Ok(logger.with_syslog_identifier(current_exe_identifier().unwrap_or_default()))
}
pub fn empty() -> std::io::Result<Self> {
Ok(Self {
client: JournalClient::new()?,
extra_fields: Vec::new(),
syslog_identifier: String::new(),
})
}
pub fn install(self) -> Result<(), SetLoggerError> {
log::set_boxed_logger(Box::new(self))
}
pub fn add_extra_field<K: AsRef<str>, V: AsRef<[u8]>>(mut self, name: K, value: V) -> Self {
put_field_bytes(
&mut self.extra_fields,
FieldName::WriteEscaped(name.as_ref()),
value.as_ref(),
);
self
}
pub fn with_extra_fields<I, K, V>(mut self, extra_fields: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<[u8]>,
{
self.extra_fields.clear();
let mut logger = self;
for (name, value) in extra_fields {
logger = logger.add_extra_field(name, value);
}
logger
}
pub fn with_syslog_identifier(mut self, identifier: String) -> Self {
self.syslog_identifier = identifier;
self
}
fn record_payload(&self, record: &Record) -> Vec<u8> {
let mut payload = record_payload(&self.syslog_identifier, record);
payload.extend_from_slice(&self.extra_fields);
payload
}
pub fn journal_send(&self, record: &Record) -> std::io::Result<()> {
let _ = self.client.send_payload(&self.record_payload(record))?;
Ok(())
}
}
impl Log for JournalLog {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
let _ = self.journal_send(record);
}
fn flush(&self) {}
}