tracing_line_filter/
lib.rs

1//! A [`tracing`] filter for enabling individual [spans] and [events] by line
2//! number.
3//!
4//! [`tracing`] is a framework for instrumenting Rust programs to collect
5//! scoped, structured, and async-aware diagnostics. The [`tracing-subscriber`]
6//! crate's [`EnvFilter`] type provides a mechanism for controlling what
7//! `tracing` [spans] and [events] are collected by matching their targets,
8//! verbosity levels, and fields. In some cases, though, it can be useful to
9//! toggle on or off individual spans or events with a higher level of
10//! granularity. Therefore, this crate provides a filtering [`Layer`] that
11//! enables individual spans and events based on their module path/file path and
12//! line numbers.
13//!
14//! Since the implementation of this filter is rather simple, the source code of
15//! this crate is also useful as an example to `tracing` users who want to
16//! implement their own filtering logic.
17//!
18//! # Usage
19//!
20//! First, add this to your Cargo.toml:
21//!
22//! ```toml
23//! tracing-line-filter = "0.1"
24//! ```
25//!
26//! ## Examples
27//!
28//! Enabling events by line:
29//!
30//! ```rust
31//! use tracing_line_filter::LineFilter;
32//! mod some_module {
33//!     pub fn do_stuff() {
34//!         tracing::info!("i'm doing stuff");
35//!         tracing::debug!("i'm also doing stuff!");
36//!     }
37//! }
38//!
39//! fn main() {
40//!     use tracing_subscriber::prelude::*;
41//!
42//!     let mut filter = LineFilter::default();
43//!     filter
44//!         .enable_by_mod("my_crate::some_module", 6)
45//!         .enable_by_mod("my_crate", 25)
46//!         .enable_by_mod("my_crate", 27);
47//!
48//!     tracing_subscriber::registry()
49//!         .with(tracing_subscriber::fmt::layer().pretty())
50//!         .with(filter)
51//!         .init();
52//!
53//!     tracing::info!("i'm not enabled");
54//!     tracing::debug!("i'm enabled!");
55//!     some_module::do_stuff();
56//!     tracing::trace!("hi!");
57//! }
58//! ```
59//!
60//! Chaining a [`LineFilter`] with a `tracing_subscriber` [`EnvFilter`]:
61//!
62//! ```rust
63//! use tracing_line_filter::LineFilter;
64//! use tracing_subscriber::EnvFilter;
65//!
66//! mod some_module {
67//!     pub fn do_stuff() {
68//!         tracing::info!("i'm doing stuff");
69//!         tracing::debug!("i'm also doing stuff!");
70//!         // This won't be enabled, because it's at the TRACE level, and the
71//!         // `EnvFilter` only enables up to the DEBUG level.
72//!         tracing::trace!("doing very verbose stuff");
73//!     }
74//! }
75//!
76//! fn main() {
77//!     use tracing_subscriber::prelude::*;
78//!
79//!     let mut filter = LineFilter::default();
80//!     filter
81//!         .enable_by_mod("with_env_filter", 30)
82//!         .enable_by_mod("with_env_filter", 33)
83//!         // use an `EnvFilter` that enables DEBUG and lower in `some_module`,
84//!         // and everything at the ERROR level.
85//!         .with_env_filter(EnvFilter::new("error,with_env_filter::some_module=debug"));
86//!
87//!     tracing_subscriber::registry()
88//!         .with(tracing_subscriber::fmt::layer().pretty())
89//!         .with(filter)
90//!         .init();
91//!
92//!     tracing::info!("i'm not enabled");
93//!     tracing::debug!("i'm enabled!!");
94//!     some_module::do_stuff();
95//!     tracing::trace!("hi!");
96//!
97//!     // This will be enabled by the `EnvFilter`.
98//!     tracing::error!("an error!");
99//! }
100//! ```
101//!
102//! [`tracing`]: https://docs.rs/tracing
103//! [spans]: https://docs.rs/tracing/latest/tracing/#spans
104//! [events]: https://docs.rs/tracing/latest/tracing/#events
105//! [`tracing-subscriber`]: https://docs.rs/tracing-subscriber
106//! [`EnvFilter`]: tracing_subscriber::EnvFilter
107//! [`Layer`]: tracing_subscriber::Layer
108
109use std::borrow::Cow;
110use std::collections::HashSet;
111use std::fmt;
112use std::path::{Path, PathBuf};
113use tracing_core::{subscriber::Interest, Metadata, Subscriber};
114use tracing_subscriber::{
115    filter::EnvFilter,
116    layer::{self, Layer},
117};
118
119/// A filter for enabling spans and events by file/module path and line number.
120#[derive(Debug, Default)]
121pub struct LineFilter {
122    by_module: HashSet<(Cow<'static, str>, u32)>,
123    by_file: HashSet<(Cow<'static, str>, u32)>,
124    env: Option<EnvFilter>,
125}
126
127/// Indicates a file path was invalid for use in a `LineFilter`.
128#[derive(Debug)]
129pub struct BadPath {
130    path: PathBuf,
131    message: &'static str,
132}
133
134impl LineFilter {
135    /// Returns a new `LineFilter`.
136    ///
137    /// By default, no spans and events are enabled.
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    /// Composes `self` with an [`EnvFilter`] that will be checked for spans and
143    /// events if they are not in the lists of enabled `(module, line)` and
144    /// `(file, line)` pairs.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use tracing_subscriber::EnvFilter;
150    /// use tracing_line_filter::LineFilter;
151    ///
152    /// let mut filter = LineFilter::default();
153    /// filter
154    ///     .enable_by_mod("my_crate", 28)
155    ///     .enable_by_mod("my_crate::my_module", 16)
156    ///     // use an ``EnvFilter` that enables DEBUG and lower in `some_module`, and
157    ///     // all ERROR spans or events, regardless of location.
158    ///     .with_env_filter(EnvFilter::new("error,my_crate::some_other_module=debug"));
159    /// ```
160    pub fn with_env_filter(&mut self, env: EnvFilter) -> &mut Self {
161        self.env = Some(env);
162        self
163    }
164
165    /// Enable a span or event in the Rust module `module` on line `line`.
166    ///
167    /// # Notes
168    ///
169    /// * Module paths should include the name of the crate. For
170    ///   example, the module `my_module` in `my_crate` would have path
171    ///  `my_crate::my_module`.
172    /// * If no span or event exists at the specified location, or if the module
173    ///   path does not exist, this will silently do nothing.
174    /// * Line numbers are relative to the start of the *file*, not to the start
175    ///   of the module. If a module does not have its own file (i.e., it's
176    ///   defined like `mod my_module { ... }`), the line number is relative to
177    ///   the containing file.
178    ///
179    /// # Examples
180    ///
181    /// Enabling an event:
182    /// ```
183    /// use tracing_line_filter::LineFilter;
184    ///
185    /// mod my_module {
186    ///     pub fn do_stuff() {
187    ///         tracing::info!("doing stuff!")
188    ///         // ...
189    ///     }
190    /// }
191    ///
192    /// // Build a line filter to enable the event in `do_stuff`.
193    /// let mut filter = LineFilter::default();
194    /// filter.enable_by_mod("my_crate::my_module", 5);
195    ///
196    /// // Build a subscriber and enable that filter.
197    /// use tracing_subscriber::prelude::*;
198    ///
199    /// tracing_subscriber::registry()
200    ///     .with(tracing_subscriber::fmt::layer())
201    ///     .with(filter)
202    ///     .init();
203    ///
204    /// // Now, the event is enabled!
205    /// my_module::do_stuff();
206    /// ```
207    ///
208    /// The [`std::module_path!()`] macro can be used to enable an event in the
209    /// current module:
210    /// ```
211    /// use tracing_line_filter::LineFilter;
212    ///
213    /// pub fn do_stuff() {
214    ///     tracing::info!("doing stuff!")
215    ///     // ...
216    /// }
217    ///
218    /// let mut filter = LineFilter::default();
219    /// filter.enable_by_mod(module_path!(), 4);
220    ///
221    ///  // ...
222    /// ```
223    pub fn enable_by_mod(&mut self, module: impl Into<Cow<'static, str>>, line: u32) -> &mut Self {
224        self.by_module.insert((module.into(), line));
225        self
226    }
227
228    /// Enable a span or event in the file `file` on line `line`.
229    ///
230    /// # Notes
231    ///
232    /// These file paths must match the file paths emitted by the
233    /// [`std::file!()`] macro. In particular:
234    ///
235    /// * Paths must be absolute.
236    /// * Paths must be Rust source code files.
237    /// * Paths must be valid UTF-8.
238    ///
239    /// This method validates paths and returns an error if the path is not
240    /// valid for use in a `LineFilter`.
241    ///
242    /// Since these paths are absolute, files in Cargo dependencies will include
243    /// their full path in the local Cargo registry. For example:
244    /// ```text
245    /// /home/eliza/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.0.0/src/util/trace.rs
246    /// ```
247    ///
248    /// Therefore, it can be challenging for humans to determine the correct
249    /// path to a file, especially when it is in a dependency. For this reason,
250    /// it's likely best to prefer Rust module paths rather than file paths when
251    /// accepting input from users directly. Enabling events and spans by file
252    /// paths is primarily intended for use by automated tools.
253    pub fn enable_by_file(
254        &mut self,
255        file: impl AsRef<Path>,
256        line: u32,
257    ) -> Result<&mut Self, BadPath> {
258        let file = file.as_ref();
259        if !file.is_absolute() {
260            return Err(BadPath::new(file, "file paths must be absolute"));
261        }
262
263        if file.extension().and_then(std::ffi::OsStr::to_str) != Some("rs") {
264            return Err(BadPath::new(file, "files must be Rust source code files"));
265        }
266
267        let file = file
268            .to_str()
269            .ok_or_else(|| BadPath::new(file, "file paths must be valid utf-8"))?
270            .to_owned();
271
272        self.by_file.insert((Cow::Owned(file), line));
273        Ok(self)
274    }
275
276    /// Enable a set of spans or events by module path.
277    ///
278    /// This is equivalent to repeatedly calling [`enable_by_mod`].
279    ///
280    ///
281    /// # Examples
282    /// ```
283    /// use tracing_line_filter::LineFilter;
284    ///
285    /// mod foo {
286    ///     pub fn do_stuff() {
287    ///         tracing::info!("doing stuff!")
288    ///         // ...
289    ///     }
290    /// }
291    ///
292    /// mod bar {
293    ///     pub fn do_other_stuff() {
294    ///         tracing::debug!("doing some different stuff...")
295    ///         // ...
296    ///     }
297    /// }
298    ///
299    /// // Build a line filter to enable the events in `do_stuff`
300    /// // and `do_other_stuff`.
301    /// let mut filter = LineFilter::default();
302    /// filter.with_modules(vec![
303    ///    ("my_crate::foo", 5),
304    ///    ("my_crate::bar", 12)
305    /// ]);
306    ///
307    /// // Build a subscriber and enable that filter.
308    /// use tracing_subscriber::prelude::*;
309    ///
310    /// tracing_subscriber::registry()
311    ///     .with(tracing_subscriber::fmt::layer())
312    ///     .with(filter)
313    ///     .init();
314    ///
315    /// // Now, the events are enabled!
316    /// foo::do_stuff();
317    /// bar::do_other_stuff();
318    /// ```
319    pub fn with_modules<I>(&mut self, modules: impl IntoIterator<Item = (I, u32)>) -> &mut Self
320    where
321        I: Into<Cow<'static, str>>,
322    {
323        let modules = modules
324            .into_iter()
325            .map(|(module, line)| (module.into(), line));
326        self.by_module.extend(modules);
327        self
328    }
329
330    /// Enable a set of spans or events by file path.
331    ///
332    /// This is equivalent to repeatedly calling [`enable_by_file`], and follows
333    /// the same path validation rules as that method. See the documentation for
334    /// [`enable_by_file`] for details.
335    pub fn with_files<I>(
336        &mut self,
337        files: impl IntoIterator<Item = (I, u32)>,
338    ) -> Result<&mut Self, BadPath>
339    where
340        I: AsRef<Path>,
341    {
342        for (file, line) in files {
343            self.enable_by_file(file, line)?;
344        }
345        Ok(self)
346    }
347
348    fn contains(&self, metadata: &Metadata<'_>) -> bool {
349        if let Some(line) = metadata.line() {
350            let module = metadata.module_path().unwrap_or_else(|| metadata.target());
351            let location = (Cow::Borrowed(module), line);
352            if self.by_module.contains(&location) {
353                return true;
354            }
355
356            if let Some(file) = metadata.file() {
357                let location = (Cow::Borrowed(file), line);
358                if self.by_file.contains(&location) {
359                    return true;
360                }
361            }
362        }
363
364        false
365    }
366}
367
368impl<S: Subscriber> Layer<S> for LineFilter
369where
370    EnvFilter: Layer<S>,
371{
372    fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
373        if self.contains(metadata) {
374            return Interest::always();
375        }
376
377        self.env
378            .as_ref()
379            .map(|env| env.register_callsite(metadata))
380            .unwrap_or_else(Interest::never)
381    }
382
383    fn enabled(&self, metadata: &Metadata<'_>, cx: layer::Context<'_, S>) -> bool {
384        if self.contains(metadata) {
385            return true;
386        }
387
388        self.env
389            .as_ref()
390            .map(|env| env.enabled(metadata, cx))
391            .unwrap_or(false)
392    }
393}
394
395// === impl BadPath ===
396
397impl fmt::Display for BadPath {
398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399        write!(
400            f,
401            "invalid path '{}': {}",
402            self.path.display(),
403            self.message
404        )
405    }
406}
407
408impl std::error::Error for BadPath {}
409
410impl BadPath {
411    fn new(path: &Path, message: &'static str) -> Self {
412        Self {
413            path: path.to_path_buf(),
414            message,
415        }
416    }
417}