informalsystems_malachitebft_config/
lib.rs

1use core::fmt;
2use std::net::{IpAddr, SocketAddr};
3use std::str::FromStr;
4use std::time::Duration;
5
6use bytesize::ByteSize;
7use malachitebft_core_types::TimeoutKind;
8use multiaddr::Multiaddr;
9use serde::{Deserialize, Serialize};
10
11/// P2P configuration options
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13pub struct P2pConfig {
14    /// Address to listen for incoming connections
15    pub listen_addr: Multiaddr,
16
17    /// List of nodes to keep persistent connections to
18    pub persistent_peers: Vec<Multiaddr>,
19
20    /// Peer discovery
21    #[serde(default)]
22    pub discovery: DiscoveryConfig,
23
24    /// The type of pub-sub protocol to use for consensus
25    pub protocol: PubSubProtocol,
26
27    /// The maximum size of messages to send over pub-sub
28    pub pubsub_max_size: ByteSize,
29
30    /// The maximum size of messages to send over RPC
31    pub rpc_max_size: ByteSize,
32}
33
34impl Default for P2pConfig {
35    fn default() -> Self {
36        P2pConfig {
37            listen_addr: Multiaddr::empty(),
38            persistent_peers: vec![],
39            discovery: Default::default(),
40            protocol: Default::default(),
41            rpc_max_size: ByteSize::mib(10),
42            pubsub_max_size: ByteSize::mib(4),
43        }
44    }
45}
46
47/// Peer Discovery configuration options
48#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct DiscoveryConfig {
50    /// Enable peer discovery
51    #[serde(default)]
52    pub enabled: bool,
53
54    /// Bootstrap protocol
55    #[serde(default)]
56    pub bootstrap_protocol: BootstrapProtocol,
57
58    /// Selector
59    #[serde(default)]
60    pub selector: Selector,
61
62    /// Number of outbound peers
63    #[serde(default)]
64    pub num_outbound_peers: usize,
65
66    /// Number of inbound peers
67    #[serde(default)]
68    pub num_inbound_peers: usize,
69
70    /// Maximum number of connections per peer
71    #[serde(default)]
72    pub max_connections_per_peer: usize,
73
74    /// Ephemeral connection timeout
75    #[serde(default)]
76    #[serde(with = "humantime_serde")]
77    pub ephemeral_connection_timeout: Duration,
78}
79
80impl Default for DiscoveryConfig {
81    fn default() -> Self {
82        DiscoveryConfig {
83            enabled: false,
84            bootstrap_protocol: Default::default(),
85            selector: Default::default(),
86            num_outbound_peers: 0,
87            num_inbound_peers: 20,
88            max_connections_per_peer: 5,
89            ephemeral_connection_timeout: Default::default(),
90        }
91    }
92}
93
94#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
95#[serde(rename_all = "lowercase")]
96pub enum BootstrapProtocol {
97    #[default]
98    Kademlia,
99    Full,
100}
101
102impl BootstrapProtocol {
103    pub fn name(&self) -> &'static str {
104        match self {
105            Self::Kademlia => "kademlia",
106            Self::Full => "full",
107        }
108    }
109}
110
111impl FromStr for BootstrapProtocol {
112    type Err = String;
113
114    fn from_str(s: &str) -> Result<Self, Self::Err> {
115        match s {
116            "kademlia" => Ok(Self::Kademlia),
117            "full" => Ok(Self::Full),
118            e => Err(format!(
119                "unknown bootstrap protocol: {e}, available: kademlia, full"
120            )),
121        }
122    }
123}
124
125#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
126#[serde(rename_all = "lowercase")]
127pub enum Selector {
128    #[default]
129    Kademlia,
130    Random,
131}
132
133impl Selector {
134    pub fn name(&self) -> &'static str {
135        match self {
136            Self::Kademlia => "kademlia",
137            Self::Random => "random",
138        }
139    }
140}
141
142impl FromStr for Selector {
143    type Err = String;
144
145    fn from_str(s: &str) -> Result<Self, Self::Err> {
146        match s {
147            "kademlia" => Ok(Self::Kademlia),
148            "random" => Ok(Self::Random),
149            e => Err(format!(
150                "unknown selector: {e}, available: kademlia, random"
151            )),
152        }
153    }
154}
155
156#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
157pub enum TransportProtocol {
158    #[default]
159    Tcp,
160    Quic,
161}
162
163impl TransportProtocol {
164    pub fn multiaddr(&self, host: &str, port: usize) -> Multiaddr {
165        match self {
166            Self::Tcp => format!("/ip4/{host}/tcp/{port}").parse().unwrap(),
167            Self::Quic => format!("/ip4/{host}/udp/{port}/quic-v1").parse().unwrap(),
168        }
169    }
170}
171
172impl FromStr for TransportProtocol {
173    type Err = String;
174
175    fn from_str(s: &str) -> Result<Self, Self::Err> {
176        match s {
177            "tcp" => Ok(Self::Tcp),
178            "quic" => Ok(Self::Quic),
179            e => Err(format!(
180                "unknown transport protocol: {e}, available: tcp, quic"
181            )),
182        }
183    }
184}
185
186/// The type of pub-sub protocol.
187/// If multiple protocols are configured in the configuration file, the first one from this list
188/// will be used.
189#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
190#[serde(tag = "type", rename_all = "lowercase")]
191pub enum PubSubProtocol {
192    GossipSub(GossipSubConfig),
193    Broadcast,
194}
195
196impl Default for PubSubProtocol {
197    fn default() -> Self {
198        Self::GossipSub(GossipSubConfig::default())
199    }
200}
201
202/// GossipSub configuration
203#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
204#[serde(from = "gossipsub::RawConfig", default)]
205pub struct GossipSubConfig {
206    /// Target number of peers for the mesh network (D in the GossipSub spec)
207    mesh_n: usize,
208
209    /// Maximum number of peers in mesh network before removing some (D_high in the GossipSub spec)
210    mesh_n_high: usize,
211
212    /// Minimum number of peers in mesh network before adding more (D_low in the spec)
213    mesh_n_low: usize,
214
215    /// Minimum number of outbound peers in the mesh network before adding more (D_out in the spec).
216    /// This value must be smaller or equal than `mesh_n / 2` and smaller than `mesh_n_low`.
217    /// When this value is set to 0 or does not meet the above constraints,
218    /// it will be calculated as `max(1, min(mesh_n / 2, mesh_n_low - 1))`
219    mesh_outbound_min: usize,
220}
221
222impl Default for GossipSubConfig {
223    fn default() -> Self {
224        Self::new(6, 12, 4, 2)
225    }
226}
227
228impl GossipSubConfig {
229    /// Create a new, valid GossipSub configuration.
230    pub fn new(
231        mesh_n: usize,
232        mesh_n_high: usize,
233        mesh_n_low: usize,
234        mesh_outbound_min: usize,
235    ) -> Self {
236        let mut result = Self {
237            mesh_n,
238            mesh_n_high,
239            mesh_n_low,
240            mesh_outbound_min,
241        };
242
243        result.adjust();
244        result
245    }
246
247    /// Adjust the configuration values.
248    pub fn adjust(&mut self) {
249        use std::cmp::{max, min};
250
251        if self.mesh_n == 0 {
252            self.mesh_n = 6;
253        }
254
255        if self.mesh_n_high == 0 || self.mesh_n_high < self.mesh_n {
256            self.mesh_n_high = self.mesh_n * 2;
257        }
258
259        if self.mesh_n_low == 0 || self.mesh_n_low > self.mesh_n {
260            self.mesh_n_low = self.mesh_n * 2 / 3;
261        }
262
263        if self.mesh_outbound_min == 0
264            || self.mesh_outbound_min > self.mesh_n / 2
265            || self.mesh_outbound_min >= self.mesh_n_low
266        {
267            self.mesh_outbound_min = max(1, min(self.mesh_n / 2, self.mesh_n_low - 1));
268        }
269    }
270
271    pub fn mesh_n(&self) -> usize {
272        self.mesh_n
273    }
274
275    pub fn mesh_n_high(&self) -> usize {
276        self.mesh_n_high
277    }
278
279    pub fn mesh_n_low(&self) -> usize {
280        self.mesh_n_low
281    }
282
283    pub fn mesh_outbound_min(&self) -> usize {
284        self.mesh_outbound_min
285    }
286}
287
288mod gossipsub {
289    #[derive(serde::Deserialize)]
290    pub struct RawConfig {
291        #[serde(default)]
292        mesh_n: usize,
293        #[serde(default)]
294        mesh_n_high: usize,
295        #[serde(default)]
296        mesh_n_low: usize,
297        #[serde(default)]
298        mesh_outbound_min: usize,
299    }
300
301    impl From<RawConfig> for super::GossipSubConfig {
302        fn from(raw: RawConfig) -> Self {
303            super::GossipSubConfig::new(
304                raw.mesh_n,
305                raw.mesh_n_high,
306                raw.mesh_n_low,
307                raw.mesh_outbound_min,
308            )
309        }
310    }
311}
312
313#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
314#[serde(tag = "load_type", rename_all = "snake_case")]
315pub enum MempoolLoadType {
316    NoLoad,
317    UniformLoad(mempool_load::UniformLoadConfig),
318    NonUniformLoad(mempool_load::NonUniformLoadConfig),
319}
320
321impl Default for MempoolLoadType {
322    fn default() -> Self {
323        Self::NoLoad
324    }
325}
326
327pub mod mempool_load {
328    use super::*;
329
330    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
331    pub struct NonUniformLoadConfig {
332        /// Base transaction count
333        pub base_count: i32,
334
335        /// Base transaction size
336        pub base_size: i32,
337
338        /// How much the transaction count can vary
339        pub count_variation: std::ops::Range<i32>,
340
341        /// How much the transaction size can vary
342        pub size_variation: std::ops::Range<i32>,
343
344        /// Chance of generating a spike.
345        /// e.g. 0.1 = 10% chance of spike
346        pub spike_probability: f64,
347
348        /// Multiplier for spike transactions
349        /// e.g. 10 = 10x more transactions during spike
350        pub spike_multiplier: usize,
351
352        /// Range of intervals between generating load, in milliseconds
353        pub sleep_interval: std::ops::Range<u64>,
354    }
355
356    impl Default for NonUniformLoadConfig {
357        fn default() -> Self {
358            Self {
359                base_count: 100,
360                base_size: 256,
361                count_variation: -100..200,
362                size_variation: -64..128,
363                spike_probability: 0.10,
364                spike_multiplier: 2,
365                sleep_interval: 1000..5000,
366            }
367        }
368    }
369
370    #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
371    pub struct UniformLoadConfig {
372        /// Interval at which to generate load
373        #[serde(with = "humantime_serde")]
374        pub interval: Duration,
375
376        /// Number of transactions to generate
377        pub count: usize,
378
379        /// Size of each generated transaction
380        pub size: ByteSize,
381    }
382
383    impl Default for UniformLoadConfig {
384        fn default() -> Self {
385            Self {
386                interval: Duration::from_secs(1),
387                count: 1000,
388                size: ByteSize::b(256),
389            }
390        }
391    }
392}
393
394/// Mempool configuration options
395#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
396pub struct MempoolLoadConfig {
397    /// Mempool loading type
398    #[serde(flatten)]
399    pub load_type: MempoolLoadType,
400}
401
402/// Mempool configuration options
403#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
404pub struct MempoolConfig {
405    /// P2P configuration options
406    pub p2p: P2pConfig,
407
408    /// Maximum number of transactions
409    pub max_tx_count: usize,
410
411    /// Maximum number of transactions to gossip at once in a batch
412    pub gossip_batch_size: usize,
413
414    /// Mempool load configuration options
415    pub load: MempoolLoadConfig,
416}
417
418/// ValueSync configuration options
419#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
420pub struct ValueSyncConfig {
421    /// Enable ValueSync
422    pub enabled: bool,
423
424    /// Interval at which to update other peers of our status
425    #[serde(with = "humantime_serde")]
426    pub status_update_interval: Duration,
427
428    /// Timeout duration for sync requests
429    #[serde(with = "humantime_serde")]
430    pub request_timeout: Duration,
431
432    /// Maximum size of a request
433    pub max_request_size: ByteSize,
434
435    /// Maximum size of a response
436    pub max_response_size: ByteSize,
437
438    /// Maximum number of parallel requests to send
439    pub parallel_requests: usize,
440
441    /// Scoring strategy for peers
442    #[serde(default)]
443    pub scoring_strategy: ScoringStrategy,
444
445    /// Threshold for considering a peer inactive
446    #[serde(with = "humantime_serde")]
447    pub inactive_threshold: Duration,
448}
449
450impl Default for ValueSyncConfig {
451    fn default() -> Self {
452        Self {
453            enabled: true,
454            status_update_interval: Duration::from_secs(10),
455            request_timeout: Duration::from_secs(10),
456            max_request_size: ByteSize::mib(1),
457            max_response_size: ByteSize::mib(512),
458            parallel_requests: 5,
459            scoring_strategy: ScoringStrategy::default(),
460            inactive_threshold: Duration::from_secs(60),
461        }
462    }
463}
464
465#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
466#[serde(rename_all = "lowercase")]
467pub enum ScoringStrategy {
468    #[default]
469    Ema,
470}
471
472impl ScoringStrategy {
473    pub fn name(&self) -> &'static str {
474        match self {
475            Self::Ema => "ema",
476        }
477    }
478}
479
480impl FromStr for ScoringStrategy {
481    type Err = String;
482
483    fn from_str(s: &str) -> Result<Self, Self::Err> {
484        match s {
485            "ema" => Ok(Self::Ema),
486            e => Err(format!("unknown scoring strategy: {e}, available: ema")),
487        }
488    }
489}
490
491/// Consensus configuration options
492#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
493pub struct ConsensusConfig {
494    /// Timeouts
495    #[serde(flatten)]
496    pub timeouts: TimeoutConfig,
497
498    /// P2P configuration options
499    pub p2p: P2pConfig,
500
501    /// Message types that can carry values
502    pub value_payload: ValuePayload,
503
504    /// Size of the consensus input queue
505    ///
506    /// # Deprecated
507    /// This setting is deprecated and will be removed in the future.
508    /// The queue capacity is now derived from the `sync.parallel_requests` setting.
509    #[serde(default)]
510    pub queue_capacity: usize,
511}
512
513/// Message types required by consensus to deliver the value being proposed
514#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
515#[serde(rename_all = "kebab-case")]
516pub enum ValuePayload {
517    #[default]
518    PartsOnly,
519    ProposalOnly, // TODO - add small block app to test this option
520    ProposalAndParts,
521}
522
523impl ValuePayload {
524    pub fn include_parts(&self) -> bool {
525        match self {
526            Self::ProposalOnly => false,
527            Self::PartsOnly | Self::ProposalAndParts => true,
528        }
529    }
530
531    pub fn include_proposal(&self) -> bool {
532        match self {
533            Self::PartsOnly => false,
534            Self::ProposalOnly | Self::ProposalAndParts => true,
535        }
536    }
537}
538
539/// Timeouts
540#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
541pub struct TimeoutConfig {
542    /// How long we wait for a proposal block before prevoting nil
543    #[serde(with = "humantime_serde")]
544    pub timeout_propose: Duration,
545
546    /// How much timeout_propose increases with each round
547    #[serde(with = "humantime_serde")]
548    pub timeout_propose_delta: Duration,
549
550    /// How long we wait after receiving +2/3 prevotes for “anything” (ie. not a single block or nil)
551    #[serde(with = "humantime_serde")]
552    pub timeout_prevote: Duration,
553
554    /// How much the timeout_prevote increases with each round
555    #[serde(with = "humantime_serde")]
556    pub timeout_prevote_delta: Duration,
557
558    /// How long we wait after receiving +2/3 precommits for “anything” (ie. not a single block or nil)
559    #[serde(with = "humantime_serde")]
560    pub timeout_precommit: Duration,
561
562    /// How much the timeout_precommit increases with each round
563    #[serde(with = "humantime_serde")]
564    pub timeout_precommit_delta: Duration,
565
566    /// How long we wait after entering a round before starting
567    /// the rebroadcast liveness protocol
568    #[serde(with = "humantime_serde")]
569    pub timeout_rebroadcast: Duration,
570}
571
572impl TimeoutConfig {
573    pub fn timeout_duration(&self, step: TimeoutKind) -> Duration {
574        match step {
575            TimeoutKind::Propose => self.timeout_propose,
576            TimeoutKind::Prevote => self.timeout_prevote,
577            TimeoutKind::Precommit => self.timeout_precommit,
578            TimeoutKind::Rebroadcast => {
579                self.timeout_propose + self.timeout_prevote + self.timeout_precommit
580            }
581        }
582    }
583
584    pub fn delta_duration(&self, step: TimeoutKind) -> Option<Duration> {
585        match step {
586            TimeoutKind::Propose => Some(self.timeout_propose_delta),
587            TimeoutKind::Prevote => Some(self.timeout_prevote_delta),
588            TimeoutKind::Precommit => Some(self.timeout_precommit_delta),
589            TimeoutKind::Rebroadcast => None,
590        }
591    }
592}
593
594impl Default for TimeoutConfig {
595    fn default() -> Self {
596        let timeout_propose = Duration::from_secs(3);
597        let timeout_prevote = Duration::from_secs(1);
598        let timeout_precommit = Duration::from_secs(1);
599        let timeout_rebroadcast = timeout_propose + timeout_prevote + timeout_precommit;
600
601        Self {
602            timeout_propose,
603            timeout_propose_delta: Duration::from_millis(500),
604            timeout_prevote,
605            timeout_prevote_delta: Duration::from_millis(500),
606            timeout_precommit,
607            timeout_precommit_delta: Duration::from_millis(500),
608            timeout_rebroadcast,
609        }
610    }
611}
612
613#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
614pub struct MetricsConfig {
615    /// Enable the metrics server
616    pub enabled: bool,
617
618    /// Address at which to serve the metrics at
619    pub listen_addr: SocketAddr,
620}
621
622impl Default for MetricsConfig {
623    fn default() -> Self {
624        MetricsConfig {
625            enabled: false,
626            listen_addr: SocketAddr::new(IpAddr::from([127, 0, 0, 1]), 9000),
627        }
628    }
629}
630
631#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
632#[serde(tag = "flavor", rename_all = "snake_case")]
633pub enum RuntimeConfig {
634    /// Single-threaded runtime
635    #[default]
636    SingleThreaded,
637
638    /// Multi-threaded runtime
639    MultiThreaded {
640        /// Number of worker threads
641        worker_threads: usize,
642    },
643}
644
645impl RuntimeConfig {
646    pub fn single_threaded() -> Self {
647        Self::SingleThreaded
648    }
649
650    pub fn multi_threaded(worker_threads: usize) -> Self {
651        Self::MultiThreaded { worker_threads }
652    }
653}
654
655#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
656pub struct VoteExtensionsConfig {
657    pub enabled: bool,
658    pub size: ByteSize,
659}
660
661#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
662pub struct TestConfig {
663    pub max_block_size: ByteSize,
664    pub txs_per_part: usize,
665    pub time_allowance_factor: f32,
666    #[serde(with = "humantime_serde")]
667    pub exec_time_per_tx: Duration,
668    pub max_retain_blocks: usize,
669    #[serde(default)]
670    pub vote_extensions: VoteExtensionsConfig,
671    #[serde(default)]
672    pub stable_block_times: bool,
673}
674
675impl Default for TestConfig {
676    fn default() -> Self {
677        Self {
678            max_block_size: ByteSize::mib(1),
679            txs_per_part: 256,
680            time_allowance_factor: 0.5,
681            exec_time_per_tx: Duration::from_millis(1),
682            max_retain_blocks: 1000,
683            vote_extensions: VoteExtensionsConfig::default(),
684            stable_block_times: false,
685        }
686    }
687}
688
689#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
690pub struct LoggingConfig {
691    pub log_level: LogLevel,
692    pub log_format: LogFormat,
693}
694
695#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
696#[serde(rename_all = "lowercase")]
697pub enum LogLevel {
698    Trace,
699    #[default]
700    Debug,
701    Warn,
702    Info,
703    Error,
704}
705
706impl FromStr for LogLevel {
707    type Err = String;
708
709    fn from_str(s: &str) -> Result<Self, Self::Err> {
710        match s {
711            "trace" => Ok(LogLevel::Trace),
712            "debug" => Ok(LogLevel::Debug),
713            "warn" => Ok(LogLevel::Warn),
714            "info" => Ok(LogLevel::Info),
715            "error" => Ok(LogLevel::Error),
716            e => Err(format!("Invalid log level: {e}")),
717        }
718    }
719}
720
721impl fmt::Display for LogLevel {
722    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
723        match self {
724            LogLevel::Trace => write!(f, "trace"),
725            LogLevel::Debug => write!(f, "debug"),
726            LogLevel::Warn => write!(f, "warn"),
727            LogLevel::Info => write!(f, "info"),
728            LogLevel::Error => write!(f, "error"),
729        }
730    }
731}
732
733#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
734#[serde(rename_all = "lowercase")]
735pub enum LogFormat {
736    #[default]
737    Plaintext,
738    Json,
739}
740
741impl FromStr for LogFormat {
742    type Err = String;
743
744    fn from_str(s: &str) -> Result<Self, Self::Err> {
745        match s {
746            "plaintext" => Ok(LogFormat::Plaintext),
747            "json" => Ok(LogFormat::Json),
748            e => Err(format!("Invalid log format: {e}")),
749        }
750    }
751}
752
753impl fmt::Display for LogFormat {
754    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
755        match self {
756            LogFormat::Plaintext => write!(f, "plaintext"),
757            LogFormat::Json => write!(f, "json"),
758        }
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765
766    #[test]
767    fn log_format() {
768        assert_eq!(
769            LogFormat::from_str("yaml"),
770            Err("Invalid log format: yaml".to_string())
771        )
772    }
773
774    #[test]
775    fn timeout_durations() {
776        let t = TimeoutConfig::default();
777        assert_eq!(t.timeout_duration(TimeoutKind::Propose), t.timeout_propose);
778        assert_eq!(t.timeout_duration(TimeoutKind::Prevote), t.timeout_prevote);
779        assert_eq!(
780            t.timeout_duration(TimeoutKind::Precommit),
781            t.timeout_precommit
782        );
783    }
784
785    #[test]
786    fn runtime_multi_threaded() {
787        assert_eq!(
788            RuntimeConfig::multi_threaded(5),
789            RuntimeConfig::MultiThreaded { worker_threads: 5 }
790        );
791    }
792
793    #[test]
794    fn log_formatting() {
795        assert_eq!(
796            format!(
797                "{} {} {} {} {}",
798                LogLevel::Trace,
799                LogLevel::Debug,
800                LogLevel::Warn,
801                LogLevel::Info,
802                LogLevel::Error
803            ),
804            "trace debug warn info error"
805        );
806
807        assert_eq!(
808            format!("{} {}", LogFormat::Plaintext, LogFormat::Json),
809            "plaintext json"
810        );
811    }
812}