zebrad/components/tracing.rs
1//! Tracing and logging infrastructure for Zebra.
2
3use std::{
4 io::IsTerminal,
5 net::SocketAddr,
6 ops::{Deref, DerefMut},
7 path::PathBuf,
8};
9
10use serde::{Deserialize, Serialize};
11
12mod component;
13mod endpoint;
14
15#[cfg(feature = "flamegraph")]
16mod flame;
17
18#[cfg(feature = "opentelemetry")]
19mod otel;
20
21pub use component::Tracing;
22pub use endpoint::TracingEndpoint;
23
24#[cfg(feature = "flamegraph")]
25pub use flame::{layer, Grapher};
26
27/// Tracing configuration section: outer config after cross-field defaults are applied.
28///
29/// This is a wrapper type that dereferences to the inner config type.
30///
31//
32// TODO: replace with serde's finalizer attribute when that feature is implemented.
33// we currently use the recommended workaround of a wrapper struct with from/into attributes.
34// https://github.com/serde-rs/serde/issues/642#issuecomment-525432907
35#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
36#[serde(
37 deny_unknown_fields,
38 default,
39 from = "InnerConfig",
40 into = "InnerConfig"
41)]
42pub struct Config {
43 inner: InnerConfig,
44}
45
46impl Deref for Config {
47 type Target = InnerConfig;
48
49 fn deref(&self) -> &Self::Target {
50 &self.inner
51 }
52}
53
54impl DerefMut for Config {
55 fn deref_mut(&mut self) -> &mut Self::Target {
56 &mut self.inner
57 }
58}
59
60impl From<InnerConfig> for Config {
61 fn from(mut inner: InnerConfig) -> Self {
62 inner.log_file = runtime_default_log_file(inner.log_file, inner.progress_bar);
63
64 Self { inner }
65 }
66}
67
68impl From<Config> for InnerConfig {
69 fn from(mut config: Config) -> Self {
70 config.log_file = disk_default_log_file(config.log_file.clone(), config.progress_bar);
71
72 config.inner
73 }
74}
75
76/// Tracing configuration section: inner config used to deserialize and apply cross-field defaults.
77#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
78#[serde(deny_unknown_fields, default)]
79pub struct InnerConfig {
80 /// Whether to use colored terminal output, if available.
81 ///
82 /// Colored terminal output is automatically disabled if an output stream
83 /// is connected to a file. (Or another non-terminal device.)
84 ///
85 /// Defaults to `true`, which automatically enables colored output to
86 /// terminals.
87 pub use_color: bool,
88
89 /// Whether to force the use of colored terminal output, even if it's not available.
90 ///
91 /// Will force Zebra to use colored terminal output even if it does not detect that the output
92 /// is a terminal that supports colors.
93 ///
94 /// Defaults to `false`, which keeps the behavior of `use_color`.
95 pub force_use_color: bool,
96
97 /// The filter used for tracing events.
98 ///
99 /// The filter is used to create a `tracing-subscriber`
100 /// [`EnvFilter`](https://docs.rs/tracing-subscriber/0.2.10/tracing_subscriber/filter/struct.EnvFilter.html#directives),
101 /// and more details on the syntax can be found there or in the examples
102 /// below.
103 ///
104 /// If no filter is specified (`None`), the filter is set to `info` if the
105 /// `-v` flag is given and `warn` if it is not given.
106 ///
107 /// # Examples
108 ///
109 /// `warn,zebrad=info,zebra_network=debug` sets a global `warn` level, an
110 /// `info` level for the `zebrad` crate, and a `debug` level for the
111 /// `zebra_network` crate.
112 ///
113 /// ```ascii,no_run
114 /// [block_verify{height=Some\(block::Height\(.*000\)\)}]=trace
115 /// ```
116 /// sets `trace` level for all events occurring in the context of a
117 /// `block_verify` span whose `height` field ends in `000`, i.e., traces the
118 /// verification of every 1000th block.
119 pub filter: Option<String>,
120
121 /// The buffer_limit size sets the number of log lines that can be queued by the tracing subscriber
122 /// to be written to stdout before logs are dropped.
123 ///
124 /// Defaults to 128,000 with a minimum of 100.
125 pub buffer_limit: usize,
126
127 /// The address used for an ad-hoc RPC endpoint allowing dynamic control of the tracing filter.
128 ///
129 /// Install Zebra using `cargo install --features=filter-reload` to enable this config.
130 ///
131 /// If this is set to None, the endpoint is disabled.
132 pub endpoint_addr: Option<SocketAddr>,
133
134 /// Controls whether to write a flamegraph of tracing spans.
135 ///
136 /// Install Zebra using `cargo install --features=flamegraph` to enable this config.
137 ///
138 /// If this is set to None, flamegraphs are disabled. Otherwise, it specifies
139 /// an output file path, as described below.
140 ///
141 /// This path is not used verbatim when writing out the flamegraph. This is
142 /// because the flamegraph is written out as two parts. First the flamegraph
143 /// is constantly persisted to the disk in a "folded" representation that
144 /// records collapsed stack traces of the tracing spans that are active.
145 /// Then, when the application is finished running the destructor will flush
146 /// the flamegraph output to the folded file and then read that file and
147 /// generate the final flamegraph from it as an SVG.
148 ///
149 /// The need to create two files means that we will slightly manipulate the
150 /// path given to us to create the two representations.
151 ///
152 /// # Security
153 ///
154 /// If you are running Zebra with elevated permissions ("root"), create the
155 /// directory for this file before running Zebra, and make sure the Zebra user
156 /// account has exclusive access to that directory, and other users can't modify
157 /// its parent directories.
158 ///
159 /// # Example
160 ///
161 /// Given `flamegraph = "flamegraph"` we will generate a `flamegraph.svg` and
162 /// a `flamegraph.folded` file in the current directory.
163 ///
164 /// If you provide a path with an extension the extension will be ignored and
165 /// replaced with `.folded` and `.svg` for the respective files.
166 pub flamegraph: Option<PathBuf>,
167
168 /// Shows progress bars for block syncing, and mempool transactions, and peer networking.
169 /// Also sends logs to the default log file path.
170 ///
171 /// This config field is ignored unless the `progress-bar` feature is enabled.
172 pub progress_bar: Option<ProgressConfig>,
173
174 /// If set to a path, write the tracing logs to that path.
175 ///
176 /// By default, logs are sent to the terminal standard output.
177 /// But if the `progress_bar` config is activated, logs are sent to the standard log file path:
178 /// - Linux: `$XDG_STATE_HOME/zebrad.log` or `$HOME/.local/state/zebrad.log`
179 /// - macOS: `$HOME/Library/Application Support/zebrad.log`
180 /// - Windows: `%LOCALAPPDATA%\zebrad.log` or `C:\Users\%USERNAME%\AppData\Local\zebrad.log`
181 ///
182 /// # Security
183 ///
184 /// If you are running Zebra with elevated permissions ("root"), create the
185 /// directory for this file before running Zebra, and make sure the Zebra user
186 /// account has exclusive access to that directory, and other users can't modify
187 /// its parent directories.
188 pub log_file: Option<PathBuf>,
189
190 /// The use_journald flag sends tracing events to systemd-journald, on Linux
191 /// distributions that use systemd.
192 ///
193 /// Install Zebra using `cargo install --features=journald` to enable this config.
194 pub use_journald: bool,
195
196 /// OpenTelemetry OTLP endpoint URL for distributed tracing.
197 ///
198 /// Install Zebra using `cargo install --features=opentelemetry` to enable this config.
199 ///
200 /// When `None` (default), OpenTelemetry is completely disabled with zero runtime overhead.
201 /// When set, traces are exported via OTLP HTTP protocol.
202 ///
203 /// Example: `"http://localhost:4318"`
204 ///
205 /// Can also be set via `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable (lower precedence).
206 pub opentelemetry_endpoint: Option<String>,
207
208 /// Service name reported to OpenTelemetry collector.
209 ///
210 /// Defaults to `"zebra"` if not specified.
211 ///
212 /// Can also be set via `OTEL_SERVICE_NAME` environment variable.
213 pub opentelemetry_service_name: Option<String>,
214
215 /// Trace sampling percentage between 0 and 100.
216 ///
217 /// Controls what percentage of traces are exported:
218 /// - `100` = 100% (all traces, default)
219 /// - `10` = 10% (recommended for high-traffic production)
220 /// - `0` = 0% (effectively disabled)
221 ///
222 /// Lower values reduce network/collector overhead for busy nodes.
223 ///
224 /// Note: This differs from the standard `OTEL_TRACES_SAMPLER_ARG` which uses
225 /// a ratio (0.0-1.0). Zebra uses percentage (0-100) for consistency with
226 /// other integer-based configuration options.
227 pub opentelemetry_sample_percent: Option<u8>,
228}
229
230/// The progress bars that Zebra will show while running.
231#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
232#[serde(rename_all = "lowercase")]
233pub enum ProgressConfig {
234 /// Show a lot of progress bars.
235 Detailed,
236
237 /// Show a few important progress bars.
238 //
239 // TODO: actually hide some progress bars in this mode.
240 #[default]
241 #[serde(other)]
242 Summary,
243}
244
245impl Config {
246 /// Returns `true` if standard output should use color escapes.
247 /// Automatically checks if Zebra is running in a terminal.
248 pub fn use_color_stdout(&self) -> bool {
249 self.force_use_color || (self.use_color && std::io::stdout().is_terminal())
250 }
251
252 /// Returns `true` if standard error should use color escapes.
253 /// Automatically checks if Zebra is running in a terminal.
254 pub fn use_color_stderr(&self) -> bool {
255 self.force_use_color || (self.use_color && std::io::stderr().is_terminal())
256 }
257
258 /// Returns `true` if output that could go to standard output or standard error
259 /// should use color escapes. Automatically checks if Zebra is running in a terminal.
260 pub fn use_color_stdout_and_stderr(&self) -> bool {
261 self.force_use_color
262 || (self.use_color
263 && std::io::stdout().is_terminal()
264 && std::io::stderr().is_terminal())
265 }
266}
267
268impl Default for InnerConfig {
269 fn default() -> Self {
270 // TODO: enable progress bars by default once they have been tested
271 let progress_bar = None;
272
273 Self {
274 use_color: true,
275 force_use_color: false,
276 filter: None,
277 buffer_limit: 128_000,
278 endpoint_addr: None,
279 flamegraph: None,
280 progress_bar,
281 log_file: runtime_default_log_file(None, progress_bar),
282 use_journald: false,
283 opentelemetry_endpoint: None,
284 opentelemetry_service_name: None,
285 opentelemetry_sample_percent: None,
286 }
287 }
288}
289
290/// Returns the runtime default log file path based on the `log_file` and `progress_bar` configs.
291fn runtime_default_log_file(
292 log_file: Option<PathBuf>,
293 progress_bar: Option<ProgressConfig>,
294) -> Option<PathBuf> {
295 if let Some(log_file) = log_file {
296 return Some(log_file);
297 }
298
299 // If the progress bar is active, we want to use a log file regardless of the config.
300 // (Logging to a terminal erases parts of the progress bars, making both unreadable.)
301 if progress_bar.is_some() {
302 return default_log_file();
303 }
304
305 None
306}
307
308/// Returns the configured log file path using the runtime `log_file` and `progress_bar` config.
309///
310/// This is the inverse of [`runtime_default_log_file()`].
311fn disk_default_log_file(
312 log_file: Option<PathBuf>,
313 progress_bar: Option<ProgressConfig>,
314) -> Option<PathBuf> {
315 // If the progress bar is active, and we've likely substituted the default log file path,
316 // don't write that substitute to the config on disk.
317 if progress_bar.is_some() && log_file == default_log_file() {
318 return None;
319 }
320
321 log_file
322}
323
324/// Returns the default log file path.
325fn default_log_file() -> Option<PathBuf> {
326 dirs::state_dir()
327 .or_else(dirs::data_local_dir)
328 .map(|dir| dir.join("zebrad.log"))
329}