logger2 0.1.5

A modern, feature-rich replacement for the classic `logger` CLI: syslog (local/remote, RFC3164 & RFC5424), truecolor hex output, emoji, JSON, and a fully configurable TOML config file.
Documentation
use crate::color::HexColor;
use crate::config::Config;
use crate::level::Level;
use chrono::Local;
use serde::Serialize;

/// Everything needed to render a single log line to the console or as JSON.
pub struct RenderContext<'a> {
    pub level: Level,
    pub tag: &'a str,
    pub pid: Option<u32>,
    pub message: &'a str,
    pub fields: &'a [(String, String)],
    pub use_color: bool,
    pub use_emoji: bool,
    pub color_override: Option<HexColor>,
    pub emoji_override: Option<&'a str>,
}

#[derive(Serialize)]
struct JsonLine<'a> {
    timestamp: String,
    level: &'a str,
    severity: u8,
    tag: &'a str,
    pid: Option<u32>,
    message: &'a str,
    fields: std::collections::BTreeMap<&'a str, &'a str>,
}

/// Renders a human-oriented, optionally colored/emoji-decorated console line
/// according to `config.output.template`.
pub fn render_console(ctx: &RenderContext, config: &Config) -> String {
    let now = Local::now();
    let timestamp = now.format(&config.general.timestamp_format).to_string();

    let emoji = if ctx.use_emoji {
        ctx.emoji_override
            .unwrap_or_else(|| config.emoji.for_level(ctx.level))
    } else {
        ""
    };

    let level_label = ctx.level.label();
    let pid_str = ctx.pid.map(|p| format!("[{p}]")).unwrap_or_default();
    let tag_with_pid = format!("{}{}", ctx.tag, pid_str);

    let mut fields_str = String::new();
    if !ctx.fields.is_empty() {
        fields_str.push(' ');
        fields_str.push_str(
            &ctx.fields
                .iter()
                .map(|(k, v)| format!("{k}={v}"))
                .collect::<Vec<_>>()
                .join(" "),
        );
    }

    let rendered_message = format!("{}{}", ctx.message, fields_str);

    let body = config
        .output
        .template
        .replace("{timestamp}", &timestamp)
        .replace("{emoji}", emoji)
        .replace("{level}", level_label)
        .replace("{tag}", &tag_with_pid)
        .replace("{message}", &rendered_message);

    // Collapse the double space left behind when emoji is disabled.
    let body = body.replace("  ", " ").trim_start().to_string();

    if !ctx.use_color {
        return body;
    }

    let color = ctx
        .color_override
        .unwrap_or_else(|| config.colors.for_level(ctx.level));
    color.paint(&body)
}

/// Renders the line as a single-line JSON object.
pub fn render_json(ctx: &RenderContext) -> serde_json::Result<String> {
    let now = Local::now();
    let line = JsonLine {
        timestamp: now.to_rfc3339_opts(chrono::SecondsFormat::Millis, false),
        level: ctx.level.as_str(),
        severity: ctx.level.severity(),
        tag: ctx.tag,
        pid: ctx.pid,
        message: ctx.message,
        fields: ctx
            .fields
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_str()))
            .collect(),
    };
    serde_json::to_string(&line)
}