Skip to main content

solana_core/
banking_trace.rs

1use {
2    agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver},
3    bincode::serialize_into,
4    chrono::{DateTime, Local},
5    crossbeam_channel::{Receiver, SendError, TryRecvError, TrySendError, bounded},
6    rolling_file::{RollingCondition, RollingConditionBasic, RollingFileAppender},
7    serde::{Deserialize, Serialize},
8    solana_clock::Slot,
9    solana_hash::Hash,
10    solana_streamer::{evicting_sender::EvictingSender, streamer::ChannelSend},
11    std::{
12        fs::{create_dir_all, remove_dir_all},
13        io::{self, Write},
14        path::PathBuf,
15        sync::{
16            Arc,
17            atomic::{AtomicBool, Ordering},
18        },
19        thread::{self, JoinHandle, sleep},
20        time::{Duration, SystemTime},
21    },
22    thiserror::Error,
23};
24
25/// Capacity of the vote channel between sigverify and the banking-stage.
26/// Sized to fit all votes from a reasonably sized cluster for 1 slot, + margin.
27const VOTE_CHANNEL_CAPACITY: usize = 1024 * 8;
28
29/// Capacity of the non-vote (transaction) channel between sigverify and the banking-stage.
30/// Larger than the vote channel to absorb bursty TPU load.
31const NON_VOTE_CHANNEL_CAPACITY: usize = 1024 * 16;
32
33pub type BankingPacketSender = TracedSender;
34pub type TracerThreadResult = Result<(), TraceError>;
35pub type TracerThread = Option<JoinHandle<TracerThreadResult>>;
36pub type DirByteLimit = u64;
37
38#[derive(Error, Debug)]
39pub enum TraceError {
40    #[error("IO Error: {0}")]
41    IoError(#[from] std::io::Error),
42
43    #[error("Serialization Error: {0}")]
44    SerializeError(#[from] bincode::Error),
45
46    #[error("Integer Cast Error: {0}")]
47    IntegerCastError(#[from] std::num::TryFromIntError),
48
49    #[error("Trace directory's byte limit is too small (must be larger than {1}): {0}")]
50    TooSmallDirByteLimit(DirByteLimit, DirByteLimit),
51}
52
53pub(crate) const BASENAME: &str = "events";
54const TRACE_FILE_ROTATE_COUNT: u64 = 14; // target 2 weeks retention under normal load
55const TRACE_FILE_WRITE_INTERVAL_MS: u64 = 100;
56const BUF_WRITER_CAPACITY: usize = 10 * 1024 * 1024;
57pub const TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD: u64 = 1024 * 1024 * 1024;
58pub const DISABLED_BAKING_TRACE_DIR: DirByteLimit = 0;
59pub const BANKING_TRACE_DIR_DEFAULT_BYTE_LIMIT: DirByteLimit =
60    TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD * TRACE_FILE_ROTATE_COUNT;
61
62#[derive(Clone)]
63struct ActiveTracer {
64    trace_sender: EvictingSender<TimedTracedEvent>,
65    exit: Arc<AtomicBool>,
66}
67
68pub struct BankingTracer {
69    active_tracer: Option<ActiveTracer>,
70}
71
72#[cfg_attr(
73    feature = "frozen-abi",
74    derive(AbiExample),
75    frozen_abi(digest = "DY2zjwewCSNansb5xwtoxkCcNuXbVmWZe3U9nNH2kzNz")
76)]
77#[derive(Serialize, Deserialize, Debug)]
78pub struct TimedTracedEvent(pub std::time::SystemTime, pub TracedEvent);
79
80#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))]
81#[derive(Serialize, Deserialize, Debug)]
82pub enum TracedEvent {
83    PacketBatch(ChannelLabel, BankingPacketBatch),
84    BlockAndBankHash(Slot, Hash, Hash),
85}
86
87#[cfg_attr(
88    feature = "frozen-abi",
89    derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample)
90)]
91#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
92pub enum ChannelLabel {
93    NonVote,
94    TpuVote,
95    GossipVote,
96    Dummy,
97}
98
99struct RollingConditionGrouped {
100    basic: RollingConditionBasic,
101    tried_rollover_after_opened: bool,
102    is_checked: bool,
103}
104
105impl RollingConditionGrouped {
106    fn new(basic: RollingConditionBasic) -> Self {
107        Self {
108            basic,
109            tried_rollover_after_opened: bool::default(),
110            is_checked: bool::default(),
111        }
112    }
113
114    fn reset(&mut self) {
115        self.is_checked = false;
116    }
117}
118
119struct GroupedWriter<'a> {
120    now: DateTime<Local>,
121    underlying: &'a mut RollingFileAppender<RollingConditionGrouped>,
122}
123
124impl<'a> GroupedWriter<'a> {
125    fn new(underlying: &'a mut RollingFileAppender<RollingConditionGrouped>) -> Self {
126        Self {
127            now: Local::now(),
128            underlying,
129        }
130    }
131}
132
133impl RollingCondition for RollingConditionGrouped {
134    fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
135        if !self.tried_rollover_after_opened {
136            self.tried_rollover_after_opened = true;
137
138            // rollover normally if empty to reuse it if possible
139            if current_filesize > 0 {
140                // forcibly rollover anew, so that we always avoid to append
141                // to a possibly-damaged tracing file even after unclean
142                // restarts
143                return true;
144            }
145        }
146
147        if !self.is_checked {
148            self.is_checked = true;
149            self.basic.should_rollover(now, current_filesize)
150        } else {
151            false
152        }
153    }
154}
155
156impl Write for GroupedWriter<'_> {
157    fn write(&mut self, buf: &[u8]) -> std::result::Result<usize, io::Error> {
158        self.underlying.write_with_datetime(buf, &self.now)
159    }
160    fn flush(&mut self) -> std::result::Result<(), io::Error> {
161        self.underlying.flush()
162    }
163}
164
165pub fn receiving_loop_with_minimized_sender_overhead<T, E, const SLEEP_MS: u64>(
166    exit: Arc<AtomicBool>,
167    receiver: Receiver<T>,
168    mut on_recv: impl FnMut(T) -> Result<(), E>,
169) -> Result<(), E> {
170    'outer: while !exit.load(Ordering::Relaxed) {
171        'inner: loop {
172            // avoid futex-based blocking here, otherwise a sender would have to
173            // wake me up at a syscall cost...
174            match receiver.try_recv() {
175                Ok(message) => on_recv(message)?,
176                Err(TryRecvError::Empty) => break 'inner,
177                Err(TryRecvError::Disconnected) => {
178                    break 'outer;
179                }
180            };
181            if exit.load(Ordering::Relaxed) {
182                break 'outer;
183            }
184        }
185        sleep(Duration::from_millis(SLEEP_MS));
186    }
187
188    Ok(())
189}
190
191/// A small grouping struct to temporarily hold banking packet channel senders and receivers with
192/// different source labels for the banking stage setup.
193pub struct Channels {
194    pub non_vote_sender: BankingPacketSender,
195    pub non_vote_receiver: BankingPacketReceiver,
196    pub tpu_vote_sender: BankingPacketSender,
197    pub tpu_vote_receiver: BankingPacketReceiver,
198    pub gossip_vote_sender: BankingPacketSender,
199    pub gossip_vote_receiver: BankingPacketReceiver,
200}
201
202impl BankingTracer {
203    pub fn new(
204        maybe_config: Option<(&PathBuf, Arc<AtomicBool>, DirByteLimit)>,
205    ) -> Result<(Arc<Self>, TracerThread), TraceError> {
206        match maybe_config {
207            None => Ok((Self::new_disabled(), None)),
208            Some((path, exit, dir_byte_limit)) => {
209                let rotate_threshold_size = dir_byte_limit / TRACE_FILE_ROTATE_COUNT;
210                if rotate_threshold_size == 0 {
211                    return Err(TraceError::TooSmallDirByteLimit(
212                        dir_byte_limit,
213                        TRACE_FILE_ROTATE_COUNT,
214                    ));
215                }
216
217                const TRACING_CHANNEL_CAPACITY: usize = 50_000;
218                let (trace_sender, trace_receiver) =
219                    EvictingSender::new_bounded(TRACING_CHANNEL_CAPACITY);
220
221                let file_appender = Self::create_file_appender(path, rotate_threshold_size)?;
222
223                let tracer_thread =
224                    Self::spawn_background_thread(trace_receiver, file_appender, exit.clone())?;
225
226                Ok((
227                    Arc::new(Self {
228                        active_tracer: Some(ActiveTracer { trace_sender, exit }),
229                    }),
230                    Some(tracer_thread),
231                ))
232            }
233        }
234    }
235
236    pub fn new_disabled() -> Arc<Self> {
237        Arc::new(Self {
238            active_tracer: None,
239        })
240    }
241
242    pub fn is_enabled(&self) -> bool {
243        self.active_tracer.is_some()
244    }
245
246    pub fn hash_event(&self, slot: Slot, blockhash: &Hash, bank_hash: &Hash) {
247        self.trace_event(|| {
248            TimedTracedEvent(
249                SystemTime::now(),
250                TracedEvent::BlockAndBankHash(slot, *blockhash, *bank_hash),
251            )
252        })
253    }
254
255    fn trace_event(&self, on_trace: impl Fn() -> TimedTracedEvent) {
256        if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer
257            && !exit.load(Ordering::Relaxed)
258        {
259            // Ignore errors in sending to tracer - it is a non-critical component.
260            let _ = trace_sender.try_send(on_trace());
261        }
262    }
263
264    pub fn create_channels(&self) -> Channels {
265        let (non_vote_sender, non_vote_receiver) = self.create_channel_non_vote();
266
267        let (tpu_vote_sender, tpu_vote_receiver) = Self::channel(
268            ChannelLabel::TpuVote,
269            VOTE_CHANNEL_CAPACITY,
270            self.active_tracer.as_ref().cloned(),
271        );
272        let (gossip_vote_sender, gossip_vote_receiver) = Self::channel(
273            ChannelLabel::GossipVote,
274            VOTE_CHANNEL_CAPACITY,
275            self.active_tracer.as_ref().cloned(),
276        );
277
278        Channels {
279            non_vote_sender,
280            non_vote_receiver,
281            tpu_vote_sender,
282            tpu_vote_receiver,
283            gossip_vote_sender,
284            gossip_vote_receiver,
285        }
286    }
287
288    pub fn create_channel_non_vote(&self) -> (BankingPacketSender, BankingPacketReceiver) {
289        Self::channel(
290            ChannelLabel::NonVote,
291            NON_VOTE_CHANNEL_CAPACITY,
292            self.active_tracer.as_ref().cloned(),
293        )
294    }
295
296    pub fn channel_for_test() -> (TracedSender, Receiver<BankingPacketBatch>) {
297        Self::channel(ChannelLabel::Dummy, VOTE_CHANNEL_CAPACITY, None)
298    }
299
300    fn channel(
301        label: ChannelLabel,
302        capacity: usize,
303        active_tracer: Option<ActiveTracer>,
304    ) -> (TracedSender, Receiver<BankingPacketBatch>) {
305        let (sender, receiver) = bounded(capacity);
306        let evicting = EvictingSender::new(sender, receiver.clone());
307
308        (TracedSender::new(label, evicting, active_tracer), receiver)
309    }
310
311    pub fn ensure_cleanup_path(path: &PathBuf) -> Result<(), io::Error> {
312        remove_dir_all(path).or_else(|err| {
313            if err.kind() == io::ErrorKind::NotFound {
314                Ok(())
315            } else {
316                Err(err)
317            }
318        })
319    }
320
321    fn create_file_appender(
322        path: &PathBuf,
323        rotate_threshold_size: u64,
324    ) -> Result<RollingFileAppender<RollingConditionGrouped>, TraceError> {
325        create_dir_all(path)?;
326        let grouped = RollingConditionGrouped::new(
327            RollingConditionBasic::new()
328                .daily()
329                .max_size(rotate_threshold_size),
330        );
331        let appender = RollingFileAppender::new_with_buffer_capacity(
332            path.join(BASENAME),
333            grouped,
334            (TRACE_FILE_ROTATE_COUNT - 1).try_into()?,
335            BUF_WRITER_CAPACITY,
336        )?;
337        Ok(appender)
338    }
339
340    fn spawn_background_thread(
341        trace_receiver: Receiver<TimedTracedEvent>,
342        mut file_appender: RollingFileAppender<RollingConditionGrouped>,
343        exit: Arc<AtomicBool>,
344    ) -> Result<JoinHandle<TracerThreadResult>, TraceError> {
345        let thread = thread::Builder::new().name("solBanknTracer".into()).spawn(
346            move || -> TracerThreadResult {
347                receiving_loop_with_minimized_sender_overhead::<_, _, TRACE_FILE_WRITE_INTERVAL_MS>(
348                    exit,
349                    trace_receiver,
350                    |event| -> Result<(), TraceError> {
351                        file_appender.condition_mut().reset();
352                        serialize_into(&mut GroupedWriter::new(&mut file_appender), &event)?;
353                        Ok(())
354                    },
355                )?;
356                file_appender.flush()?;
357                Ok(())
358            },
359        )?;
360
361        Ok(thread)
362    }
363}
364
365/// A small wrapper around the sender of crossbeam channels to message banking packet batches.
366///
367/// The underlying channels are used by the banking stage to receive packets from multiple sources.
368/// The sources are labelled as non-vote, tpu-vote, and gossip-vote respectively. As such, traced
369/// senders are separately constructed for each of these sources, then all are grouped under the
370/// `Channels` struct transiently during setup.
371///
372/// This wrapper exists to enable the banking trace functionality conditionally on creation.
373///
374/// Also, this wrapper can dynamically switch sending batches to an alternate channel, called the
375/// unified channel, depending on a flag. Both the actual crossbeam sender for the unified channel
376/// and the flag are expected to be shared among all of traced sender instances, which will be used
377/// by the trio of sources respectively. This routing functionality is needed for unified scheduler
378/// and its runtime switching.
379#[derive(Clone)]
380pub struct TracedSender {
381    label: ChannelLabel,
382    sender: EvictingSender<BankingPacketBatch>,
383    active_tracer: Option<ActiveTracer>,
384}
385
386impl TracedSender {
387    fn new(
388        label: ChannelLabel,
389        sender: EvictingSender<BankingPacketBatch>,
390        active_tracer: Option<ActiveTracer>,
391    ) -> Self {
392        Self {
393            label,
394            sender,
395            active_tracer,
396        }
397    }
398
399    /// Send a batch on the channel. This may evict an existing batch to make
400    /// room; in that case `Ok(n)` is returned where `n` is the number of
401    /// evicted packets. On channel disconnect returns `Err(SendError)`.
402    pub fn send(&self, batch: BankingPacketBatch) -> Result<usize, SendError<BankingPacketBatch>> {
403        if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer
404            && !exit.load(Ordering::Relaxed)
405        {
406            // Ignore errors in sending to tracer - it is a non-critical component.
407            let _ = trace_sender.try_send(TimedTracedEvent(
408                SystemTime::now(),
409                TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)),
410            ));
411        }
412        match self.sender.try_send(batch) {
413            Ok(()) => Ok(0),
414            Err(TrySendError::Full(b)) => Ok(b.iter().map(|pb| pb.len()).sum()),
415            Err(TrySendError::Disconnected(b)) => Err(SendError(b)),
416        }
417    }
418
419    pub fn len(&self) -> usize {
420        self.sender.len()
421    }
422
423    pub fn is_empty(&self) -> bool {
424        self.len() == 0
425    }
426}
427
428#[cfg(any(test, feature = "dev-context-only-utils"))]
429pub mod for_test {
430    use {
431        super::*,
432        solana_perf::{packet::to_packet_batches, test_tx::test_tx},
433        tempfile::TempDir,
434    };
435
436    pub fn sample_packet_batch() -> BankingPacketBatch {
437        BankingPacketBatch::new(to_packet_batches(&vec![test_tx(); 4], 10))
438    }
439
440    pub fn drop_and_clean_temp_dir_unless_suppressed(temp_dir: TempDir) {
441        std::env::var("BANKING_TRACE_LEAVE_FILES").is_ok().then(|| {
442            warn!("prevented to remove {:?}", temp_dir.path());
443            drop(temp_dir.keep());
444        });
445    }
446
447    pub fn terminate_tracer(
448        tracer: Arc<BankingTracer>,
449        tracer_thread: TracerThread,
450        main_thread: JoinHandle<TracerThreadResult>,
451        sender: TracedSender,
452        exit: Option<Arc<AtomicBool>>,
453    ) {
454        if let Some(exit) = exit {
455            exit.store(true, Ordering::Relaxed);
456        }
457        drop((sender, tracer));
458        main_thread.join().unwrap().unwrap();
459        if let Some(tracer_thread) = tracer_thread {
460            tracer_thread.join().unwrap().unwrap();
461        }
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use {
468        super::*,
469        bincode::ErrorKind::Io as BincodeIoError,
470        std::{
471            fs::File,
472            io::{BufReader, ErrorKind::UnexpectedEof},
473            str::FromStr,
474        },
475        tempfile::TempDir,
476    };
477
478    #[test]
479    fn test_new_disabled() {
480        let exit = Arc::<AtomicBool>::default();
481
482        let tracer = BankingTracer::new_disabled();
483        let (non_vote_sender, non_vote_receiver) = tracer.create_channel_non_vote();
484
485        let dummy_main_thread = thread::spawn(move || {
486            receiving_loop_with_minimized_sender_overhead::<_, TraceError, 0>(
487                exit,
488                non_vote_receiver,
489                |_packet_batch| Ok(()),
490            )
491        });
492
493        non_vote_sender
494            .send(BankingPacketBatch::new(vec![]))
495            .unwrap();
496        for_test::terminate_tracer(tracer, None, dummy_main_thread, non_vote_sender, None);
497    }
498
499    #[test]
500    fn test_send_after_exited() {
501        let temp_dir = TempDir::new().unwrap();
502        let path = temp_dir.path().join("banking-trace");
503        let exit = Arc::<AtomicBool>::default();
504        let (tracer, tracer_thread) =
505            BankingTracer::new(Some((&path, exit.clone(), DirByteLimit::MAX))).unwrap();
506        let (non_vote_sender, non_vote_receiver) = tracer.create_channel_non_vote();
507
508        let exit_for_dummy_thread = Arc::<AtomicBool>::default();
509        let exit_for_dummy_thread2 = exit_for_dummy_thread.clone();
510        let dummy_main_thread = thread::spawn(move || {
511            receiving_loop_with_minimized_sender_overhead::<_, TraceError, 0>(
512                exit_for_dummy_thread,
513                non_vote_receiver,
514                |_packet_batch| Ok(()),
515            )
516        });
517
518        // kill and join the tracer thread
519        exit.store(true, Ordering::Relaxed);
520        tracer_thread.unwrap().join().unwrap().unwrap();
521
522        // .hash_event() must succeed even after exit is already set to true
523        let blockhash = Hash::from_str("B1ockhash1111111111111111111111111111111111").unwrap();
524        let bank_hash = Hash::from_str("BankHash11111111111111111111111111111111111").unwrap();
525        tracer.hash_event(4, &blockhash, &bank_hash);
526
527        drop(tracer);
528
529        // .send() must succeed even after exit is already set to true and further tracer is
530        // already dropped
531        non_vote_sender
532            .send(for_test::sample_packet_batch())
533            .unwrap();
534
535        // finally terminate and join the main thread
536        exit_for_dummy_thread2.store(true, Ordering::Relaxed);
537        dummy_main_thread.join().unwrap().unwrap();
538    }
539
540    #[test]
541    fn test_record_and_restore() {
542        let temp_dir = TempDir::new().unwrap();
543        let path = temp_dir.path().join("banking-trace");
544        let exit = Arc::<AtomicBool>::default();
545        let (tracer, tracer_thread) =
546            BankingTracer::new(Some((&path, exit.clone(), DirByteLimit::MAX))).unwrap();
547        let (non_vote_sender, non_vote_receiver) = tracer.create_channel_non_vote();
548
549        let dummy_main_thread = thread::spawn(move || {
550            receiving_loop_with_minimized_sender_overhead::<_, TraceError, 0>(
551                exit,
552                non_vote_receiver,
553                |_packet_batch| Ok(()),
554            )
555        });
556
557        non_vote_sender
558            .send(for_test::sample_packet_batch())
559            .unwrap();
560        let blockhash = Hash::from_str("B1ockhash1111111111111111111111111111111111").unwrap();
561        let bank_hash = Hash::from_str("BankHash11111111111111111111111111111111111").unwrap();
562        tracer.hash_event(4, &blockhash, &bank_hash);
563
564        for_test::terminate_tracer(
565            tracer,
566            tracer_thread,
567            dummy_main_thread,
568            non_vote_sender,
569            None,
570        );
571
572        let mut stream = BufReader::new(File::open(path.join(BASENAME)).unwrap());
573        let results = (0..=3)
574            .map(|_| bincode::deserialize_from::<_, TimedTracedEvent>(&mut stream))
575            .collect::<Vec<_>>();
576
577        let mut i = 0;
578        assert_matches!(
579            results[i],
580            Ok(TimedTracedEvent(
581                _,
582                TracedEvent::PacketBatch(ChannelLabel::NonVote, _)
583            ))
584        );
585        i += 1;
586        assert_matches!(
587            results[i],
588            Ok(TimedTracedEvent(
589                _,
590                TracedEvent::BlockAndBankHash(4, actual_blockhash, actual_bank_hash)
591            )) if actual_blockhash == blockhash && actual_bank_hash == bank_hash
592        );
593        i += 1;
594        assert_matches!(
595            results[i],
596            Err(ref err) if matches!(
597                **err,
598                BincodeIoError(ref error) if error.kind() == UnexpectedEof
599            )
600        );
601
602        for_test::drop_and_clean_temp_dir_unless_suppressed(temp_dir);
603    }
604
605    #[test]
606    fn test_spill_over_at_rotation() {
607        let temp_dir = TempDir::new().unwrap();
608        let path = temp_dir.path().join("banking-trace");
609        const REALLY_SMALL_ROTATION_THRESHOLD: u64 = 1;
610
611        let mut file_appender =
612            BankingTracer::create_file_appender(&path, REALLY_SMALL_ROTATION_THRESHOLD).unwrap();
613        file_appender.write_all(b"foo").unwrap();
614        file_appender.condition_mut().reset();
615        file_appender.write_all(b"bar").unwrap();
616        file_appender.condition_mut().reset();
617        file_appender.flush().unwrap();
618
619        assert_eq!(
620            [
621                std::fs::read_to_string(path.join("events")).ok(),
622                std::fs::read_to_string(path.join("events.1")).ok(),
623                std::fs::read_to_string(path.join("events.2")).ok(),
624            ],
625            [Some("bar".into()), Some("foo".into()), None]
626        );
627
628        for_test::drop_and_clean_temp_dir_unless_suppressed(temp_dir);
629    }
630
631    #[test]
632    fn test_reopen_with_blank_file() {
633        let temp_dir = TempDir::new().unwrap();
634
635        let path = temp_dir.path().join("banking-trace");
636
637        let mut file_appender =
638            BankingTracer::create_file_appender(&path, TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD)
639                .unwrap();
640        // assume this is unclean write
641        file_appender.write_all(b"f").unwrap();
642        file_appender.flush().unwrap();
643
644        // reopen while shadow-dropping the old tracer
645        let mut file_appender =
646            BankingTracer::create_file_appender(&path, TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD)
647                .unwrap();
648        // new file won't be created as appender is lazy
649        assert_eq!(
650            [
651                std::fs::read_to_string(path.join("events")).ok(),
652                std::fs::read_to_string(path.join("events.1")).ok(),
653                std::fs::read_to_string(path.join("events.2")).ok(),
654            ],
655            [Some("f".into()), None, None]
656        );
657
658        // initial write actually creates the new blank file
659        file_appender.write_all(b"bar").unwrap();
660        assert_eq!(
661            [
662                std::fs::read_to_string(path.join("events")).ok(),
663                std::fs::read_to_string(path.join("events.1")).ok(),
664                std::fs::read_to_string(path.join("events.2")).ok(),
665            ],
666            [Some("".into()), Some("f".into()), None]
667        );
668
669        // flush actually write the actual data
670        file_appender.flush().unwrap();
671        assert_eq!(
672            [
673                std::fs::read_to_string(path.join("events")).ok(),
674                std::fs::read_to_string(path.join("events.1")).ok(),
675                std::fs::read_to_string(path.join("events.2")).ok(),
676            ],
677            [Some("bar".into()), Some("f".into()), None]
678        );
679
680        for_test::drop_and_clean_temp_dir_unless_suppressed(temp_dir);
681    }
682}