1
use std::collections::BTreeMap;
2
use std::fs::{self, File};
3
use std::io::Write as _;
4
use std::path::{Path, PathBuf};
5
use std::sync::{Arc, Mutex};
6

            
7
use anyhow::Context as _;
8
use tracing::field::{Field, Visit};
9
use tracing::{Event, Subscriber};
10
use tracing_subscriber::Layer;
11
use tracing_subscriber::layer::Context;
12

            
13
#[derive(Clone)]
14
pub struct RuleTraceAggregatesHandle {
15
    state: Arc<Mutex<RuleTraceAggregatesState>>,
16
}
17

            
18
pub struct RuleTraceAggregatesLayer {
19
    state: Arc<Mutex<RuleTraceAggregatesState>>,
20
}
21

            
22
struct RuleTraceAggregatesState {
23
    path: PathBuf,
24
    tmp_path: PathBuf,
25
    total_rule_applications: usize,
26
    counts: BTreeMap<String, usize>,
27
}
28

            
29
#[derive(Default)]
30
struct RuleNameVisitor {
31
    rule_name: Option<String>,
32
}
33

            
34
impl RuleTraceAggregatesHandle {
35
12
    pub fn new(path: PathBuf) -> anyhow::Result<Self> {
36
12
        let state = RuleTraceAggregatesState::new(path);
37
12
        state.write_snapshot()?;
38

            
39
12
        Ok(Self {
40
12
            state: Arc::new(Mutex::new(state)),
41
12
        })
42
12
    }
43

            
44
12
    pub fn layer(&self) -> RuleTraceAggregatesLayer {
45
12
        RuleTraceAggregatesLayer {
46
12
            state: Arc::clone(&self.state),
47
12
        }
48
12
    }
49

            
50
12
    pub fn flush(&self) {
51
12
        self.state
52
12
            .lock()
53
12
            .expect("rule trace aggregate state lock poisoned")
54
12
            .write_snapshot()
55
12
            .expect("failed to write rule trace aggregates")
56
12
    }
57
}
58

            
59
impl<S> Layer<S> for RuleTraceAggregatesLayer
60
where
61
    S: Subscriber,
62
{
63
1622
    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
64
1622
        let mut visitor = RuleNameVisitor::default();
65
1622
        event.record(&mut visitor);
66

            
67
1622
        let Some(rule_name) = visitor.rule_name else {
68
            return;
69
        };
70

            
71
1622
        self.state
72
1622
            .lock()
73
1622
            .expect("rule trace aggregate state lock poisoned")
74
1622
            .record_rule(rule_name)
75
1622
            .expect("failed to write rule trace aggregates");
76
1622
    }
77
}
78

            
79
impl RuleTraceAggregatesState {
80
12
    fn new(path: PathBuf) -> Self {
81
12
        Self {
82
12
            tmp_path: temporary_output_path(&path),
83
12
            path,
84
12
            total_rule_applications: 0,
85
12
            counts: BTreeMap::new(),
86
12
        }
87
12
    }
88

            
89
1622
    fn record_rule(&mut self, rule_name: String) -> anyhow::Result<()> {
90
1622
        self.total_rule_applications += 1;
91
1622
        *self.counts.entry(rule_name).or_insert(0) += 1;
92
1622
        self.write_snapshot()
93
1622
    }
94

            
95
1646
    fn write_snapshot(&self) -> anyhow::Result<()> {
96
1646
        let mut rows: Vec<_> = self.counts.iter().collect();
97
23464
        rows.sort_by(|(rule_name_a, count_a), (rule_name_b, count_b)| {
98
23464
            count_b
99
23464
                .cmp(count_a)
100
23464
                .then_with(|| rule_name_a.cmp(rule_name_b))
101
23464
        });
102

            
103
1646
        let mut file = File::create(&self.tmp_path).with_context(|| {
104
            format!(
105
                "Unable to create temporary aggregate trace file {}",
106
                self.tmp_path.display()
107
            )
108
        })?;
109

            
110
1646
        writeln!(
111
1646
            file,
112
            "total_rule_applications: {}",
113
            self.total_rule_applications
114
        )?;
115
10438
        for (rule_name, count) in rows {
116
10438
            writeln!(file, "{count:6} {rule_name}")?;
117
        }
118
1646
        file.flush()
119
1646
            .expect("failed to flush temporary aggregate trace file");
120

            
121
1646
        fs::rename(&self.tmp_path, &self.path).with_context(|| {
122
            format!(
123
                "Unable to move aggregate trace file into place at {}",
124
                self.path.display()
125
            )
126
        })?;
127

            
128
1646
        Ok(())
129
1646
    }
130
}
131

            
132
impl Visit for RuleNameVisitor {
133
1622
    fn record_str(&mut self, field: &Field, value: &str) {
134
1622
        if field.name() == "rule_name" {
135
1622
            self.rule_name = Some(value.to_owned());
136
1622
        }
137
1622
    }
138

            
139
1622
    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
140
1622
        if field.name() == "rule_name" && self.rule_name.is_none() {
141
            self.rule_name = Some(format!("{value:?}").trim_matches('"').to_owned());
142
1622
        }
143
1622
    }
144
}
145

            
146
12
fn temporary_output_path(path: &Path) -> PathBuf {
147
12
    let filename = path
148
12
        .file_name()
149
12
        .map(|name| name.to_string_lossy().into_owned())
150
12
        .unwrap_or_else(|| "rule-trace-aggregates".to_owned());
151

            
152
12
    path.with_file_name(format!(".{filename}.tmp"))
153
12
}