Skip to main content

diagnostics_log/fuchsia/
mod.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
3
4use crate::PublishOptions;
5use diagnostics_log_types::Severity;
6use fidl::endpoints::ClientEnd;
7use fidl_fuchsia_diagnostics_types_common::Interest;
8use fidl_fuchsia_logger::{
9    LogSinkEvent, LogSinkMarker, LogSinkOnInitRequest, LogSinkProxy, LogSinkSynchronousProxy,
10};
11use fuchsia_async as fasync;
12use fuchsia_component_client::connect::connect_to_protocol;
13#[cfg(not(feature = "no_startup_handle"))]
14use fuchsia_runtime::{HandleInfo, HandleType};
15use fuchsia_sync::Mutex;
16use futures::stream::StreamExt;
17use std::borrow::Borrow;
18use std::collections::HashSet;
19use std::fmt::Debug;
20use std::sync::Arc;
21use thiserror::Error;
22
23mod filter;
24mod sink;
25
26use filter::InterestFilter;
27use sink::{BufferedSink, IoBufferSink, Sink, SinkConfig};
28
29pub use diagnostics_log_encoding::Metatag;
30pub use diagnostics_log_encoding::encode::{LogEvent, TestRecord};
31pub use paste::paste;
32
33#[cfg(test)]
34use std::{
35    sync::atomic::{AtomicI64, Ordering},
36    time::Duration,
37};
38
39/// Callback for interest listeners
40pub trait OnInterestChanged {
41    /// Callback for when the interest changes
42    fn on_changed(&self, severity: Severity);
43}
44
45/// Options to configure a `Publisher`.
46#[derive(Default)]
47pub struct PublisherOptions<'t> {
48    pub(crate) interest: Interest,
49    listen_for_interest_updates: bool,
50    log_sink_client: Option<ClientEnd<LogSinkMarker>>,
51    pub(crate) metatags: HashSet<Metatag>,
52    pub(crate) tags: &'t [&'t str],
53    pub(crate) always_log_file_line: bool,
54    register_global_logger: bool,
55}
56
57impl Default for PublishOptions<'static> {
58    fn default() -> Self {
59        Self {
60            publisher: PublisherOptions {
61                // Default to registering the global logger and listening for interest updates for
62                // `PublishOptions` because it's used by the `initialize...` functions which are
63                // typically called at program start time.
64                listen_for_interest_updates: true,
65                register_global_logger: true,
66                ..PublisherOptions::default()
67            },
68            install_panic_hook: true,
69            panic_prefix: None,
70        }
71    }
72}
73
74macro_rules! publisher_options {
75    ($(($name:ident, $self:ident, $($self_arg:ident),*)),*) => {
76        $(
77            impl<'t> $name<'t> {
78                /// Whether or not to log file/line information regardless of severity.
79                ///
80                /// Default: false.
81                pub fn log_file_line_info(mut $self, enable: bool) -> Self {
82                    let this = &mut $self$(.$self_arg)*;
83                    this.always_log_file_line = enable;
84                    $self
85                }
86
87                /// When set, a `fuchsia_async::Task` will be spawned and held that will be
88                /// listening for interest changes. This option can only be set if
89                /// `register_global_logger` is set.
90                ///
91                /// Default: true for `PublishOptions`, false for `PublisherOptions`.
92                pub fn listen_for_interest_updates(mut $self, enable: bool) -> Self {
93                    let this = &mut $self$(.$self_arg)*;
94                    this.listen_for_interest_updates = enable;
95                    $self
96                }
97
98                /// Sets the `LogSink` that will be used.
99                ///
100                /// Default: the `fuchsia.logger.LogSink` available in the incoming namespace.
101                pub fn use_log_sink(mut $self, client: ClientEnd<LogSinkMarker>) -> Self {
102                    let this = &mut $self$(.$self_arg)*;
103                    this.log_sink_client = Some(client);
104                    $self
105                }
106
107                /// When set to true, the publisher will be registered as the global logger. This
108                /// can only be done once.
109                ///
110                /// Default: true for `PublishOptions`, false for `PublisherOptions`.
111                pub fn register_global_logger(mut $self, value: bool) -> Self {
112                    let this = &mut $self$(.$self_arg)*;
113                    this.register_global_logger = value;
114                    $self
115                }
116            }
117        )*
118    };
119}
120
121publisher_options!((PublisherOptions, self,), (PublishOptions, self, publisher));
122
123/// Initializes logging with the given options.
124///
125/// IMPORTANT: this should be called at most once in a program, and must be
126/// called only after an async executor has been set for the current thread,
127/// otherwise it'll return errors or panic. Therefore it's recommended to never
128/// call this from libraries and only do it from binaries.
129// Ideally this would be an async function, but fixing that is a bit of a Yak shave.
130pub fn initialize(opts: PublishOptions<'_>) -> Result<(), PublishError> {
131    let result = Publisher::new_sync_with_async_listener(opts.publisher);
132    if matches!(result, Err(PublishError::MissingOnInit)) {
133        // NOTE: We ignore missing OnInit errors as these can happen on products where the log sink
134        // connection isn't routed. If this is a mistake, then there will be warning messages from
135        // Component Manager regarding failed routing.
136        return Ok(());
137    }
138    result?;
139    if opts.install_panic_hook {
140        crate::install_panic_hook(opts.panic_prefix);
141    }
142    Ok(())
143}
144
145/// Sets the global minimum log severity.
146/// IMPORTANT: this function can panic if `initialize` wasn't called before.
147pub fn set_minimum_severity(severity: impl Into<Severity>) {
148    let severity: Severity = severity.into();
149    log::set_max_level(severity.into());
150}
151
152/// Initializes logging with the given options.
153///
154/// This must be used when working in an environment where a [`fuchsia_async::Executor`] can't be
155/// used.
156///
157/// IMPORTANT: this should be called at most once in a program, and must be
158/// called only after an async executor has been set for the current thread,
159/// otherwise it'll return errors or panic. Therefore it's recommended to never
160/// call this from libraries and only do it from binaries.
161pub fn initialize_sync(opts: PublishOptions<'_>) {
162    match Publisher::new_sync(opts.publisher) {
163        Ok(_) => {}
164        Err(PublishError::MissingOnInit) => {
165            // NOTE: We ignore missing OnInit errors as these can happen on products where the log
166            // sink connection isn't routed. If this is a mistake, then there will be warning
167            // messages from Component Manager regarding failed routing.
168            return;
169        }
170        Err(e) => panic!("Unable to initialize logging: {e:?}"),
171    }
172    if opts.install_panic_hook {
173        crate::install_panic_hook(opts.panic_prefix);
174    }
175}
176
177/// A `Publisher` acts as broker, implementing [`log::Log`] to receive log
178/// events from a component, and then forwarding that data on to a diagnostics service.
179#[derive(Clone)]
180pub struct Publisher {
181    inner: Arc<InnerPublisher>,
182}
183
184struct InnerPublisher {
185    sink: IoBufferSink,
186    filter: InterestFilter,
187}
188
189impl Publisher {
190    fn new(opts: PublisherOptions<'_>, iob: zx::Iob) -> Self {
191        Self {
192            inner: Arc::new(InnerPublisher {
193                sink: IoBufferSink::new(
194                    iob,
195                    SinkConfig {
196                        tags: opts.tags.iter().map(|s| s.to_string()).collect(),
197                        metatags: opts.metatags,
198                        always_log_file_line: opts.always_log_file_line,
199                    },
200                ),
201                filter: InterestFilter::new(opts.interest),
202            }),
203        }
204    }
205
206    /// Returns a new `Publisher`. This will connect synchronously and, if configured, run a
207    /// listener in a separate thread.
208    pub fn new_sync(opts: PublisherOptions<'_>) -> Result<Self, PublishError> {
209        let listen_for_interest_updates = opts.listen_for_interest_updates;
210        let (publisher, client) = Self::new_sync_no_listener(opts)?;
211        if listen_for_interest_updates {
212            let publisher = publisher.clone();
213            std::thread::spawn(move || {
214                fuchsia_async::LocalExecutor::default()
215                    .run_singlethreaded(publisher.listen_for_interest_updates(client.into_proxy()));
216            });
217        }
218        Ok(publisher)
219    }
220
221    /// Returns a new `Publisher`. This will connect synchronously and, if configured, run a
222    /// listener in an async task. Prefer to use `new_async`.
223    pub fn new_sync_with_async_listener(opts: PublisherOptions<'_>) -> Result<Self, PublishError> {
224        let listen_for_interest_updates = opts.listen_for_interest_updates;
225        let (publisher, client) = Self::new_sync_no_listener(opts)?;
226        if listen_for_interest_updates {
227            fasync::Task::spawn(publisher.clone().listen_for_interest_updates(client.into_proxy()))
228                .detach();
229        }
230        Ok(publisher)
231    }
232
233    /// Returns a new `Publisher`, but doesn't listen for interest updates. This will connect
234    /// synchronously.
235    fn new_sync_no_listener(
236        mut opts: PublisherOptions<'_>,
237    ) -> Result<(Self, ClientEnd<LogSinkMarker>), PublishError> {
238        let PublisherOptions { listen_for_interest_updates, register_global_logger, .. } = opts;
239
240        if listen_for_interest_updates && !register_global_logger {
241            // We can only support listening for interest updates if we are registering a global
242            // logger. This is because if we don't register, the initial interest is dropped.
243            return Err(PublishError::UnsupportedOption);
244        }
245
246        let mut get_client = || match opts.log_sink_client.take() {
247            Some(log_sink) => Ok(log_sink),
248            None => {
249                #[cfg(not(feature = "no_startup_handle"))]
250                {
251                    let log_sink = fuchsia_runtime::take_startup_handle(HandleInfo::new(
252                        HandleType::LogSink,
253                        0,
254                    ));
255                    if let Some(log_sink) = log_sink {
256                        let log_sink = fidl::Channel::from(log_sink);
257                        return Ok(ClientEnd::<LogSinkMarker>::from(log_sink));
258                    }
259                }
260                // If startup handle is unavailable fallback to /svc.
261                connect_to_protocol()
262                    .map_err(|e| e.to_string())
263                    .map_err(PublishError::LogSinkConnect)
264            }
265        };
266        let client = (get_client)()?;
267
268        let proxy = zx::Unowned::<LogSinkSynchronousProxy>::new(client.channel());
269        let Ok(LogSinkEvent::OnInit {
270            payload: LogSinkOnInitRequest { buffer: Some(iob), interest, .. },
271        }) = proxy.wait_for_event(zx::MonotonicInstant::INFINITE)
272        else {
273            return Err(PublishError::MissingOnInit);
274        };
275
276        let publisher = Self::new(opts, iob);
277
278        if register_global_logger {
279            publisher.register_logger(if listen_for_interest_updates { interest } else { None })?;
280        }
281
282        Ok((publisher, client))
283    }
284
285    /// Returns a new `Publisher`. This will connect asynchronously and, if configured, run a
286    /// listener in an async task.
287    pub async fn new_async(mut opts: PublisherOptions<'_>) -> Result<Self, PublishError> {
288        let PublisherOptions { listen_for_interest_updates, register_global_logger, .. } = opts;
289
290        if listen_for_interest_updates && !register_global_logger {
291            // We can only support listening for interest updates if we are registering a global
292            // logger. This is because if we don't register, the initial interest is dropped.
293            return Err(PublishError::UnsupportedOption);
294        }
295
296        let mut get_client = || match opts.log_sink_client.take() {
297            Some(log_sink) => Ok(log_sink.into_proxy()),
298            None => {
299                #[cfg(not(feature = "no_startup_handle"))]
300                {
301                    let log_sink = fuchsia_runtime::take_startup_handle(HandleInfo::new(
302                        HandleType::LogSink,
303                        0,
304                    ));
305                    if let Some(log_sink) = log_sink {
306                        let log_sink = fidl::Channel::from(log_sink);
307                        let log_sink = ClientEnd::<LogSinkMarker>::from(log_sink);
308                        return Ok(log_sink.into_proxy());
309                    }
310                }
311                // If startup handle is unavailable fallback to /svc.
312                connect_to_protocol()
313                    .map_err(|e| e.to_string())
314                    .map_err(PublishError::LogSinkConnect)
315            }
316        };
317        let proxy = (get_client)()?;
318
319        let Some(Ok(LogSinkEvent::OnInit {
320            payload: LogSinkOnInitRequest { buffer: Some(iob), interest, .. },
321        })) = proxy.take_event_stream().next().await
322        else {
323            return Err(PublishError::MissingOnInit);
324        };
325
326        let publisher = Self::new(opts, iob);
327
328        if register_global_logger {
329            publisher.register_logger(if listen_for_interest_updates { interest } else { None })?;
330            fasync::Task::spawn(publisher.clone().listen_for_interest_updates(proxy)).detach();
331        }
332
333        Ok(publisher)
334    }
335
336    /// Publish the provided event for testing.
337    pub fn event_for_testing(&self, record: TestRecord<'_>) {
338        if self.inner.filter.enabled_for_testing(&record) {
339            self.inner.sink.event_for_testing(record);
340        }
341    }
342
343    /// Registers an interest listener
344    pub fn set_interest_listener<T>(&self, listener: T)
345    where
346        T: OnInterestChanged + Send + Sync + 'static,
347    {
348        self.inner.filter.set_interest_listener(listener);
349    }
350
351    /// Sets the global logger to this publisher. This function may only be called once in the
352    /// lifetime of a program.
353    pub fn register_logger(&self, interest: Option<Interest>) -> Result<(), PublishError> {
354        self.inner.filter.update_interest(interest.unwrap_or_default());
355        // SAFETY: This leaks which guarantees the publisher remains alive for the lifetime of the
356        // program.
357        unsafe {
358            let ptr = Arc::into_raw(self.inner.clone());
359            log::set_logger(&*ptr).inspect_err(|_| {
360                let _ = Arc::from_raw(ptr);
361            })?;
362        }
363        Ok(())
364    }
365
366    /// Listens for interest updates. Callers must maintain a clone to keep the publisher alive;
367    /// this function will downgrade to a weak reference.
368    async fn listen_for_interest_updates(self, proxy: LogSinkProxy) {
369        self.inner.filter.listen_for_interest_updates(proxy).await;
370    }
371}
372
373impl log::Log for InnerPublisher {
374    fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
375        // NOTE: we handle minimum severity directly through the log max_level. So we call,
376        // log::set_max_level, log::max_level where appropriate.
377        true
378    }
379
380    fn log(&self, record: &log::Record<'_>) {
381        self.sink.record_log(record);
382    }
383
384    fn flush(&self) {}
385}
386
387impl log::Log for Publisher {
388    #[inline]
389    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
390        self.inner.enabled(metadata)
391    }
392
393    #[inline]
394    fn log(&self, record: &log::Record<'_>) {
395        self.inner.log(record)
396    }
397
398    #[inline]
399    fn flush(&self) {
400        self.inner.flush()
401    }
402}
403
404impl Borrow<InterestFilter> for InnerPublisher {
405    fn borrow(&self) -> &InterestFilter {
406        &self.filter
407    }
408}
409
410/// Initializes logging, but buffers logs until the connection is established. This is required for
411/// things like Component Manager, which would otherwise deadlock when starting. This carries some
412/// overhead, so should be avoided unless required.
413pub fn initialize_buffered(opts: PublishOptions<'_>) -> Result<(), PublishError> {
414    BufferedPublisher::new(opts.publisher)?;
415    if opts.install_panic_hook {
416        crate::install_panic_hook(opts.panic_prefix);
417    }
418    Ok(())
419}
420
421/// A buffered publisher will buffer log messages until the IOBuffer is received. If this is
422/// registered as the global logger, then messages will be logged at the default level until an
423/// updated level is received from Archivist.
424pub struct BufferedPublisher {
425    sink: BufferedSink,
426    filter: InterestFilter,
427    interest_listening_task: Mutex<Option<fasync::Task<()>>>,
428}
429
430impl BufferedPublisher {
431    /// Returns a publisher that will buffer messages until the IOBuffer is received. An async
432    /// executor must be established.
433    pub fn new(opts: PublisherOptions<'_>) -> Result<Arc<Self>, PublishError> {
434        if opts.listen_for_interest_updates && !opts.register_global_logger {
435            // We can only support listening for interest updates if we are registering a global
436            // logger. This is because if we don't register, the initial interest is dropped.
437            return Err(PublishError::UnsupportedOption);
438        }
439
440        let get_client = || match opts.log_sink_client {
441            Some(log_sink) => Ok(log_sink),
442            None => {
443                #[cfg(not(feature = "no_startup_handle"))]
444                {
445                    let log_sink = fuchsia_runtime::take_startup_handle(HandleInfo::new(
446                        HandleType::LogSink,
447                        0,
448                    ));
449                    if let Some(log_sink) = log_sink {
450                        let log_sink = fidl::Channel::from(log_sink);
451                        return Ok(ClientEnd::<LogSinkMarker>::from(log_sink));
452                    }
453                }
454                // If startup handle is unavailable fallback to /svc.
455                connect_to_protocol()
456                    .map_err(|e| e.to_string())
457                    .map_err(PublishError::LogSinkConnect)
458            }
459        };
460        let client = (get_client)()?;
461
462        let this = Arc::new(Self {
463            sink: BufferedSink::new(SinkConfig {
464                tags: opts.tags.iter().map(|s| s.to_string()).collect(),
465                metatags: opts.metatags,
466                always_log_file_line: opts.always_log_file_line,
467            }),
468            filter: InterestFilter::new(opts.interest),
469            interest_listening_task: Mutex::default(),
470        });
471
472        if opts.register_global_logger {
473            // SAFETY: This leaks which guarantees the publisher remains alive for the lifetime
474            // of the program. This leaks even when there is an error (which shouldn't happen so
475            // we don't worry about it).
476            unsafe {
477                log::set_logger(&*Arc::into_raw(this.clone()))?;
478            }
479        }
480
481        // Whilst we are waiting for the OnInit event, we hold a strong reference to the publisher
482        // which will prevent the publisher from being dropped and ensure that buffered log messages
483        // are sent.
484        let this_clone = this.clone();
485        *this_clone.interest_listening_task.lock() = Some(fasync::Task::spawn(async move {
486            let proxy = client.into_proxy();
487
488            let Some(Ok(LogSinkEvent::OnInit {
489                payload: LogSinkOnInitRequest { buffer: Some(buffer), interest, .. },
490            })) = proxy.take_event_stream().next().await
491            else {
492                // There's not a lot we can do here: we haven't received the event we expected
493                // and there's no way we can log the issue.
494                return;
495            };
496
497            // Ignore the interest sent in the OnInit request if `listen_for_interest_updates`
498            // is false; it is assumed that the caller wants the interest specified in the
499            // options to stick.
500            this.filter.update_interest(
501                (if opts.listen_for_interest_updates { interest } else { None })
502                    .unwrap_or_default(),
503            );
504
505            this.sink.set_buffer(buffer);
506
507            if opts.listen_for_interest_updates {
508                this.filter.listen_for_interest_updates(proxy).await;
509            }
510        }));
511
512        Ok(this_clone)
513    }
514}
515
516impl log::Log for BufferedPublisher {
517    fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
518        // NOTE: we handle minimum severity directly through the log max_level. So we call,
519        // log::set_max_level, log::max_level where appropriate.
520        true
521    }
522
523    fn log(&self, record: &log::Record<'_>) {
524        self.sink.record_log(record);
525    }
526
527    fn flush(&self) {}
528}
529
530impl Borrow<InterestFilter> for BufferedPublisher {
531    fn borrow(&self) -> &InterestFilter {
532        &self.filter
533    }
534}
535
536/// Errors arising while forwarding a diagnostics stream to the environment.
537#[derive(Debug, Error)]
538pub enum PublishError {
539    /// Connection to fuchsia.logger.LogSink failed.
540    #[error("failed to connect to fuchsia.logger.LogSink ({0})")]
541    LogSinkConnect(String),
542
543    /// Installing a Logger.
544    #[error("failed to install the loger")]
545    InitLogForward(#[from] log::SetLoggerError),
546
547    /// Unsupported publish option.
548    #[error("unsupported option")]
549    UnsupportedOption,
550
551    /// The channel was closed with no OnInit event.
552    #[error("did not receive the OnInit event")]
553    MissingOnInit,
554}
555
556#[cfg(test)]
557static CURRENT_TIME_NANOS: AtomicI64 = AtomicI64::new(Duration::from_secs(10).as_nanos() as i64);
558
559/// Increments the test clock.
560#[cfg(test)]
561pub fn increment_clock(duration: Duration) {
562    CURRENT_TIME_NANOS.fetch_add(duration.as_nanos() as i64, Ordering::SeqCst);
563}
564
565/// Gets the current monotonic time in nanoseconds.
566#[doc(hidden)]
567pub fn get_now() -> i64 {
568    #[cfg(not(test))]
569    return zx::MonotonicInstant::get().into_nanos();
570
571    #[cfg(test)]
572    CURRENT_TIME_NANOS.load(Ordering::Relaxed)
573}
574
575/// Logs every N seconds using an Atomic variable
576/// to keep track of the time. This will have a higher
577/// performance impact on ARM compared to regular logging due to the use
578/// of an atomic.
579#[macro_export]
580macro_rules! log_every_n_seconds {
581    ($seconds:expr, $severity:expr, $($arg:tt)*) => {{
582        use std::{
583            sync::atomic::{AtomicI64, Ordering},
584            time::Duration,
585        };
586        use $crate::{get_now, paste};
587
588        let now = get_now();
589
590        static LAST_LOG_TIMESTAMP: AtomicI64 = AtomicI64::new(0);
591        if now - LAST_LOG_TIMESTAMP.load(Ordering::Acquire)
592            >= Duration::from_secs($seconds).as_nanos() as i64
593        {
594            paste! {
595                log::[< $severity:lower >]!($($arg)*);
596            }
597            LAST_LOG_TIMESTAMP.store(now, Ordering::Release);
598        }
599    }};
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605    use assert_matches::assert_matches;
606    use diagnostics_reader::ArchiveReader;
607    use fidl_fuchsia_diagnostics_crasher::{CrasherMarker, CrasherProxy};
608    use fuchsia_async::TimeoutExt;
609    use fuchsia_component_test::{Capability, ChildOptions, RealmBuilder, Ref, Route};
610    use futures::{FutureExt, StreamExt, future};
611    use log::{debug, error, info};
612    use moniker::ExtendedMoniker;
613
614    #[fuchsia::test]
615    async fn panic_integration_test() {
616        let builder = RealmBuilder::new().await.unwrap();
617        let puppet = builder
618            .add_child("rust-crasher", "#meta/crasher.cm", ChildOptions::new())
619            .await
620            .unwrap();
621        builder
622            .add_route(
623                Route::new()
624                    .capability(Capability::protocol::<CrasherMarker>())
625                    .from(&puppet)
626                    .to(Ref::parent()),
627            )
628            .await
629            .unwrap();
630        let realm = builder.build().await.unwrap();
631        let child_name = realm.root.child_name();
632        let reader = ArchiveReader::logs();
633        let (logs, _) = reader.snapshot_then_subscribe().unwrap().split_streams();
634        let proxy: CrasherProxy = realm.root.connect_to_protocol_at_exposed_dir().unwrap();
635        let target_moniker =
636            ExtendedMoniker::parse_str(&format!("realm_builder:{}/rust-crasher", child_name))
637                .unwrap();
638        proxy.crash("This is a test panic.").await.unwrap();
639
640        let result =
641            logs.filter(|data| future::ready(target_moniker == data.moniker)).next().await.unwrap();
642        assert_eq!(result.line_number(), Some(29).as_ref());
643        assert_eq!(
644            result.file_path(),
645            Some("src/lib/diagnostics/log/rust/rust-crasher/src/main.rs")
646        );
647        assert!(
648            result
649                .payload_keys()
650                .unwrap()
651                .get_property("info")
652                .unwrap()
653                .to_string()
654                .contains("This is a test panic.")
655        );
656    }
657
658    #[fuchsia::test(logging = false)]
659    async fn verify_setting_minimum_log_severity() {
660        let reader = ArchiveReader::logs();
661        let (logs, _) = reader.snapshot_then_subscribe().unwrap().split_streams();
662        let _publisher = Publisher::new_async(PublisherOptions {
663            tags: &["verify_setting_minimum_log_severity"],
664            register_global_logger: true,
665            ..PublisherOptions::default()
666        })
667        .await
668        .expect("initialized log");
669
670        info!("I'm an info log");
671        debug!("I'm a debug log and won't show up");
672
673        set_minimum_severity(Severity::Debug);
674        debug!("I'm a debug log and I show up");
675
676        let results = logs
677            .filter(|data| {
678                future::ready(
679                    data.tags().unwrap().iter().any(|t| t == "verify_setting_minimum_log_severity"),
680                )
681            })
682            .take(2)
683            .collect::<Vec<_>>()
684            .await;
685        assert_eq!(results[0].msg().unwrap(), "I'm an info log");
686        assert_eq!(results[1].msg().unwrap(), "I'm a debug log and I show up");
687    }
688
689    #[fuchsia::test]
690    async fn log_macro_logs_are_recorded() {
691        let reader = ArchiveReader::logs();
692        let (logs, mut errors) = reader.snapshot_then_subscribe().unwrap().split_streams();
693
694        let total_threads = 10;
695
696        for i in 0..total_threads {
697            std::thread::spawn(move || {
698                log::info!(thread=i; "log from thread {}", i);
699            });
700        }
701
702        let mut filtered = 0;
703        let mut results = logs
704            .filter(|data| {
705                future::ready(
706                    if data.tags().unwrap().iter().any(|t| t == "log_macro_logs_are_recorded") {
707                        true
708                    } else {
709                        filtered += 1;
710                        false
711                    },
712                )
713            })
714            .take(total_threads);
715
716        let mut seen = vec![];
717        while let Some(log) = results
718            .next()
719            .on_timeout(Duration::from_secs(30), || {
720                error!("Timed out!");
721                None
722            })
723            .await
724        {
725            let hierarchy = log.payload_keys().unwrap();
726            assert_eq!(hierarchy.properties.len(), 1);
727            assert_eq!(hierarchy.properties[0].name(), "thread");
728            let thread_id = hierarchy.properties[0].uint().unwrap();
729            seen.push(thread_id as usize);
730            assert_eq!(log.msg().unwrap(), format!("log from thread {thread_id}"));
731        }
732
733        assert_matches!(errors.next().now_or_never(), None);
734
735        seen.sort();
736        assert_eq!(seen, (0..total_threads).collect::<Vec<_>>(), "filtered={filtered}");
737    }
738}