change(ui): Enable the progress bar feature by default, but only show progress bars when the config is enabled (#7615)

* Add a progress bar config that is disabled unless the feature is on

* Simplify the default config

* Enable the progress bar feature by default, but require the config

* Rename progress bars config to avoid merge conflicts

* Use a log file when the progress bar is activated

* Document how to configure progress bars

* Handle log files in config_tests and check config path

* Fix doc link

* Fix path check

* Fix config log matching

* Fix clippy warning

* Add tracing to config tests

* It's zebrad not zebra

* cargo fmt --all

* Update release for config file changes

* Fix config test failures

* Allow printing to stdout in a method
This commit is contained in:
teor 2023-10-12 10:25:37 +10:00 committed by GitHub
parent 0cffae5dd0
commit ae52e3d23d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 319 additions and 57 deletions

View File

@ -5779,6 +5779,7 @@ dependencies = [
"humantime",
"indexmap 2.0.1",
"insta",
"itertools 0.11.0",
"lazy_static",
"once_cell",
"owo-colors",

View File

@ -11,7 +11,7 @@
- [Getting Started](#getting-started)
- [Docker](#docker)
- [Building Zebra](#building-zebra)
- [Optional Features](#optional-features)
- [Optional Configs & Features](#optional-features)
- [Known Issues](#known-issues)
- [Future Work](#future-work)
- [Documentation](#documentation)
@ -116,13 +116,24 @@ zebrad start
See the [Installing Zebra](https://zebra.zfnd.org/user/install.html) and [Running Zebra](https://zebra.zfnd.org/user/run.html)
sections in the book for more details.
#### Optional Features
#### Optional Configs & Features
##### Configuring Progress Bars
Configure `tracing.progress_bar` in your `zebrad.toml` to
[show key metrics in the terminal using progress bars](https://zfnd.org/experimental-zebra-progress-bars/).
When progress bars are active, Zebra automatically sends logs to a file.
In future releases, the `progress_bar = "summary"` config will show a few key metrics,
and the "detailed" config will show all available metrics. Please let us know which metrics are
important to you!
##### Custom Build Features
You can also build Zebra with additional [Cargo features](https://doc.rust-lang.org/cargo/reference/features.html#command-line-feature-options):
- `getblocktemplate-rpcs` for [mining support](https://zebra.zfnd.org/user/mining.html)
- `prometheus` for [Prometheus metrics](https://zebra.zfnd.org/user/metrics.html)
- `progress-bar` [experimental progress bars](https://zfnd.org/experimental-zebra-progress-bars/)
- `sentry` for [Sentry monitoring](https://zebra.zfnd.org/user/tracing.html#sentry-production-monitoring)
- `elasticsearch` for [experimental Elasticsearch support](https://zebra.zfnd.org/user/elasticsearch.html)

View File

@ -19,6 +19,7 @@ hex = "0.4.3"
indexmap = "2.0.1"
lazy_static = "1.4.0"
insta = "1.33.0"
itertools = "0.11.0"
proptest = "1.3.1"
once_cell = "1.18.0"
rand = "0.8.5"

View File

@ -21,10 +21,11 @@ use tracing::instrument;
#[macro_use]
mod arguments;
pub mod to_regex;
pub use self::arguments::Arguments;
use self::to_regex::{CollectRegexSet, ToRegex, ToRegexSet};
use self::to_regex::{CollectRegexSet, ToRegexSet};
/// A super-trait for [`Iterator`] + [`Debug`].
pub trait IteratorDebug: Iterator + Debug {}
@ -791,7 +792,7 @@ impl<T> TestChild<T> {
#[allow(clippy::unwrap_in_result)]
pub fn expect_stdout_line_matches<R>(&mut self, success_regex: R) -> Result<String>
where
R: ToRegex + Debug,
R: ToRegexSet + Debug,
{
self.apply_failure_regexes_to_outputs();
@ -823,7 +824,7 @@ impl<T> TestChild<T> {
#[allow(clippy::unwrap_in_result)]
pub fn expect_stderr_line_matches<R>(&mut self, success_regex: R) -> Result<String>
where
R: ToRegex + Debug,
R: ToRegexSet + Debug,
{
self.apply_failure_regexes_to_outputs();
@ -855,7 +856,7 @@ impl<T> TestChild<T> {
#[allow(clippy::unwrap_in_result)]
pub fn expect_stdout_line_matches_silent<R>(&mut self, success_regex: R) -> Result<String>
where
R: ToRegex + Debug,
R: ToRegexSet + Debug,
{
self.apply_failure_regexes_to_outputs();
@ -887,7 +888,7 @@ impl<T> TestChild<T> {
#[allow(clippy::unwrap_in_result)]
pub fn expect_stderr_line_matches_silent<R>(&mut self, success_regex: R) -> Result<String>
where
R: ToRegex + Debug,
R: ToRegexSet + Debug,
{
self.apply_failure_regexes_to_outputs();
@ -1246,9 +1247,9 @@ impl<T> TestOutput<T> {
#[allow(clippy::unwrap_in_result)]
pub fn stdout_matches<R>(&self, regex: R) -> Result<&Self>
where
R: ToRegex + Debug,
R: ToRegexSet + Debug,
{
let re = regex.to_regex().expect("regex must be valid");
let re = regex.to_regex_set().expect("regex must be valid");
self.output_check(
|stdout| re.is_match(stdout),
@ -1270,9 +1271,9 @@ impl<T> TestOutput<T> {
#[allow(clippy::unwrap_in_result)]
pub fn stdout_line_matches<R>(&self, regex: R) -> Result<&Self>
where
R: ToRegex + Debug,
R: ToRegexSet + Debug,
{
let re = regex.to_regex().expect("regex must be valid");
let re = regex.to_regex_set().expect("regex must be valid");
self.any_output_line(
|line| re.is_match(line),
@ -1300,9 +1301,9 @@ impl<T> TestOutput<T> {
#[allow(clippy::unwrap_in_result)]
pub fn stderr_matches<R>(&self, regex: R) -> Result<&Self>
where
R: ToRegex + Debug,
R: ToRegexSet + Debug,
{
let re = regex.to_regex().expect("regex must be valid");
let re = regex.to_regex_set().expect("regex must be valid");
self.output_check(
|stderr| re.is_match(stderr),
@ -1324,9 +1325,9 @@ impl<T> TestOutput<T> {
#[allow(clippy::unwrap_in_result)]
pub fn stderr_line_matches<R>(&self, regex: R) -> Result<&Self>
where
R: ToRegex + Debug,
R: ToRegexSet + Debug,
{
let re = regex.to_regex().expect("regex must be valid");
let re = regex.to_regex_set().expect("regex must be valid");
self.any_output_line(
|line| re.is_match(line),

View File

@ -2,6 +2,7 @@
use std::iter;
use itertools::Itertools;
use regex::{Error, Regex, RegexBuilder, RegexSet, RegexSetBuilder};
/// A trait for converting a value to a [`Regex`].
@ -135,15 +136,17 @@ pub trait CollectRegexSet {
impl<I> CollectRegexSet for I
where
I: IntoIterator,
I::Item: ToRegex,
I::Item: ToRegexSet,
{
fn collect_regex_set(self) -> Result<RegexSet, Error> {
let regexes: Result<Vec<Regex>, Error> =
self.into_iter().map(|item| item.to_regex()).collect();
let regexes: Result<Vec<RegexSet>, Error> = self
.into_iter()
.map(|item| item.to_regex_set())
.try_collect();
let regexes = regexes?;
// This conversion discards flags and limits from Regex and RegexBuilder.
let regexes = regexes.iter().map(|regex| regex.as_str());
let regexes = regexes.iter().flat_map(|regex_set| regex_set.patterns());
RegexSet::new(regexes)
}

View File

@ -52,7 +52,7 @@ features = [
[features]
# In release builds, don't compile debug logging code, to improve performance.
default = ["release_max_level_info"]
default = ["release_max_level_info", "progress-bar"]
# Default features for official ZF binary release builds
default-release-binaries = ["default", "sentry"]

View File

@ -434,6 +434,7 @@ impl Application for ZebradApp {
// Override the default tracing filter based on the command-line verbosity.
tracing_config.filter = tracing_config
.filter
.clone()
.or_else(|| Some(default_filter.to_owned()));
} else {
// Don't apply the configured filter for short-lived commands.

View File

@ -1,6 +1,10 @@
//! Tracing and logging infrastructure for Zebra.
use std::{net::SocketAddr, path::PathBuf};
use std::{
net::SocketAddr,
ops::{Deref, DerefMut},
path::PathBuf,
};
use serde::{Deserialize, Serialize};
@ -16,10 +20,59 @@ pub use endpoint::TracingEndpoint;
#[cfg(feature = "flamegraph")]
pub use flame::{layer, Grapher};
/// Tracing configuration section.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields, default)]
/// Tracing configuration section: outer config after cross-field defaults are applied.
///
/// This is a wrapper type that dereferences to the inner config type.
///
//
// TODO: replace with serde's finalizer attribute when that feature is implemented.
// we currently use the recommended workaround of a wrapper struct with from/into attributes.
// https://github.com/serde-rs/serde/issues/642#issuecomment-525432907
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(
deny_unknown_fields,
default,
from = "InnerConfig",
into = "InnerConfig"
)]
pub struct Config {
inner: InnerConfig,
}
impl Deref for Config {
type Target = InnerConfig;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Config {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl From<InnerConfig> for Config {
fn from(mut inner: InnerConfig) -> Self {
inner.log_file = runtime_default_log_file(inner.log_file, inner.progress_bar);
Self { inner }
}
}
impl From<Config> for InnerConfig {
fn from(mut config: Config) -> Self {
config.log_file = disk_default_log_file(config.log_file.clone(), config.progress_bar);
config.inner
}
}
/// Tracing configuration section: inner config used to deserialize and apply cross-field defaults.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields, default)]
pub struct InnerConfig {
/// Whether to use colored terminal output, if available.
///
/// Colored terminal output is automatically disabled if an output stream
@ -108,10 +161,16 @@ pub struct Config {
/// replaced with `.folded` and `.svg` for the respective files.
pub flamegraph: Option<PathBuf>,
/// Shows progress bars for block syncing, and mempool transactions, and peer networking.
/// Also sends logs to the default log file path.
///
/// This config field is ignored unless the `progress-bar` feature is enabled.
pub progress_bar: Option<ProgressConfig>,
/// If set to a path, write the tracing logs to that path.
///
/// By default, logs are sent to the terminal standard output.
/// But if the `progress-bar` feature is activated, logs are sent to the standard log file path:
/// But if the `progress_bar` config is activated, logs are sent to the standard log file path:
/// - Linux: `$XDG_STATE_HOME/zebrad.log` or `$HOME/.local/state/zebrad.log`
/// - macOS: `$HOME/Library/Application Support/zebrad.log`
/// - Windows: `%LOCALAPPDATA%\zebrad.log` or `C:\Users\%USERNAME%\AppData\Local\zebrad.log`
@ -131,6 +190,21 @@ pub struct Config {
pub use_journald: bool,
}
/// The progress bars that Zebra will show while running.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ProgressConfig {
/// Show a lot of progress bars.
Detailed,
/// Show a few important progress bars.
//
// TODO: actually hide some progress bars in this mode.
#[default]
#[serde(other)]
Summary,
}
impl Config {
/// Returns `true` if standard output should use color escapes.
/// Automatically checks if Zebra is running in a terminal.
@ -152,12 +226,10 @@ impl Config {
}
}
impl Default for Config {
impl Default for InnerConfig {
fn default() -> Self {
#[cfg(feature = "progress-bar")]
let default_log_file = dirs::state_dir()
.or_else(dirs::data_local_dir)
.map(|dir| dir.join("zebrad.log"));
// TODO: enable progress bars by default once they have been tested
let progress_bar = None;
Self {
use_color: true,
@ -166,11 +238,50 @@ impl Default for Config {
buffer_limit: 128_000,
endpoint_addr: None,
flamegraph: None,
#[cfg(not(feature = "progress-bar"))]
log_file: None,
#[cfg(feature = "progress-bar")]
log_file: default_log_file,
progress_bar,
log_file: runtime_default_log_file(None, progress_bar),
use_journald: false,
}
}
}
/// Returns the runtime default log file path based on the `log_file` and `progress_bar` configs.
fn runtime_default_log_file(
log_file: Option<PathBuf>,
progress_bar: Option<ProgressConfig>,
) -> Option<PathBuf> {
if let Some(log_file) = log_file {
return Some(log_file);
}
// If the progress bar is active, we want to use a log file regardless of the config.
// (Logging to a terminal erases parts of the progress bars, making both unreadable.)
if progress_bar.is_some() {
return default_log_file();
}
None
}
/// Returns the configured log file path using the runtime `log_file` and `progress_bar` config.
///
/// This is the inverse of [`runtime_default_log_file()`].
fn disk_default_log_file(
log_file: Option<PathBuf>,
progress_bar: Option<ProgressConfig>,
) -> Option<PathBuf> {
// If the progress bar is active, and we've likely substituted the default log file path,
// don't write that substitute to the config on disk.
if progress_bar.is_some() && log_file == default_log_file() {
return None;
}
log_file
}
/// Returns the default log file path.
fn default_log_file() -> Option<PathBuf> {
dirs::state_dir()
.or_else(dirs::data_local_dir)
.map(|dir| dir.join("zebrad.log"))
}

View File

@ -79,14 +79,14 @@ impl Tracing {
/// and the Zebra logo on startup. (If the terminal supports it.)
//
// This method should only print to stderr, because stdout is for tracing logs.
#[allow(clippy::print_stderr, clippy::unwrap_in_result)]
#[allow(clippy::print_stdout, clippy::print_stderr, clippy::unwrap_in_result)]
pub fn new(network: Network, config: Config, uses_intro: bool) -> Result<Self, FrameworkError> {
// Only use color if tracing output is being sent to a terminal or if it was explicitly
// forced to.
let use_color = config.use_color_stdout();
let use_color_stderr = config.use_color_stderr();
let filter = config.filter.unwrap_or_default();
let filter = config.filter.clone().unwrap_or_default();
let flame_root = &config.flamegraph;
// Only show the intro for user-focused node server commands like `start`
@ -139,7 +139,8 @@ impl Tracing {
}
if uses_intro {
eprintln!("Sending logs to {log_file:?}...");
// We want this to appear on stdout instead of the usual log messages.
println!("Sending logs to {log_file:?}...");
}
let log_file = File::options().append(true).create(true).open(log_file)?;
Box::new(log_file) as BoxWrite
@ -305,7 +306,7 @@ impl Tracing {
//
// TODO: move this to its own module?
#[cfg(feature = "progress-bar")]
{
if let Some(progress_bar_config) = config.progress_bar.as_ref() {
use howudoin::consumers::TermLine;
use std::time::Duration;
@ -315,7 +316,11 @@ impl Tracing {
let terminal_consumer = TermLine::with_debounce(PROGRESS_BAR_DEBOUNCE);
howudoin::init(terminal_consumer);
info!("activated progress bar");
info!(?progress_bar_config, "activated progress bars");
} else {
info!(
"set 'tracing.progress_bar =\"summary\"' in zebrad.toml to activate progress bars"
);
}
Ok(Self {

View File

@ -61,9 +61,10 @@
//!
//! ### Metrics
//!
//! * `prometheus`: export metrics to prometheus.
//! * `progress-bar`: shows key metrics in the terminal using progress bars,
//! * configuring a `tracing.progress_bar`: shows key metrics in the terminal using progress bars,
//! and automatically configures Zebra to send logs to a file.
//! (The `progress-bar` feature is activated by default.)
//! * `prometheus`: export metrics to prometheus.
//!
//! Read the [metrics](https://zebra.zfnd.org/user/metrics.html) section of the book
//! for more details.

View File

@ -161,7 +161,12 @@ use zebra_network::constants::PORT_IN_USE_ERROR;
use zebra_node_services::rpc_client::RpcRequestClient;
use zebra_state::{constants::LOCK_FILE_ERROR, database_format_version_in_code};
use zebra_test::{args, command::ContextFrom, net::random_known_port, prelude::*};
use zebra_test::{
args,
command::{to_regex::CollectRegexSet, ContextFrom},
net::random_known_port,
prelude::*,
};
mod common;
@ -605,7 +610,7 @@ fn config_tests() -> Result<()> {
// Check that Zebra's previous configurations still work
stored_configs_work()?;
// Runs `zebrad` serially to avoid potential port conflicts
// We run the `zebrad` app test after the config tests, to avoid potential port conflicts
app_no_args()?;
Ok(())
@ -619,6 +624,8 @@ fn app_no_args() -> Result<()> {
// start caches state, so run one of the start tests with persistent state
let testdir = testdir()?.with_config(&mut persistent_test_config()?)?;
tracing::info!(?testdir, "running zebrad with no config (default settings)");
let mut child = testdir.spawn_child(args![])?;
// Run the program and kill it after a few seconds
@ -651,6 +658,8 @@ fn valid_generated_config(command: &str, expect_stdout_line_contains: &str) -> R
// Add a config file name to tempdir path
let generated_config_path = testdir.path().join("zebrad.toml");
tracing::info!(?generated_config_path, "generating valid config");
// Generate configuration in temp dir path
let child =
testdir.spawn_child(args!["generate", "-o": generated_config_path.to_str().unwrap()])?;
@ -664,6 +673,8 @@ fn valid_generated_config(command: &str, expect_stdout_line_contains: &str) -> R
"generated config file not found"
);
tracing::info!(?generated_config_path, "testing valid config parsing");
// Run command using temp dir and kill it after a few seconds
let mut child = testdir.spawn_child(args![command])?;
std::thread::sleep(LAUNCH_DELAY);
@ -702,6 +713,8 @@ fn last_config_is_stored() -> Result<()> {
// Add a config file name to tempdir path
let generated_config_path = testdir.path().join("zebrad.toml");
tracing::info!(?generated_config_path, "generated current config");
// Generate configuration in temp dir path
let child =
testdir.spawn_child(args!["generate", "-o": generated_config_path.to_str().unwrap()])?;
@ -715,6 +728,11 @@ fn last_config_is_stored() -> Result<()> {
"generated config file not found"
);
tracing::info!(
?generated_config_path,
"testing current config is in stored configs"
);
// Get the contents of the generated config file
let generated_content =
fs::read_to_string(generated_config_path).expect("Should have been able to read the file");
@ -815,6 +833,11 @@ fn invalid_generated_config() -> Result<()> {
// Add a config file name to tempdir path.
let config_path = testdir.path().join("zebrad.toml");
tracing::info!(
?config_path,
"testing invalid config parsing: generating valid config"
);
// Generate a valid config file in the temp dir.
let child = testdir.spawn_child(args!["generate", "-o": config_path.to_str().unwrap()])?;
@ -849,10 +872,14 @@ fn invalid_generated_config() -> Result<()> {
secs = 3600
";
tracing::info!(?config_path, "writing invalid config");
// Write the altered config file so that Zebra can pick it up.
fs::write(config_path.to_str().unwrap(), config_file.as_bytes())
.expect("Could not write the altered config file.");
tracing::info!(?config_path, "testing invalid config parsing");
// Run Zebra in a temp dir so that it loads the config.
let mut child = testdir.spawn_child(args!["start"])?;
@ -883,6 +910,8 @@ fn invalid_generated_config() -> Result<()> {
fn stored_configs_work() -> Result<()> {
let old_configs_dir = configs_dir();
tracing::info!(?old_configs_dir, "testing older config parsing");
for config_file in old_configs_dir
.read_dir()
.expect("read_dir call failed")
@ -892,10 +921,10 @@ fn stored_configs_work() -> Result<()> {
let config_file_name = config_file_path
.file_name()
.expect("config files must have a file name")
.to_string_lossy();
.to_str()
.expect("config file names are valid unicode");
if config_file_name.as_ref().starts_with('.') || config_file_name.as_ref().starts_with('#')
{
if config_file_name.starts_with('.') || config_file_name.starts_with('#') {
// Skip editor files and other invalid config paths
tracing::info!(
?config_file_path,
@ -907,10 +936,7 @@ fn stored_configs_work() -> Result<()> {
// ignore files starting with getblocktemplate prefix
// if we were not built with the getblocktemplate-rpcs feature.
#[cfg(not(feature = "getblocktemplate-rpcs"))]
if config_file_name
.as_ref()
.starts_with(GET_BLOCK_TEMPLATE_CONFIG_PREFIX)
{
if config_file_name.starts_with(GET_BLOCK_TEMPLATE_CONFIG_PREFIX) {
tracing::info!(
?config_file_path,
"skipping getblocktemplate-rpcs config file path"
@ -921,12 +947,41 @@ fn stored_configs_work() -> Result<()> {
let run_dir = testdir()?;
let stored_config_path = config_file_full_path(config_file.path());
tracing::info!(
?stored_config_path,
"testing old config can be parsed by current zebrad"
);
// run zebra with stored config
let mut child =
run_dir.spawn_child(args!["-c", stored_config_path.to_str().unwrap(), "start"])?;
// zebra was able to start with the stored config
child.expect_stdout_line_matches("Starting zebrad".to_string())?;
let success_regexes = [
// When logs are sent to the terminal, we see the config loading message and path.
format!(
"loaded zebrad config.*config_path.*=.*{}",
regex::escape(config_file_name)
),
// If they are sent to a file, we see a log file message on stdout,
// and a logo, welcome message, and progress bar on stderr.
"Sending logs to".to_string(),
// TODO: add expect_stdout_or_stderr_line_matches() and check for this instead:
//"Thank you for running a mainnet zebrad".to_string(),
];
tracing::info!(
?stored_config_path,
?success_regexes,
"waiting for zebrad to parse config and start logging"
);
let success_regexes = success_regexes
.iter()
.collect_regex_set()
.expect("regexes are valid");
// Zebra was able to start with the stored config.
child.expect_stdout_line_matches(success_regexes)?;
// finish
child.kill(false)?;

View File

@ -58,10 +58,9 @@ pub fn default_test_config() -> Result<ZebradConfig> {
env::var("ZEBRA_FORCE_USE_COLOR"),
Err(env::VarError::NotPresent)
);
let tracing = tracing::Config {
force_use_color,
..tracing::Config::default()
};
let mut tracing = tracing::Config::default();
tracing.force_use_color = force_use_color;
let mut state = zebra_state::Config::ephemeral();
state.debug_validity_check_interval = Some(DATABASE_FORMAT_CHECK_INTERVAL);

View File

@ -0,0 +1,73 @@
# Default configuration for zebrad.
#
# This file can be used as a skeleton for custom configs.
#
# Unspecified fields use default values. Optional fields are Some(field) if the
# field is present and None if it is absent.
#
# This file is generated as an example using zebrad's current defaults.
# You should set only the config options you want to keep, and delete the rest.
# Only a subset of fields are present in the skeleton, since optional values
# whose default is None are omitted.
#
# The config format (including a complete list of sections and fields) is
# documented here:
# https://doc.zebra.zfnd.org/zebrad/config/struct.ZebradConfig.html
#
# zebrad attempts to load configs in the following order:
#
# 1. The -c flag on the command line, e.g., `zebrad -c myconfig.toml start`;
# 2. The file `zebrad.toml` in the users's preference directory (platform-dependent);
# 3. The default config.
[consensus]
checkpoint_sync = true
debug_skip_parameter_preload = false
[mempool]
eviction_memory_time = "1h"
tx_cost_limit = 80000000
[metrics]
[network]
cache_dir = true
crawl_new_peer_interval = "1m 1s"
initial_mainnet_peers = [
"dnsseed.z.cash:8233",
"dnsseed.str4d.xyz:8233",
"mainnet.seeder.zfnd.org:8233",
"mainnet.is.yolo.money:8233",
]
initial_testnet_peers = [
"dnsseed.testnet.z.cash:18233",
"testnet.seeder.zfnd.org:18233",
"testnet.is.yolo.money:18233",
]
listen_addr = "0.0.0.0:8233"
max_connections_per_ip = 1
network = "Mainnet"
peerset_initial_target_size = 25
[rpc]
debug_force_finished_sync = false
parallel_cpu_threads = 1
[state]
cache_dir = "cache_dir"
delete_old_database = true
ephemeral = false
[sync]
checkpoint_verify_concurrency_limit = 1000
download_concurrency_limit = 50
full_verify_concurrency_limit = 20
parallel_cpu_threads = 0
[tracing]
buffer_limit = 128000
force_use_color = false
progress_bar = "summary"
use_color = true
use_journald = false