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