Skip to content

Commit 906f00c

Browse files
authored
examples: add env-filter-explorer example (#3233)
This example demonstrates how to use the `tracing-subscriber` crate's `EnvFilter` type to filter log messages based on their metadata. The example provides a text area where users can input an environment filter string, and displays the log messages that would be captured by that filter.
1 parent 9c28e64 commit 906f00c

File tree

2 files changed

+260
-3
lines changed

2 files changed

+260
-3
lines changed

examples/Cargo.toml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ default = []
1111
[dev-dependencies]
1212

1313
# tracing crates
14-
tracing = { path = "../tracing", version = "0.2"}
15-
tracing-core = { path = "../tracing-core", version = "0.2"}
14+
tracing = { path = "../tracing", version = "0.2" }
15+
tracing-core = { path = "../tracing-core", version = "0.2" }
1616
tracing-error = { path = "../tracing-error" }
1717
tracing-flame = { path = "../tracing-flame" }
1818
tracing-tower = { version = "0.1.0", path = "../tracing-tower" }
1919
tracing-subscriber = { path = "../tracing-subscriber", version = "0.3", features = ["json", "env-filter"] }
2020
tracing-futures = { version = "0.3", path = "../tracing-futures", features = ["futures-01"] }
21-
tracing-attributes = { path = "../tracing-attributes", version = "0.2"}
21+
tracing-attributes = { path = "../tracing-attributes", version = "0.2" }
2222
tracing-log = { path = "../tracing-log", version = "0.2", features = ["env_logger"] }
2323
tracing-serde = { path = "../tracing-serde" }
2424
tracing-appender = { path = "../tracing-appender" }
@@ -51,5 +51,11 @@ tempfile = "3.3.0"
5151
snafu = "0.6.10"
5252
thiserror = "1.0.31"
5353

54+
# env-filter-explorer example
55+
ansi-to-tui = "7.0.0"
56+
ratatui = "0.29.0"
57+
crossterm = "0.28.1"
58+
tui-textarea = "0.7.0"
59+
5460
[lints]
5561
workspace = true
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
use std::{
2+
io::{self},
3+
sync::{Arc, Mutex},
4+
};
5+
6+
use ansi_to_tui::IntoText;
7+
use crossterm::event;
8+
use ratatui::{
9+
buffer::Buffer,
10+
layout::{Constraint, Layout, Rect},
11+
style::Stylize,
12+
widgets::{Block, Widget},
13+
DefaultTerminal, Frame,
14+
};
15+
use tracing_subscriber::{filter::ParseError, fmt::MakeWriter, EnvFilter};
16+
use tui_textarea::{Input, Key, TextArea};
17+
18+
/// A list of preset filters to make it easier to explore the filter syntax.
19+
///
20+
/// The UI allows you to select a preset filter with the up/down arrow keys.
21+
const PRESET_FILTERS: &[&str] = &[
22+
"trace",
23+
"debug",
24+
"info",
25+
"warn",
26+
"error",
27+
"[with_fields]",
28+
"[with_fields{foo}]",
29+
"[with_fields{bar}]",
30+
"[with_fields{foo=42}]",
31+
"[with_fields{bar=bar}]",
32+
"[with_fields{foo=99}]",
33+
"[with_fields{bar=nope}]",
34+
"[with_fields{nonexistent}]",
35+
"other_crate=info",
36+
"other_crate=debug",
37+
"trace,other_crate=warn",
38+
"warn,other_crate=info",
39+
];
40+
41+
fn main() -> io::Result<()> {
42+
let terminal = ratatui::init();
43+
let result = App::new().run(terminal);
44+
ratatui::restore();
45+
result
46+
}
47+
48+
struct App {
49+
filter: TextArea<'static>,
50+
preset_index: usize,
51+
exit: bool,
52+
log_widget: Result<LogWidget, ParseError>,
53+
}
54+
55+
impl App {
56+
/// Creates a new instance of the application, ready to run
57+
fn new() -> Self {
58+
let mut filter = TextArea::new(vec![PRESET_FILTERS[0].to_string()]);
59+
let title = "Env Filter Explorer. <Esc> to quit, <Up>/<Down> to select preset";
60+
filter.set_block(Block::bordered().title(title));
61+
Self {
62+
filter,
63+
preset_index: 0,
64+
exit: false,
65+
log_widget: Ok(LogWidget::default()),
66+
}
67+
}
68+
69+
/// The application's main loop until the user exits.
70+
fn run(mut self, mut terminal: DefaultTerminal) -> io::Result<()> {
71+
while !self.exit {
72+
self.log_widget = self.evaluate_filter();
73+
terminal.draw(|frame| self.render(frame))?;
74+
self.handle_event()?;
75+
}
76+
Ok(())
77+
}
78+
79+
/// Render the application with a filter input area and a log output area.
80+
fn render(&self, frame: &mut Frame) {
81+
let layout = Layout::vertical([Constraint::Length(3), Constraint::Fill(1)]);
82+
let [filter_area, main_area] = layout.areas(frame.area());
83+
frame.render_widget(&self.filter, filter_area);
84+
match &self.log_widget {
85+
Ok(log_widget) => frame.render_widget(log_widget, main_area),
86+
Err(error) => frame.render_widget(error.to_string().red(), main_area),
87+
}
88+
}
89+
90+
/// Handles a single terminal event (e.g. mouse, keyboard, resize).
91+
fn handle_event(&mut self) -> io::Result<()> {
92+
let event = event::read()?;
93+
let input = Input::from(event);
94+
match input.key {
95+
Key::Enter => return Ok(()), // ignore new lines
96+
Key::Esc => self.exit = true,
97+
Key::Up => self.select_previous_preset(),
98+
Key::Down => self.select_next_preset(),
99+
_ => self.add_input(input),
100+
}
101+
Ok(())
102+
}
103+
104+
/// Selects the previous preset filter in the list.
105+
fn select_previous_preset(&mut self) {
106+
self.select_preset(self.preset_index.saturating_sub(1));
107+
}
108+
109+
/// Selects the next preset filter in the list.
110+
fn select_next_preset(&mut self) {
111+
self.select_preset((self.preset_index + 1).min(PRESET_FILTERS.len() - 1));
112+
}
113+
114+
/// Selects a preset filter by index and updates the filter text area.
115+
fn select_preset(&mut self, index: usize) {
116+
self.preset_index = index;
117+
self.filter.select_all();
118+
self.filter.delete_line_by_head();
119+
self.filter.insert_str(PRESET_FILTERS[self.preset_index]);
120+
}
121+
122+
/// Handles normal keyboard input by adding it to the filter text area.
123+
fn add_input(&mut self, input: Input) {
124+
self.filter.input(input);
125+
}
126+
127+
/// Evaluates the current filter and returns a log widget with the filtered logs or an error.
128+
fn evaluate_filter(&mut self) -> Result<LogWidget, ParseError> {
129+
let filter = self.filter.lines()[0].to_string();
130+
let env_filter = EnvFilter::builder().parse(filter)?;
131+
let log_widget = LogWidget::default();
132+
let collector = tracing_subscriber::fmt()
133+
.with_env_filter(env_filter)
134+
.with_writer(log_widget.clone())
135+
.finish();
136+
tracing::collect::with_default(collector, || {
137+
simulate_logging();
138+
other_crate_span();
139+
});
140+
Ok(log_widget)
141+
}
142+
}
143+
144+
/// A writer that collects logs into a buffer and can be displayed as a widget.
145+
#[derive(Clone, Default, Debug)]
146+
struct LogWidget {
147+
buffer: Arc<Mutex<Vec<u8>>>,
148+
}
149+
150+
impl io::Write for LogWidget {
151+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
152+
self.buffer.lock().unwrap().write(buf)
153+
}
154+
155+
fn flush(&mut self) -> io::Result<()> {
156+
self.buffer.lock().unwrap().flush()
157+
}
158+
}
159+
160+
impl<'a> MakeWriter<'a> for LogWidget {
161+
type Writer = Self;
162+
163+
fn make_writer(&'a self) -> Self::Writer {
164+
self.clone()
165+
}
166+
}
167+
168+
impl Widget for &LogWidget {
169+
/// Displays the logs that have been collected in the buffer.
170+
///
171+
/// If the buffer is empty, it displays "No matching logs".
172+
fn render(self, area: Rect, buf: &mut Buffer) {
173+
let buffer = self.buffer.lock().unwrap();
174+
let string = String::from_utf8_lossy(&buffer).to_string();
175+
if string.is_empty() {
176+
"No matching logs".render(area, buf);
177+
return;
178+
}
179+
string
180+
.into_text() // convert a string with ANSI escape codes into ratatui Text
181+
.unwrap_or_else(|err| format!("Error parsing output: {err}").into())
182+
.render(area, buf);
183+
}
184+
}
185+
186+
#[tracing::instrument]
187+
fn simulate_logging() {
188+
tracing::info!("This is an info message");
189+
tracing::error!("This is an error message");
190+
tracing::warn!("This is a warning message");
191+
tracing::debug!("This is a debug message");
192+
tracing::trace!("This is a trace message");
193+
194+
with_fields(42, "bar");
195+
with_fields(99, "nope");
196+
197+
trace_span();
198+
debug_span();
199+
info_span();
200+
warn_span();
201+
error_span();
202+
}
203+
204+
#[tracing::instrument]
205+
fn with_fields(foo: u32, bar: &'static str) {
206+
tracing::info!(foo, bar, "This is an info message with fields");
207+
}
208+
209+
#[tracing::instrument(level = "trace")]
210+
fn trace_span() {
211+
tracing::error!("Error message inside a span with trace level");
212+
tracing::info!("Info message inside a span with trace level");
213+
tracing::trace!("Trace message inside a span with trace level");
214+
}
215+
216+
#[tracing::instrument]
217+
fn debug_span() {
218+
tracing::error!("Error message inside a span with debug level");
219+
tracing::info!("Info message inside a span with debug level");
220+
tracing::debug!("Debug message inside a span with debug level");
221+
}
222+
223+
#[tracing::instrument]
224+
fn info_span() {
225+
tracing::error!("Error message inside a span with info level");
226+
tracing::info!("Info message inside a span with info level");
227+
tracing::debug!("Debug message inside a span with info level");
228+
}
229+
230+
#[tracing::instrument]
231+
fn warn_span() {
232+
tracing::error!("Error message inside a span with warn level");
233+
tracing::info!("Info message inside a span with warn level");
234+
tracing::debug!("Debug message inside a span with warn level");
235+
}
236+
237+
#[tracing::instrument]
238+
fn error_span() {
239+
tracing::error!("Error message inside a span with error level");
240+
tracing::info!("Info message inside a span with error level");
241+
tracing::debug!("Debug message inside a span with error level");
242+
}
243+
244+
#[tracing::instrument(target = "other_crate")]
245+
fn other_crate_span() {
246+
tracing::error!(target: "other_crate", "An error message from another crate");
247+
tracing::warn!(target: "other_crate", "A warning message from another crate");
248+
tracing::info!(target: "other_crate", "An info message from another crate");
249+
tracing::debug!(target: "other_crate", "A debug message from another crate");
250+
tracing::trace!(target: "other_crate", "A trace message from another crate");
251+
}

0 commit comments

Comments
 (0)