atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git

Blame: xtask/src/log/overview.rs

7ff8adafe9e4atagen2026-10-22 12:30 UTC1
use std::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC2
    collections::{BTreeMap, HashMap},
7ff8adafe9e4atagen2026-10-22 12:30 UTC3
    fmt, fs,
7ff8adafe9e4atagen2026-10-22 12:30 UTC4
    io::{self, BufRead, Write},
7ff8adafe9e4atagen2026-10-22 12:30 UTC5
    ops::Not,
7ff8adafe9e4atagen2026-10-22 12:30 UTC6
    path,
7ff8adafe9e4atagen2026-10-22 12:30 UTC7
    str::FromStr,
7ff8adafe9e4atagen2026-10-22 12:30 UTC8
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC9
7ff8adafe9e4atagen2026-10-22 12:30 UTC10
use chrono::{DateTime, FixedOffset};
7ff8adafe9e4atagen2026-10-22 12:30 UTC11
use lazy_static::lazy_static;
7ff8adafe9e4atagen2026-10-22 12:30 UTC12
use regex::{Regex, RegexBuilder};
7ff8adafe9e4atagen2026-10-22 12:30 UTC13
7ff8adafe9e4atagen2026-10-22 12:30 UTC14
use crate::Result;
7ff8adafe9e4atagen2026-10-22 12:30 UTC15
7ff8adafe9e4atagen2026-10-22 12:30 UTC16
const OUTPUT_TEMPLATE: &str = include_str!("overview.template.html");
7ff8adafe9e4atagen2026-10-22 12:30 UTC17
7ff8adafe9e4atagen2026-10-22 12:30 UTC18
lazy_static! {
7ff8adafe9e4atagen2026-10-22 12:30 UTC19
    static ref LINE_PARSER: Regex = {
7ff8adafe9e4atagen2026-10-22 12:30 UTC20
        RegexBuilder::new(
7ff8adafe9e4atagen2026-10-22 12:30 UTC21
            r#"
7ff8adafe9e4atagen2026-10-22 12:30 UTC22
            # Let's start.
7ff8adafe9e4atagen2026-10-22 12:30 UTC23
            ^
7ff8adafe9e4atagen2026-10-22 12:30 UTC24
7ff8adafe9e4atagen2026-10-22 12:30 UTC25
            # Datetime of the log line.
7ff8adafe9e4atagen2026-10-22 12:30 UTC26
            (?<datetime>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)
7ff8adafe9e4atagen2026-10-22 12:30 UTC27
7ff8adafe9e4atagen2026-10-22 12:30 UTC28
            # Log level.
7ff8adafe9e4atagen2026-10-22 12:30 UTC29
            \s+(?<level>\S+)
7ff8adafe9e4atagen2026-10-22 12:30 UTC30
7ff8adafe9e4atagen2026-10-22 12:30 UTC31
            # Target.
7ff8adafe9e4atagen2026-10-22 12:30 UTC32
            \s(?<target>matrix_[\w_]+(::[\w_]+)*):
7ff8adafe9e4atagen2026-10-22 12:30 UTC33
7ff8adafe9e4atagen2026-10-22 12:30 UTC34
            # The log message. We don't care about it.
7ff8adafe9e4atagen2026-10-22 12:30 UTC35
            (?<message>.*)
7ff8adafe9e4atagen2026-10-22 12:30 UTC36
7ff8adafe9e4atagen2026-10-22 12:30 UTC37
            # The source file and line.
7ff8adafe9e4atagen2026-10-22 12:30 UTC38
            \|\scrates/
7ff8adafe9e4atagen2026-10-22 12:30 UTC39
            (?<source_file>[^:]+)
7ff8adafe9e4atagen2026-10-22 12:30 UTC40
            :(?<source_line>\d+)
7ff8adafe9e4atagen2026-10-22 12:30 UTC41
7ff8adafe9e4atagen2026-10-22 12:30 UTC42
            # The spans.
7ff8adafe9e4atagen2026-10-22 12:30 UTC43
            \s\|\sspans:\s
7ff8adafe9e4atagen2026-10-22 12:30 UTC44
            (?<spans>.+)
7ff8adafe9e4atagen2026-10-22 12:30 UTC45
        "#,
7ff8adafe9e4atagen2026-10-22 12:30 UTC46
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC47
        .ignore_whitespace(true)
7ff8adafe9e4atagen2026-10-22 12:30 UTC48
        .build()
7ff8adafe9e4atagen2026-10-22 12:30 UTC49
        .expect("Failed to build the `line_parser` regex")
7ff8adafe9e4atagen2026-10-22 12:30 UTC50
    };
7ff8adafe9e4atagen2026-10-22 12:30 UTC51
    static ref MESSAGE_PARSER: Regex = {
7ff8adafe9e4atagen2026-10-22 12:30 UTC52
        RegexBuilder::new(
7ff8adafe9e4atagen2026-10-22 12:30 UTC53
            r#"
7ff8adafe9e4atagen2026-10-22 12:30 UTC54
            # Let's start.
7ff8adafe9e4atagen2026-10-22 12:30 UTC55
            ^
7ff8adafe9e4atagen2026-10-22 12:30 UTC56
7ff8adafe9e4atagen2026-10-22 12:30 UTC57
            # Anything (trimmed)…
7ff8adafe9e4atagen2026-10-22 12:30 UTC58
            \s*(?<message>.*?)
7ff8adafe9e4atagen2026-10-22 12:30 UTC59
7ff8adafe9e4atagen2026-10-22 12:30 UTC60
            # … until optional fields!
7ff8adafe9e4atagen2026-10-22 12:30 UTC61
            (\s(?<fields>[\w\d_]+=.*))?$
7ff8adafe9e4atagen2026-10-22 12:30 UTC62
        "#,
7ff8adafe9e4atagen2026-10-22 12:30 UTC63
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC64
        .ignore_whitespace(true)
7ff8adafe9e4atagen2026-10-22 12:30 UTC65
        .build()
7ff8adafe9e4atagen2026-10-22 12:30 UTC66
        .expect("Failed to build the `message_parser` regex")
7ff8adafe9e4atagen2026-10-22 12:30 UTC67
    };
7ff8adafe9e4atagen2026-10-22 12:30 UTC68
    static ref FIELDS_PARSER: Regex = {
7ff8adafe9e4atagen2026-10-22 12:30 UTC69
        RegexBuilder::new(
7ff8adafe9e4atagen2026-10-22 12:30 UTC70
            r#"
7ff8adafe9e4atagen2026-10-22 12:30 UTC71
            # Let's start.
7ff8adafe9e4atagen2026-10-22 12:30 UTC72
            ^
7ff8adafe9e4atagen2026-10-22 12:30 UTC73
7ff8adafe9e4atagen2026-10-22 12:30 UTC74
            # A name.
7ff8adafe9e4atagen2026-10-22 12:30 UTC75
            \s*(?<name>[\w\d_]+)
7ff8adafe9e4atagen2026-10-22 12:30 UTC76
            # Equal
7ff8adafe9e4atagen2026-10-22 12:30 UTC77
            =
7ff8adafe9e4atagen2026-10-22 12:30 UTC78
            # A value, which can be anything: it stops when a new field is found.
7ff8adafe9e4atagen2026-10-22 12:30 UTC79
            (?<value>.*?)
7ff8adafe9e4atagen2026-10-22 12:30 UTC80
7ff8adafe9e4atagen2026-10-22 12:30 UTC81
            # The new field (and everything else).
7ff8adafe9e4atagen2026-10-22 12:30 UTC82
            (?<next_fields>\s[\w\d_]+=.+)?$
7ff8adafe9e4atagen2026-10-22 12:30 UTC83
        "#,
7ff8adafe9e4atagen2026-10-22 12:30 UTC84
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC85
        .ignore_whitespace(true)
7ff8adafe9e4atagen2026-10-22 12:30 UTC86
        .build()
7ff8adafe9e4atagen2026-10-22 12:30 UTC87
        .expect("Failed to build the `fields_parser` regex")
7ff8adafe9e4atagen2026-10-22 12:30 UTC88
    };
7ff8adafe9e4atagen2026-10-22 12:30 UTC89
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC90
7ff8adafe9e4atagen2026-10-22 12:30 UTC91
pub(super) fn run(log_path: path::PathBuf, output_path: path::PathBuf) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC92
    let line_parser = &*LINE_PARSER;
7ff8adafe9e4atagen2026-10-22 12:30 UTC93
7ff8adafe9e4atagen2026-10-22 12:30 UTC94
    let log_file = fs::File::open(&log_path)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC95
    let mut output_file = io::BufWriter::new(fs::File::create(&output_path)?);
7ff8adafe9e4atagen2026-10-22 12:30 UTC96
7ff8adafe9e4atagen2026-10-22 12:30 UTC97
    let reader = io::BufReader::new(log_file);
7ff8adafe9e4atagen2026-10-22 12:30 UTC98
    let mut number_of_analysed_lines = 0;
7ff8adafe9e4atagen2026-10-22 12:30 UTC99
    let mut number_of_matched_lines = 0;
7ff8adafe9e4atagen2026-10-22 12:30 UTC100
7ff8adafe9e4atagen2026-10-22 12:30 UTC101
    let mut tree = TargetTree::default();
7ff8adafe9e4atagen2026-10-22 12:30 UTC102
7ff8adafe9e4atagen2026-10-22 12:30 UTC103
    for (line_nth, line) in reader.lines().enumerate().map(|(nth, line)| {
7ff8adafe9e4atagen2026-10-22 12:30 UTC104
        (
7ff8adafe9e4atagen2026-10-22 12:30 UTC105
            nth + 1,
7ff8adafe9e4atagen2026-10-22 12:30 UTC106
            line.unwrap_or_else(|error| {
7ff8adafe9e4atagen2026-10-22 12:30 UTC107
                panic!("Failed to read line #{nth}\n{error}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC108
            }),
7ff8adafe9e4atagen2026-10-22 12:30 UTC109
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC110
    }) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC111
        number_of_analysed_lines += 1;
7ff8adafe9e4atagen2026-10-22 12:30 UTC112
7ff8adafe9e4atagen2026-10-22 12:30 UTC113
        if let Some(captures) = line_parser.captures(&line) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC114
            number_of_matched_lines += 1;
7ff8adafe9e4atagen2026-10-22 12:30 UTC115
7ff8adafe9e4atagen2026-10-22 12:30 UTC116
            let date_time = DateTime::parse_from_rfc3339(
7ff8adafe9e4atagen2026-10-22 12:30 UTC117
                captures.name("datetime").expect("Failed to capture `datetime`").as_str(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC118
            )
7ff8adafe9e4atagen2026-10-22 12:30 UTC119
            .expect("Failed to parse `datetime`");
7ff8adafe9e4atagen2026-10-22 12:30 UTC120
            let full_target = captures.name("target").expect("Failed to capture `target`").as_str();
7ff8adafe9e4atagen2026-10-22 12:30 UTC121
            let level = captures
7ff8adafe9e4atagen2026-10-22 12:30 UTC122
                .name("level")
7ff8adafe9e4atagen2026-10-22 12:30 UTC123
                .expect("Failed to capture `level`")
7ff8adafe9e4atagen2026-10-22 12:30 UTC124
                .as_str()
7ff8adafe9e4atagen2026-10-22 12:30 UTC125
                .parse()
7ff8adafe9e4atagen2026-10-22 12:30 UTC126
                .expect("Failed to parse the `level`");
7ff8adafe9e4atagen2026-10-22 12:30 UTC127
            let message = captures.name("message").expect("Failed to capture `message`").as_str();
7ff8adafe9e4atagen2026-10-22 12:30 UTC128
            let source_file =
7ff8adafe9e4atagen2026-10-22 12:30 UTC129
                captures.name("source_file").expect("Failed to capture `source_file`").as_str();
7ff8adafe9e4atagen2026-10-22 12:30 UTC130
            let source_line = captures
7ff8adafe9e4atagen2026-10-22 12:30 UTC131
                .name("source_line")
7ff8adafe9e4atagen2026-10-22 12:30 UTC132
                .expect("Failed to capture `source_line`")
7ff8adafe9e4atagen2026-10-22 12:30 UTC133
                .as_str()
7ff8adafe9e4atagen2026-10-22 12:30 UTC134
                .parse()
7ff8adafe9e4atagen2026-10-22 12:30 UTC135
                .expect("Failed to parse the `source_line`");
7ff8adafe9e4atagen2026-10-22 12:30 UTC136
            let spans = captures.name("spans").expect("Failed to capture `spans`").as_str();
7ff8adafe9e4atagen2026-10-22 12:30 UTC137
7ff8adafe9e4atagen2026-10-22 12:30 UTC138
            let mut tree_node = &mut tree;
7ff8adafe9e4atagen2026-10-22 12:30 UTC139
7ff8adafe9e4atagen2026-10-22 12:30 UTC140
            for target in full_target.split("::") {
7ff8adafe9e4atagen2026-10-22 12:30 UTC141
                tree_node =
7ff8adafe9e4atagen2026-10-22 12:30 UTC142
                    match tree_node.entry(target.to_owned()).or_insert_with(|| Target::Node {
7ff8adafe9e4atagen2026-10-22 12:30 UTC143
                        number_of_errors: 0,
7ff8adafe9e4atagen2026-10-22 12:30 UTC144
                        number_of_warnings: 0,
7ff8adafe9e4atagen2026-10-22 12:30 UTC145
                        node: TargetTree::default(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC146
                    }) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC147
                        Target::Leaf(_) => panic!("Expect a `Node`, not a `Leaf`"),
7ff8adafe9e4atagen2026-10-22 12:30 UTC148
                        Target::Node { number_of_errors, number_of_warnings, node } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC149
                            // Adjust number of specific log levels all the targets.
7ff8adafe9e4atagen2026-10-22 12:30 UTC150
                            match level {
7ff8adafe9e4atagen2026-10-22 12:30 UTC151
                                Level::Error => *number_of_errors += 1,
7ff8adafe9e4atagen2026-10-22 12:30 UTC152
                                Level::Warn => *number_of_warnings += 1,
7ff8adafe9e4atagen2026-10-22 12:30 UTC153
                                _ => (),
7ff8adafe9e4atagen2026-10-22 12:30 UTC154
                            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC155
7ff8adafe9e4atagen2026-10-22 12:30 UTC156
                            node
7ff8adafe9e4atagen2026-10-22 12:30 UTC157
                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC158
                    };
7ff8adafe9e4atagen2026-10-22 12:30 UTC159
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC160
7ff8adafe9e4atagen2026-10-22 12:30 UTC161
            let span =
7ff8adafe9e4atagen2026-10-22 12:30 UTC162
                Span { message: message.to_owned(), log_line: line_nth, raw: spans.to_owned() };
7ff8adafe9e4atagen2026-10-22 12:30 UTC163
7ff8adafe9e4atagen2026-10-22 12:30 UTC164
            tree_node
7ff8adafe9e4atagen2026-10-22 12:30 UTC165
                // `LogLevels`
7ff8adafe9e4atagen2026-10-22 12:30 UTC166
                .entry("".to_owned())
7ff8adafe9e4atagen2026-10-22 12:30 UTC167
                .or_insert_with(|| Target::Leaf(LogLevels::default()))
7ff8adafe9e4atagen2026-10-22 12:30 UTC168
                .as_leaf_mut()
7ff8adafe9e4atagen2026-10-22 12:30 UTC169
                .expect("Expect a `Leaf`, not a `Node`")
7ff8adafe9e4atagen2026-10-22 12:30 UTC170
                // `LogLocations`
7ff8adafe9e4atagen2026-10-22 12:30 UTC171
                .entry(level)
7ff8adafe9e4atagen2026-10-22 12:30 UTC172
                .or_insert_with(LogLocations::default)
7ff8adafe9e4atagen2026-10-22 12:30 UTC173
                // `SourceFile`
7ff8adafe9e4atagen2026-10-22 12:30 UTC174
                .entry(source_file.to_owned())
7ff8adafe9e4atagen2026-10-22 12:30 UTC175
                .or_insert_with(Spans::default)
7ff8adafe9e4atagen2026-10-22 12:30 UTC176
                // `Spans`
7ff8adafe9e4atagen2026-10-22 12:30 UTC177
                .entry(source_line)
7ff8adafe9e4atagen2026-10-22 12:30 UTC178
                .or_insert_with(BTreeMap::default)
7ff8adafe9e4atagen2026-10-22 12:30 UTC179
                // `Span`!
7ff8adafe9e4atagen2026-10-22 12:30 UTC180
                .insert(date_time, span);
7ff8adafe9e4atagen2026-10-22 12:30 UTC181
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC182
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC183
7ff8adafe9e4atagen2026-10-22 12:30 UTC184
    fn render_target_tree(tree: TargetTree, buffer: &mut io::BufWriter<fs::File>) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC185
        if tree.is_empty() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC186
            return;
7ff8adafe9e4atagen2026-10-22 12:30 UTC187
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC188
7ff8adafe9e4atagen2026-10-22 12:30 UTC189
        writeln!(buffer, "<ul>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC190
7ff8adafe9e4atagen2026-10-22 12:30 UTC191
        for (target_name, target) in tree {
7ff8adafe9e4atagen2026-10-22 12:30 UTC192
            match target {
7ff8adafe9e4atagen2026-10-22 12:30 UTC193
                Target::Node { node, number_of_errors, number_of_warnings } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC194
                    render_node(target_name, node, number_of_errors, number_of_warnings, buffer);
7ff8adafe9e4atagen2026-10-22 12:30 UTC195
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC196
7ff8adafe9e4atagen2026-10-22 12:30 UTC197
                Target::Leaf(log_levels) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC198
                    render_leaf(log_levels, buffer);
7ff8adafe9e4atagen2026-10-22 12:30 UTC199
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC200
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC201
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC202
7ff8adafe9e4atagen2026-10-22 12:30 UTC203
        writeln!(buffer, "</ul>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC204
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC205
7ff8adafe9e4atagen2026-10-22 12:30 UTC206
    fn render_node(
7ff8adafe9e4atagen2026-10-22 12:30 UTC207
        target_name: String,
7ff8adafe9e4atagen2026-10-22 12:30 UTC208
        node: TargetTree,
7ff8adafe9e4atagen2026-10-22 12:30 UTC209
        number_of_errors: usize,
7ff8adafe9e4atagen2026-10-22 12:30 UTC210
        number_of_warnings: usize,
7ff8adafe9e4atagen2026-10-22 12:30 UTC211
        buffer: &mut io::BufWriter<fs::File>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC212
    ) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC213
        writeln!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC214
            buffer,
7ff8adafe9e4atagen2026-10-22 12:30 UTC215
            "<li>\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC216
            <details><summary>{target_name}\
7ff8adafe9e4atagen2026-10-22 12:30 UTC217
            <span class=\"aside\">\
7ff8adafe9e4atagen2026-10-22 12:30 UTC218
            <span class=\"highlight\"><input type=\"checkbox\" title=\"Highlight\" /></span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC219
            <span class=\"error\" title=\"Number of errors\">{number_of_errors}</span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC220
            <span class=\"warn\" title=\"Number of warnings\">{number_of_warnings}</span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC221
            </span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC222
            </summary>"
7ff8adafe9e4atagen2026-10-22 12:30 UTC223
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC224
        .unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC225
        render_target_tree(node, buffer);
7ff8adafe9e4atagen2026-10-22 12:30 UTC226
        writeln!(buffer, "</details></li>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC227
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC228
7ff8adafe9e4atagen2026-10-22 12:30 UTC229
    fn render_leaf(log_levels: LogLevels, buffer: &mut io::BufWriter<fs::File>) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC230
        writeln!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC231
            buffer,
7ff8adafe9e4atagen2026-10-22 12:30 UTC232
            "<li>\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC233
            <details open><summary>(root)\
7ff8adafe9e4atagen2026-10-22 12:30 UTC234
            <span class=\"aside\">\
7ff8adafe9e4atagen2026-10-22 12:30 UTC235
            <span class=\"highlight\"><input type=\"checkbox\" title=\"Highlight\" /></span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC236
            </span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC237
            </summary>"
7ff8adafe9e4atagen2026-10-22 12:30 UTC238
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC239
        .unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC240
7ff8adafe9e4atagen2026-10-22 12:30 UTC241
        if log_levels.is_empty().not() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC242
            writeln!(buffer, "<ul>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC243
7ff8adafe9e4atagen2026-10-22 12:30 UTC244
            for (level, locations) in log_levels {
7ff8adafe9e4atagen2026-10-22 12:30 UTC245
                render_locations(level, locations, buffer);
7ff8adafe9e4atagen2026-10-22 12:30 UTC246
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC247
7ff8adafe9e4atagen2026-10-22 12:30 UTC248
            writeln!(buffer, "</ul>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC249
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC250
7ff8adafe9e4atagen2026-10-22 12:30 UTC251
        writeln!(buffer, "</details></li>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC252
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC253
7ff8adafe9e4atagen2026-10-22 12:30 UTC254
    fn render_locations(
7ff8adafe9e4atagen2026-10-22 12:30 UTC255
        level: Level,
7ff8adafe9e4atagen2026-10-22 12:30 UTC256
        locations: LogLocations,
7ff8adafe9e4atagen2026-10-22 12:30 UTC257
        buffer: &mut io::BufWriter<fs::File>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC258
    ) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC259
        for (source_file, spans) in locations {
7ff8adafe9e4atagen2026-10-22 12:30 UTC260
            writeln!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC261
                buffer,
7ff8adafe9e4atagen2026-10-22 12:30 UTC262
                "<li>\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC263
                <details><summary><span class=\"{level_class}\">{level}</span> <code>{source_file}</code>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC264
                <span class=\"aside\">\
7ff8adafe9e4atagen2026-10-22 12:30 UTC265
                <span class=\"highlight\"><input type=\"checkbox\" title=\"Highlight\" /></span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC266
                </span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC267
                </summary>\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC268
                <ul>",
7ff8adafe9e4atagen2026-10-22 12:30 UTC269
                level_class = level.css(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC270
            )
7ff8adafe9e4atagen2026-10-22 12:30 UTC271
            .unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC272
            render_spans(spans, buffer);
7ff8adafe9e4atagen2026-10-22 12:30 UTC273
            writeln!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC274
                buffer,
7ff8adafe9e4atagen2026-10-22 12:30 UTC275
                "</ul>\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC276
                </details></li>"
7ff8adafe9e4atagen2026-10-22 12:30 UTC277
            )
7ff8adafe9e4atagen2026-10-22 12:30 UTC278
            .unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC279
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC280
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC281
7ff8adafe9e4atagen2026-10-22 12:30 UTC282
    fn render_spans(spans: Spans, buffer: &mut io::BufWriter<fs::File>) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC283
        for (source_line, all_spans) in spans {
7ff8adafe9e4atagen2026-10-22 12:30 UTC284
            writeln!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC285
                buffer,
7ff8adafe9e4atagen2026-10-22 12:30 UTC286
                "<li>\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC287
                <details><summary>{n} ocurrrence{s} at line <code>{source_line}</code>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC288
                <span class=\"aside\">\
7ff8adafe9e4atagen2026-10-22 12:30 UTC289
                <span class=\"highlight\"><input type=\"checkbox\" title=\"Highlight\" /></span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC290
                </span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC291
                </summary>\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC292
                <ol class=\"spans\">",
7ff8adafe9e4atagen2026-10-22 12:30 UTC293
                n = all_spans.len(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC294
                s = if all_spans.len() > 1 { "s" } else { "" }
7ff8adafe9e4atagen2026-10-22 12:30 UTC295
            )
7ff8adafe9e4atagen2026-10-22 12:30 UTC296
            .unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC297
7ff8adafe9e4atagen2026-10-22 12:30 UTC298
            for (date_time, span) in all_spans {
7ff8adafe9e4atagen2026-10-22 12:30 UTC299
                render_span(date_time, span, buffer);
7ff8adafe9e4atagen2026-10-22 12:30 UTC300
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC301
7ff8adafe9e4atagen2026-10-22 12:30 UTC302
            writeln!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC303
                buffer,
7ff8adafe9e4atagen2026-10-22 12:30 UTC304
                "</ol>\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC305
                </details></li>"
7ff8adafe9e4atagen2026-10-22 12:30 UTC306
            )
7ff8adafe9e4atagen2026-10-22 12:30 UTC307
            .unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC308
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC309
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC310
7ff8adafe9e4atagen2026-10-22 12:30 UTC311
    fn render_span(
7ff8adafe9e4atagen2026-10-22 12:30 UTC312
        date_time: DateTime<FixedOffset>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC313
        span: Span,
7ff8adafe9e4atagen2026-10-22 12:30 UTC314
        buffer: &mut io::BufWriter<fs::File>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC315
    ) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC316
        let Span { message, log_line, .. } = span;
7ff8adafe9e4atagen2026-10-22 12:30 UTC317
7ff8adafe9e4atagen2026-10-22 12:30 UTC318
        let message_parser = &*MESSAGE_PARSER;
7ff8adafe9e4atagen2026-10-22 12:30 UTC319
        let fields_parser = &*FIELDS_PARSER;
7ff8adafe9e4atagen2026-10-22 12:30 UTC320
7ff8adafe9e4atagen2026-10-22 12:30 UTC321
        if let Some(captures) = message_parser.captures(&message) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC322
            // Poor man HTML sanitizer.
7ff8adafe9e4atagen2026-10-22 12:30 UTC323
            let formatted_message = captures
7ff8adafe9e4atagen2026-10-22 12:30 UTC324
                .name("message")
7ff8adafe9e4atagen2026-10-22 12:30 UTC325
                .expect("Failed to parse the `message`")
7ff8adafe9e4atagen2026-10-22 12:30 UTC326
                .as_str()
7ff8adafe9e4atagen2026-10-22 12:30 UTC327
                .replace("&", "&amp;")
7ff8adafe9e4atagen2026-10-22 12:30 UTC328
                .replace("<", "&lt;")
7ff8adafe9e4atagen2026-10-22 12:30 UTC329
                .replace(">", "&gt;");
7ff8adafe9e4atagen2026-10-22 12:30 UTC330
7ff8adafe9e4atagen2026-10-22 12:30 UTC331
            writeln!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC332
                buffer,
7ff8adafe9e4atagen2026-10-22 12:30 UTC333
                "<li><time datetime=\"{date_time}\">{time}</time> {formatted_message} <span title=\"Log line\">[#{log_line}]</span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC334
                <span class=\"aside\">\
7ff8adafe9e4atagen2026-10-22 12:30 UTC335
                <span class=\"highlight\"><input type=\"checkbox\" title=\"Highlight\" /></span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC336
                </span>\
7ff8adafe9e4atagen2026-10-22 12:30 UTC337
                ",
7ff8adafe9e4atagen2026-10-22 12:30 UTC338
                date_time = date_time.format("%+"),
7ff8adafe9e4atagen2026-10-22 12:30 UTC339
                time = date_time.format("%H:%M:%S%.3f"),
7ff8adafe9e4atagen2026-10-22 12:30 UTC340
            )
7ff8adafe9e4atagen2026-10-22 12:30 UTC341
            .unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC342
7ff8adafe9e4atagen2026-10-22 12:30 UTC343
            // The message can contain HTML data. It happens notably when the
7ff8adafe9e4atagen2026-10-22 12:30 UTC344
            // homeserver is broken, and an HTML document is returned in some
7ff8adafe9e4atagen2026-10-22 12:30 UTC345
            // errors. We don't want to parse the fields in this case, because
7ff8adafe9e4atagen2026-10-22 12:30 UTC346
            // HTML breaks everything.
7ff8adafe9e4atagen2026-10-22 12:30 UTC347
            if !formatted_message.contains("!DOCTYPE html")
7ff8adafe9e4atagen2026-10-22 12:30 UTC348
                && let Some(fields) = captures.name("fields")
7ff8adafe9e4atagen2026-10-22 12:30 UTC349
            {
7ff8adafe9e4atagen2026-10-22 12:30 UTC350
                writeln!(buffer, "<ul class=\"fields\">").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC351
7ff8adafe9e4atagen2026-10-22 12:30 UTC352
                let mut fields = &message[fields.start()..fields.end()];
7ff8adafe9e4atagen2026-10-22 12:30 UTC353
7ff8adafe9e4atagen2026-10-22 12:30 UTC354
                while let Some(captures) = fields_parser.captures(fields) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC355
                    let name = captures
7ff8adafe9e4atagen2026-10-22 12:30 UTC356
                        .name("name")
7ff8adafe9e4atagen2026-10-22 12:30 UTC357
                        .expect("Failed to parse the `name` of the field")
7ff8adafe9e4atagen2026-10-22 12:30 UTC358
                        .as_str();
7ff8adafe9e4atagen2026-10-22 12:30 UTC359
                    let value = captures
7ff8adafe9e4atagen2026-10-22 12:30 UTC360
                        .name("value")
7ff8adafe9e4atagen2026-10-22 12:30 UTC361
                        .expect("Failed to parse the `value` of the field")
7ff8adafe9e4atagen2026-10-22 12:30 UTC362
                        .as_str();
7ff8adafe9e4atagen2026-10-22 12:30 UTC363
7ff8adafe9e4atagen2026-10-22 12:30 UTC364
                    writeln!(buffer, "<li><span>{name}</span><span>{value}</span></li>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC365
7ff8adafe9e4atagen2026-10-22 12:30 UTC366
                    if let Some(next_fields) = captures.name("next_fields") {
7ff8adafe9e4atagen2026-10-22 12:30 UTC367
                        fields = &fields[next_fields.start()..];
7ff8adafe9e4atagen2026-10-22 12:30 UTC368
                    } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC369
                        break;
7ff8adafe9e4atagen2026-10-22 12:30 UTC370
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC371
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC372
7ff8adafe9e4atagen2026-10-22 12:30 UTC373
                writeln!(buffer, "</ul>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC374
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC375
7ff8adafe9e4atagen2026-10-22 12:30 UTC376
            writeln!(buffer, "</li>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC377
        } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC378
            writeln!(buffer, "<li>{message}</li>").unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC379
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC380
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC381
7ff8adafe9e4atagen2026-10-22 12:30 UTC382
    let output = OUTPUT_TEMPLATE.replace("{log_file}", &log_path.to_string_lossy());
7ff8adafe9e4atagen2026-10-22 12:30 UTC383
7ff8adafe9e4atagen2026-10-22 12:30 UTC384
    output_file.write_all(output.as_bytes()).expect(
7ff8adafe9e4atagen2026-10-22 12:30 UTC385
        "Failed to write the
7ff8adafe9e4atagen2026-10-22 12:30 UTC386
    output",
7ff8adafe9e4atagen2026-10-22 12:30 UTC387
    );
7ff8adafe9e4atagen2026-10-22 12:30 UTC388
7ff8adafe9e4atagen2026-10-22 12:30 UTC389
    render_target_tree(tree, &mut output_file);
7ff8adafe9e4atagen2026-10-22 12:30 UTC390
7ff8adafe9e4atagen2026-10-22 12:30 UTC391
    output_file.write_all("</main></body></html>".as_bytes()).expect("Failed to write the output");
7ff8adafe9e4atagen2026-10-22 12:30 UTC392
7ff8adafe9e4atagen2026-10-22 12:30 UTC393
    println!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC394
        "\nNumber of analysed log lines: {number_of_analysed_lines}\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC395
        Number of matched lines: {number_of_matched_lines}\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC396
        Output file: {output_path:?}\n\
7ff8adafe9e4atagen2026-10-22 12:30 UTC397
        Done!"
7ff8adafe9e4atagen2026-10-22 12:30 UTC398
    );
7ff8adafe9e4atagen2026-10-22 12:30 UTC399
7ff8adafe9e4atagen2026-10-22 12:30 UTC400
    Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC401
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC402
7ff8adafe9e4atagen2026-10-22 12:30 UTC403
type TargetName = String;
7ff8adafe9e4atagen2026-10-22 12:30 UTC404
type TargetTree = BTreeMap<TargetName, Target>;
7ff8adafe9e4atagen2026-10-22 12:30 UTC405
7ff8adafe9e4atagen2026-10-22 12:30 UTC406
#[derive(Debug)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC407
enum Target {
7ff8adafe9e4atagen2026-10-22 12:30 UTC408
    Node {
7ff8adafe9e4atagen2026-10-22 12:30 UTC409
        /// Number of errors for this node entirely (including sub-trees).
7ff8adafe9e4atagen2026-10-22 12:30 UTC410
        number_of_errors: usize,
7ff8adafe9e4atagen2026-10-22 12:30 UTC411
7ff8adafe9e4atagen2026-10-22 12:30 UTC412
        /// Number of warnings for this node entirely (including sub-trees).
7ff8adafe9e4atagen2026-10-22 12:30 UTC413
        number_of_warnings: usize,
7ff8adafe9e4atagen2026-10-22 12:30 UTC414
7ff8adafe9e4atagen2026-10-22 12:30 UTC415
        /// The Node.
7ff8adafe9e4atagen2026-10-22 12:30 UTC416
        node: TargetTree,
7ff8adafe9e4atagen2026-10-22 12:30 UTC417
    },
7ff8adafe9e4atagen2026-10-22 12:30 UTC418
    Leaf(LogLevels),
7ff8adafe9e4atagen2026-10-22 12:30 UTC419
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC420
7ff8adafe9e4atagen2026-10-22 12:30 UTC421
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC422
enum Level {
7ff8adafe9e4atagen2026-10-22 12:30 UTC423
    Error,
7ff8adafe9e4atagen2026-10-22 12:30 UTC424
    Warn,
7ff8adafe9e4atagen2026-10-22 12:30 UTC425
    Info,
7ff8adafe9e4atagen2026-10-22 12:30 UTC426
    Debug,
7ff8adafe9e4atagen2026-10-22 12:30 UTC427
    Trace,
7ff8adafe9e4atagen2026-10-22 12:30 UTC428
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC429
7ff8adafe9e4atagen2026-10-22 12:30 UTC430
type LogLevels = BTreeMap<Level, LogLocations>;
7ff8adafe9e4atagen2026-10-22 12:30 UTC431
7ff8adafe9e4atagen2026-10-22 12:30 UTC432
type SourceFile = String;
7ff8adafe9e4atagen2026-10-22 12:30 UTC433
type SourceLine = usize;
7ff8adafe9e4atagen2026-10-22 12:30 UTC434
type LogLocations = HashMap<SourceFile, Spans>;
7ff8adafe9e4atagen2026-10-22 12:30 UTC435
7ff8adafe9e4atagen2026-10-22 12:30 UTC436
type Spans = BTreeMap<SourceLine, BTreeMap<DateTime<FixedOffset>, Span>>;
7ff8adafe9e4atagen2026-10-22 12:30 UTC437
7ff8adafe9e4atagen2026-10-22 12:30 UTC438
#[derive(Debug)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC439
struct Span {
7ff8adafe9e4atagen2026-10-22 12:30 UTC440
    message: String,
7ff8adafe9e4atagen2026-10-22 12:30 UTC441
    log_line: usize,
7ff8adafe9e4atagen2026-10-22 12:30 UTC442
    #[allow(dead_code)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC443
    raw: String,
7ff8adafe9e4atagen2026-10-22 12:30 UTC444
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC445
7ff8adafe9e4atagen2026-10-22 12:30 UTC446
impl Target {
7ff8adafe9e4atagen2026-10-22 12:30 UTC447
    fn as_leaf_mut(&mut self) -> Option<&mut LogLevels> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC448
        match self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC449
            Self::Leaf(log_levels) => Some(log_levels),
7ff8adafe9e4atagen2026-10-22 12:30 UTC450
            Self::Node { .. } => None,
7ff8adafe9e4atagen2026-10-22 12:30 UTC451
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC452
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC453
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC454
7ff8adafe9e4atagen2026-10-22 12:30 UTC455
impl Level {
7ff8adafe9e4atagen2026-10-22 12:30 UTC456
    fn css(&self) -> &str {
7ff8adafe9e4atagen2026-10-22 12:30 UTC457
        match self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC458
            Self::Error => "error",
7ff8adafe9e4atagen2026-10-22 12:30 UTC459
            Self::Warn => "warn",
7ff8adafe9e4atagen2026-10-22 12:30 UTC460
            Self::Info => "info",
7ff8adafe9e4atagen2026-10-22 12:30 UTC461
            Self::Debug => "debug",
7ff8adafe9e4atagen2026-10-22 12:30 UTC462
            Self::Trace => "trace",
7ff8adafe9e4atagen2026-10-22 12:30 UTC463
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC464
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC465
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC466
7ff8adafe9e4atagen2026-10-22 12:30 UTC467
impl FromStr for Level {
7ff8adafe9e4atagen2026-10-22 12:30 UTC468
    type Err = ();
7ff8adafe9e4atagen2026-10-22 12:30 UTC469
7ff8adafe9e4atagen2026-10-22 12:30 UTC470
    fn from_str(str: &str) -> Result<Self, Self::Err> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC471
        Ok(match str {
7ff8adafe9e4atagen2026-10-22 12:30 UTC472
            "ERROR" => Self::Error,
7ff8adafe9e4atagen2026-10-22 12:30 UTC473
            "WARN" => Self::Warn,
7ff8adafe9e4atagen2026-10-22 12:30 UTC474
            "INFO" => Self::Info,
7ff8adafe9e4atagen2026-10-22 12:30 UTC475
            "DEBUG" => Self::Debug,
7ff8adafe9e4atagen2026-10-22 12:30 UTC476
            "TRACE" => Self::Trace,
7ff8adafe9e4atagen2026-10-22 12:30 UTC477
            _ => return Err(()),
7ff8adafe9e4atagen2026-10-22 12:30 UTC478
        })
7ff8adafe9e4atagen2026-10-22 12:30 UTC479
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC480
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC481
7ff8adafe9e4atagen2026-10-22 12:30 UTC482
impl fmt::Display for Level {
7ff8adafe9e4atagen2026-10-22 12:30 UTC483
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
7ff8adafe9e4atagen2026-10-22 12:30 UTC484
        write!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC485
            formatter,
7ff8adafe9e4atagen2026-10-22 12:30 UTC486
            "{}",
7ff8adafe9e4atagen2026-10-22 12:30 UTC487
            match self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC488
                Self::Error => "ERROR",
7ff8adafe9e4atagen2026-10-22 12:30 UTC489
                Self::Warn => "WARNING",
7ff8adafe9e4atagen2026-10-22 12:30 UTC490
                Self::Info => "INFO",
7ff8adafe9e4atagen2026-10-22 12:30 UTC491
                Self::Debug => "DEBUG",
7ff8adafe9e4atagen2026-10-22 12:30 UTC492
                Self::Trace => "TRACE",
7ff8adafe9e4atagen2026-10-22 12:30 UTC493
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC494
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC495
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC496
}