sword-layers 0.3.0

Tower layers for the Sword web framework
Documentation
use std::fmt;
use std::sync::{
    Arc,
    atomic::{AtomicU8, Ordering},
};

use tracing::{
    Event, Level, Subscriber,
    field::{Field, Visit},
};
use tracing_subscriber::{
    fmt::{FmtContext, FormatEvent, FormatFields, format::Writer, time::FormatTime},
    registry::LookupSpan,
};

use super::{TracingConfig, TracingField, time::TimestampFormatter};

const ANSI_RESET: &str = "\x1b[0m";
const ANSI_KEY: &str = "\x1b[36m";
const ANSI_ERROR: &str = "\x1b[31m";
const ANSI_WARN: &str = "\x1b[33m";
const ANSI_INFO: &str = "\x1b[32m";
const ANSI_DEBUG: &str = "\x1b[34m";
const ANSI_TRACE: &str = "\x1b[90m";

#[derive(Clone, Debug)]
pub struct DevFormatter {
    with_target: bool,
    with_file: bool,
    with_line_number: bool,
    timer: TimestampFormatter,
    last_event_layout: Arc<AtomicU8>,
}

impl DevFormatter {
    pub fn new(config: &TracingConfig) -> Self {
        Self {
            with_target: config.has_field(TracingField::Target),
            with_file: config.has_field(TracingField::File),
            with_line_number: config.has_field(TracingField::LineNumber),
            timer: TimestampFormatter::from_config(config),
            last_event_layout: Arc::new(AtomicU8::new(0)),
        }
    }

    fn write_key(&self, writer: &mut Writer<'_>, key: &str) -> fmt::Result {
        if writer.has_ansi_escapes() {
            write!(writer, "{ANSI_KEY}{key}{ANSI_RESET}")
        } else {
            writer.write_str(key)
        }
    }

    fn write_level(&self, writer: &mut Writer<'_>, level: &Level) -> fmt::Result {
        if writer.has_ansi_escapes() {
            let color = match *level {
                Level::ERROR => ANSI_ERROR,
                Level::WARN => ANSI_WARN,
                Level::INFO => ANSI_INFO,
                Level::DEBUG => ANSI_DEBUG,
                Level::TRACE => ANSI_TRACE,
            };

            write!(writer, "{color}{level}{ANSI_RESET}")
        } else {
            write!(writer, "{level}")
        }
    }

    fn write_field(&self, writer: &mut Writer<'_>, key: &str, value: &str) -> fmt::Result {
        writer.write_str("  ")?;
        self.write_key(writer, key)?;
        writer.write_str(": ")?;
        writer.write_str(value)
    }

    fn write_multiline_field(
        &self,
        writer: &mut Writer<'_>,
        key: &str,
        value: &str,
    ) -> fmt::Result {
        writer.write_str("\n       ")?;
        self.write_key(writer, key)?;
        writer.write_str(":")?;

        let lines: Vec<&str> = value.lines().collect();

        if lines.is_empty() {
            return Ok(());
        }

        let render_as_block = lines.len() > 1 || lines[0].starts_with("- ");

        if !render_as_block {
            writer.write_str(" ")?;
            writer.write_str(lines[0])?;
            return Ok(());
        }

        for line in lines {
            writer.write_str("\n         ")?;
            writer.write_str(line)?;
        }

        Ok(())
    }

    fn should_use_multiline(&self, level: &Level, visitor: &DevFieldVisitor) -> bool {
        const MAX_SINGLE_LINE_MESSAGE: usize = 100;
        const MAX_SINGLE_LINE_FIELD: usize = 60;
        const MAX_SINGLE_LINE_FIELDS: usize = 3;
        const MAX_SINGLE_LINE_TOTAL: usize = 140;
        const MAX_SINGLE_LINE_TOTAL_ERROR: usize = 120;

        let summary = EventLayoutSummary::from_visitor(visitor);

        if summary.message_has_newline || summary.field_has_newline {
            return true;
        }

        if summary.is_diagnostic {
            return true;
        }

        if summary.message_len > MAX_SINGLE_LINE_MESSAGE {
            return true;
        }

        if summary.max_field_len > MAX_SINGLE_LINE_FIELD {
            return true;
        }

        if summary.field_count > MAX_SINGLE_LINE_FIELDS {
            return true;
        }

        if *level == Level::ERROR && summary.total_len > MAX_SINGLE_LINE_TOTAL_ERROR {
            return true;
        }

        summary.total_len > MAX_SINGLE_LINE_TOTAL
    }
}

impl<S, N> FormatEvent<S, N> for DevFormatter
where
    S: Subscriber + for<'a> LookupSpan<'a>,
    N: for<'writer> FormatFields<'writer> + 'static,
{
    fn format_event(
        &self,
        _: &FmtContext<'_, S, N>,
        mut writer: Writer<'_>,
        event: &Event<'_>,
    ) -> fmt::Result {
        let previous_layout = self.last_event_layout.load(Ordering::Relaxed);

        let metadata = event.metadata();
        let mut visitor = DevFieldVisitor::default();
        event.record(&mut visitor);

        let multiline = self.should_use_multiline(metadata.level(), &visitor);

        if previous_layout != 0 && (previous_layout == 2 || multiline) {
            writer.write_str("\n")?;
        }

        self.timer.format_time(&mut writer)?;

        if !matches!(self.timer, TimestampFormatter::None) {
            writer.write_str(" ")?;
        }

        self.write_level(&mut writer, metadata.level())?;

        if let Some(message) = visitor.message.as_deref() {
            writer.write_str("  ")?;
            writer.write_str(message)?;
        }

        if multiline {
            if self.with_target {
                self.write_multiline_field(&mut writer, "target", metadata.target())?;
            }

            if self.with_file
                && let Some(file) = metadata.file()
            {
                self.write_multiline_field(&mut writer, "file", file)?;
            }

            if self.with_line_number
                && let Some(line_number) = metadata.line()
            {
                let line_number = line_number.to_string();
                self.write_multiline_field(&mut writer, "line", &line_number)?;
            }

            for (key, value) in &visitor.fields {
                self.write_multiline_field(&mut writer, key, value)?;
            }

            writer.write_str("\n")?;
            self.last_event_layout.store(2, Ordering::Relaxed);
            return Ok(());
        }

        if self.with_target {
            self.write_field(&mut writer, "target", metadata.target())?;
        }

        if self.with_file
            && let Some(file) = metadata.file()
        {
            self.write_field(&mut writer, "file", file)?;
        }

        if self.with_line_number
            && let Some(line_number) = metadata.line()
        {
            let line_number = line_number.to_string();
            self.write_field(&mut writer, "line", &line_number)?;
        }

        for (key, value) in &visitor.fields {
            self.write_field(&mut writer, key, value)?;
        }

        self.last_event_layout.store(1, Ordering::Relaxed);
        writer.write_str("\n")
    }
}

struct EventLayoutSummary {
    field_count: usize,
    message_len: usize,
    max_field_len: usize,
    total_len: usize,
    message_has_newline: bool,
    field_has_newline: bool,
    is_diagnostic: bool,
}

impl EventLayoutSummary {
    fn from_visitor(visitor: &DevFieldVisitor) -> Self {
        let message = visitor.message.as_deref().unwrap_or_default();
        let field_count = visitor.fields.len();
        let message_len = message.len();
        let message_has_newline = message.contains('\n');
        let field_has_newline = visitor.fields.iter().any(|(_, value)| value.contains('\n'));
        let max_field_len = visitor
            .fields
            .iter()
            .map(|(_, value)| value.len())
            .max()
            .unwrap_or(0);
        let total_len = message_len
            + visitor
                .fields
                .iter()
                .map(|(key, value)| key.len() + value.len() + 4)
                .sum::<usize>();

        let is_diagnostic = visitor.fields.iter().any(|(key, _)| {
            matches!(
                key.as_str(),
                "reason"
                    | "source"
                    | "hint"
                    | "hints"
                    | "details"
                    | "dependency_path"
                    | "missing_dependency_path"
            )
        });

        Self {
            field_count,
            message_len,
            max_field_len,
            total_len,
            message_has_newline,
            field_has_newline,
            is_diagnostic,
        }
    }
}

#[derive(Default)]
struct DevFieldVisitor {
    message: Option<String>,
    fields: Vec<(String, String)>,
}

impl DevFieldVisitor {
    fn push(&mut self, field: &Field, value: String) {
        if field.name() == "message" {
            self.message = Some(value);
        } else {
            self.fields.push((field.name().to_string(), value));
        }
    }
}

impl Visit for DevFieldVisitor {
    fn record_str(&mut self, field: &Field, value: &str) {
        self.push(field, value.to_string());
    }

    fn record_bool(&mut self, field: &Field, value: bool) {
        self.push(field, value.to_string());
    }

    fn record_i64(&mut self, field: &Field, value: i64) {
        self.push(field, value.to_string());
    }

    fn record_u64(&mut self, field: &Field, value: u64) {
        self.push(field, value.to_string());
    }

    fn record_i128(&mut self, field: &Field, value: i128) {
        self.push(field, value.to_string());
    }

    fn record_u128(&mut self, field: &Field, value: u128) {
        self.push(field, value.to_string());
    }

    fn record_f64(&mut self, field: &Field, value: f64) {
        self.push(field, value.to_string());
    }

    fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
        self.push(field, value.to_string());
    }

    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
        self.push(field, format!("{value:?}"));
    }
}