1use 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
39pub trait OnInterestChanged {
41 fn on_changed(&self, severity: Severity);
43}
44
45#[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 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 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 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 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 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
123pub 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 return Ok(());
137 }
138 result?;
139 if opts.install_panic_hook {
140 crate::install_panic_hook(opts.panic_prefix);
141 }
142 Ok(())
143}
144
145pub fn set_minimum_severity(severity: impl Into<Severity>) {
148 let severity: Severity = severity.into();
149 log::set_max_level(severity.into());
150}
151
152pub fn initialize_sync(opts: PublishOptions<'_>) {
162 match Publisher::new_sync(opts.publisher) {
163 Ok(_) => {}
164 Err(PublishError::MissingOnInit) => {
165 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#[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 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 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 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 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 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 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 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 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 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 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 pub fn register_logger(&self, interest: Option<Interest>) -> Result<(), PublishError> {
354 self.inner.filter.update_interest(interest.unwrap_or_default());
355 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 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 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
410pub 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
421pub struct BufferedPublisher {
425 sink: BufferedSink,
426 filter: InterestFilter,
427 interest_listening_task: Mutex<Option<fasync::Task<()>>>,
428}
429
430impl BufferedPublisher {
431 pub fn new(opts: PublisherOptions<'_>) -> Result<Arc<Self>, PublishError> {
434 if opts.listen_for_interest_updates && !opts.register_global_logger {
435 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 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 unsafe {
477 log::set_logger(&*Arc::into_raw(this.clone()))?;
478 }
479 }
480
481 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 return;
495 };
496
497 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 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#[derive(Debug, Error)]
538pub enum PublishError {
539 #[error("failed to connect to fuchsia.logger.LogSink ({0})")]
541 LogSinkConnect(String),
542
543 #[error("failed to install the loger")]
545 InitLogForward(#[from] log::SetLoggerError),
546
547 #[error("unsupported option")]
549 UnsupportedOption,
550
551 #[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#[cfg(test)]
561pub fn increment_clock(duration: Duration) {
562 CURRENT_TIME_NANOS.fetch_add(duration.as_nanos() as i64, Ordering::SeqCst);
563}
564
565#[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#[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}