tracing_subscriber_sqlite/
lib.rs

1use std::{
2    collections::HashMap,
3    fmt::Write,
4    sync::{atomic::AtomicU64, Mutex},
5};
6
7use rusqlite::Connection;
8use time::OffsetDateTime;
9use tracing::{field::Visit, level_filters::LevelFilter, span};
10#[cfg(feature = "tracing-log")]
11use tracing_log::NormalizeEvent;
12
13pub const SQL_SCHEMA: &str = include_str!("../schema/log.sql");
14
15pub fn prepare_database(conn: &Connection) -> rusqlite::Result<()> {
16    conn.execute(SQL_SCHEMA, ()).map(|_| {})
17}
18
19pub struct Subscriber {
20    id: AtomicU64,
21    connection: Mutex<Connection>,
22    max_level: LevelFilter,
23    black_list: Option<Box<[&'static str]>>,
24    white_list: Option<Box<[&'static str]>>,
25}
26
27impl Subscriber {
28    pub fn new(connection: Connection) -> Self {
29        Self::with_max_level(connection, LevelFilter::TRACE)
30    }
31
32    fn with_details(
33        connection: Mutex<Connection>,
34        max_level: LevelFilter,
35        black_list: Option<Box<[&'static str]>>,
36        white_list: Option<Box<[&'static str]>>,
37    ) -> Self {
38        Self {
39            id: AtomicU64::new(1),
40            connection,
41            max_level,
42            black_list,
43            white_list,
44        }
45    }
46
47    pub fn with_max_level(connection: Connection, max_level: LevelFilter) -> Self {
48        Self::with_details(Mutex::new(connection), max_level, None, None)
49    }
50
51    pub fn black_list(&self) -> Option<&[&'static str]> {
52        self.black_list.as_deref()
53    }
54
55    pub fn white_list(&self) -> Option<&[&'static str]> {
56        self.white_list.as_deref()
57    }
58}
59
60impl tracing::Subscriber for Subscriber {
61    fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
62        metadata.level() <= &self.max_level
63            && metadata.module_path().map_or(true, |m| {
64                let starts_with = |module: &&str| m.starts_with(module);
65                let has_module = |modules: &[&str]| modules.iter().any(starts_with);
66                self.white_list().map_or(true, has_module)
67                    && !(self.black_list().map_or(false, has_module))
68            })
69    }
70
71    fn max_level_hint(&self) -> Option<tracing::level_filters::LevelFilter> {
72        Some(self.max_level)
73    }
74
75    fn new_span(&self, _span: &span::Attributes<'_>) -> span::Id {
76        let id = self.id.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
77        span::Id::from_u64(id)
78    }
79
80    fn record(&self, _span: &span::Id, _values: &span::Record<'_>) {}
81
82    fn record_follows_from(&self, _span: &span::Id, _follows: &span::Id) {}
83
84    fn event(&self, event: &tracing::Event<'_>) {
85        #[cfg(feature = "tracing-log")]
86        let normalized_meta = event.normalized_metadata();
87        #[cfg(feature = "tracing-log")]
88        let meta = match normalized_meta.as_ref() {
89            Some(meta) if self.enabled(meta) => meta,
90            None => event.metadata(),
91            _ => return,
92        };
93
94        #[cfg(not(feature = "tracing-log"))]
95        let meta = event.metadata();
96
97        let level = meta.level().as_str();
98        let moudle = meta.module_path();
99        let file = meta.file();
100        let line = meta.line();
101
102        let mut message = String::new();
103        let mut kvs = HashMap::new();
104
105        event.record(&mut Visitor {
106            message: &mut message,
107            kvs: &mut kvs,
108        });
109
110        let conn = self.connection.lock().unwrap();
111        let now = OffsetDateTime::now_utc();
112        conn.execute(
113            "INSERT INTO logs_v0 (time, level, module, file, line, message, structured) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
114            (now, level, moudle, file, line, message, serde_json::to_string(&kvs).unwrap()),
115        )
116        .unwrap();
117    }
118
119    fn enter(&self, _span: &span::Id) {}
120
121    fn exit(&self, _span: &span::Id) {}
122}
123
124struct Visitor<'a> {
125    pub message: &'a mut String,
126    pub kvs: &'a mut HashMap<&'static str, String>, // todo: store structured key-value data
127}
128
129impl<'a> Visit for Visitor<'a> {
130    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
131        match field.name() {
132            "message" => write!(self.message, "{value:?}").unwrap(),
133            #[cfg(feature = "tracing-log")]
134            "log.line" | "log.file" | "log.target" | "log.module_path" => {}
135            name => {
136                self.kvs.insert(name, format!("{value:?}"));
137            }
138        }
139    }
140}
141
142#[derive(Debug)]
143pub struct SubscriberBuilder {
144    max_level: LevelFilter,
145    black_list: Option<Box<[&'static str]>>,
146    white_list: Option<Box<[&'static str]>>,
147}
148
149impl SubscriberBuilder {
150    pub fn new() -> Self {
151        Self::default()
152    }
153
154    pub fn with_max_level(self, max_level: LevelFilter) -> Self {
155        Self { max_level, ..self }
156    }
157
158    /// A log will not be recorded if its module path starts with any of item in the black list.
159    pub fn with_black_list(self, black_list: impl IntoIterator<Item = &'static str>) -> Self {
160        Self {
161            black_list: Some(black_list.into_iter().collect()),
162            ..self
163        }
164    }
165
166    /// A log may be recorded only if its module path starts with any of item in the white list.
167    pub fn with_white_list(self, white_list: impl IntoIterator<Item = &'static str>) -> Self {
168        Self {
169            white_list: Some(white_list.into_iter().collect()),
170            ..self
171        }
172    }
173
174    pub fn build(self, conn: Connection) -> Subscriber {
175        Subscriber::with_details(
176            Mutex::new(conn),
177            self.max_level,
178            self.black_list,
179            self.white_list,
180        )
181    }
182
183    pub fn build_prepared(self, conn: Connection) -> Result<Subscriber, rusqlite::Error> {
184        prepare_database(&conn)?;
185
186        Ok(self.build(conn))
187    }
188}
189
190impl Default for SubscriberBuilder {
191    fn default() -> Self {
192        Self {
193            max_level: LevelFilter::DEBUG,
194            black_list: None,
195            white_list: None,
196        }
197    }
198}