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}