1use std::sync::OnceLock;
2#[cfg(feature = "sentry")]
3use std::sync::{atomic::AtomicBool, Arc};
4
5#[cfg(feature = "sentry")]
6use tracing::warn;
7use tracing_appender::rolling::{RollingFileAppender, Rotation};
8use tracing_core::Subscriber;
9use tracing_subscriber::{
10 field::RecordFields,
11 fmt::{
12 self,
13 format::{DefaultFields, Writer},
14 time::FormatTime,
15 FormatEvent, FormatFields, FormattedFields,
16 },
17 layer::{Layered, SubscriberExt as _},
18 registry::LookupSpan,
19 reload::{self, Handle},
20 util::SubscriberInitExt as _,
21 EnvFilter, Layer, Registry,
22};
23
24use crate::{error::ClientError, tracing::LogLevel};
25
26struct EventFormatter {
28 display_timestamp: bool,
29 display_level: bool,
30}
31
32impl EventFormatter {
33 fn new() -> Self {
34 Self { display_timestamp: true, display_level: true }
35 }
36
37 #[cfg(target_os = "android")]
38 fn for_logcat() -> Self {
39 Self { display_timestamp: false, display_level: false }
41 }
42
43 fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
44 if fmt::time::SystemTime.format_time(writer).is_err() {
45 writer.write_str("<unknown time>")?;
46 }
47 Ok(())
48 }
49
50 fn write_filename(
51 &self,
52 writer: &mut fmt::format::Writer<'_>,
53 filename: &str,
54 ) -> std::fmt::Result {
55 const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
56 let crates_io_filename = filename
57 .split_once(CRATES_IO_PATH_MATCHER)
58 .and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
59
60 if let Some(filename) = crates_io_filename {
61 writer.write_str("<crates.io>/")?;
62 writer.write_str(filename)
63 } else {
64 writer.write_str(filename)
65 }
66 }
67}
68
69impl<S, N> FormatEvent<S, N> for EventFormatter
70where
71 S: Subscriber + for<'a> LookupSpan<'a>,
72 N: for<'a> FormatFields<'a> + 'static,
73{
74 fn format_event(
75 &self,
76 ctx: &fmt::FmtContext<'_, S, N>,
77 mut writer: fmt::format::Writer<'_>,
78 event: &tracing_core::Event<'_>,
79 ) -> std::fmt::Result {
80 let meta = event.metadata();
81
82 if self.display_timestamp {
83 self.format_timestamp(&mut writer)?;
84 writer.write_char(' ')?;
85 }
86
87 if self.display_level {
88 write!(writer, "{:>5} ", meta.level())?;
90 }
91
92 write!(writer, "{}: ", meta.target())?;
93
94 ctx.format_fields(writer.by_ref(), event)?;
95
96 if let Some(filename) = meta.file() {
97 writer.write_str(" | ")?;
98 self.write_filename(&mut writer, filename)?;
99 if let Some(line_number) = meta.line() {
100 write!(writer, ":{line_number}")?;
101 }
102 }
103
104 if let Some(scope) = ctx.event_scope() {
105 writer.write_str(" | spans: ")?;
106
107 let mut first = true;
108
109 for span in scope.from_root() {
110 if !first {
111 writer.write_str(" > ")?;
112 }
113
114 first = false;
115
116 write!(writer, "{}", span.name())?;
117
118 if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
119 if !fields.is_empty() {
120 write!(writer, "{{{fields}}}")?;
121 }
122 }
123 }
124 }
125
126 writeln!(writer)
127 }
128}
129
130#[derive(Default)]
135struct FieldsFormatterForFiles(DefaultFields);
136
137impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
138 fn format_fields<R: RecordFields>(
139 &self,
140 writer: Writer<'writer>,
141 fields: R,
142 ) -> std::fmt::Result {
143 self.0.format_fields(writer, fields)
144 }
145}
146
147type ReloadHandle = Handle<
148 tracing_subscriber::fmt::Layer<
149 Layered<EnvFilter, Registry>,
150 FieldsFormatterForFiles,
151 EventFormatter,
152 RollingFileAppender,
153 >,
154 Layered<EnvFilter, Registry>,
155>;
156
157fn text_layers(
158 config: TracingConfiguration,
159) -> (impl Layer<Layered<EnvFilter, Registry>>, Option<ReloadHandle>) {
160 let (file_layer, reload_handle) = config
161 .write_to_files
162 .map(|c| {
163 let layer = make_file_layer(c);
164 reload::Layer::new(layer)
165 })
166 .unzip();
167
168 let layers = Layer::and_then(
169 file_layer,
170 config.write_to_stdout_or_system.then(|| {
171 #[derive(Default)]
176 struct FieldsFormatterFormStdoutOrSystem(DefaultFields);
177
178 impl<'writer> FormatFields<'writer> for FieldsFormatterFormStdoutOrSystem {
179 fn format_fields<R: RecordFields>(
180 &self,
181 writer: Writer<'writer>,
182 fields: R,
183 ) -> std::fmt::Result {
184 self.0.format_fields(writer, fields)
185 }
186 }
187
188 #[cfg(not(target_os = "android"))]
189 return fmt::layer()
190 .fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
191 .event_format(EventFormatter::new())
192 .with_ansi(false)
194 .with_writer(std::io::stderr);
195
196 #[cfg(target_os = "android")]
197 return fmt::layer()
198 .fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
199 .event_format(EventFormatter::for_logcat())
200 .with_ansi(false)
202 .with_writer(paranoid_android::AndroidLogMakeWriter::new(
203 "org.matrix.rust.sdk".to_owned(),
204 ));
205 }),
206 );
207
208 (layers, reload_handle)
209}
210
211fn make_file_layer(
212 file_configuration: TracingFileConfiguration,
213) -> tracing_subscriber::fmt::Layer<
214 Layered<EnvFilter, Registry, Registry>,
215 FieldsFormatterForFiles,
216 EventFormatter,
217 RollingFileAppender,
218> {
219 let mut builder = RollingFileAppender::builder()
220 .rotation(Rotation::HOURLY)
221 .filename_prefix(&file_configuration.file_prefix);
222
223 if let Some(max_files) = file_configuration.max_files {
224 builder = builder.max_log_files(max_files as usize)
225 }
226 if let Some(file_suffix) = file_configuration.file_suffix {
227 builder = builder.filename_suffix(file_suffix)
228 }
229
230 let writer =
231 builder.build(&file_configuration.path).expect("Failed to create a rolling file appender.");
232
233 fmt::layer()
234 .fmt_fields(FieldsFormatterForFiles::default())
235 .event_format(EventFormatter::new())
236 .with_ansi(false)
240 .with_writer(writer)
241}
242
243#[derive(uniffi::Record)]
245pub struct TracingFileConfiguration {
246 path: String,
248
249 file_prefix: String,
251
252 file_suffix: Option<String>,
254
255 max_files: Option<u64>,
260}
261
262#[derive(PartialEq, PartialOrd)]
263enum LogTarget {
264 Hyper,
266
267 MatrixSdkFfi,
269
270 MatrixSdkBaseEventCache,
272 MatrixSdkBaseSlidingSync,
273 MatrixSdkBaseStoreAmbiguityMap,
274
275 MatrixSdkCommonStoreLocks,
277
278 MatrixSdk,
280 MatrixSdkClient,
281 MatrixSdkCrypto,
282 MatrixSdkCryptoAccount,
283 MatrixSdkEventCache,
284 MatrixSdkEventCacheStore,
285 MatrixSdkHttpClient,
286 MatrixSdkOidc,
287 MatrixSdkSendQueue,
288 MatrixSdkSlidingSync,
289
290 MatrixSdkUiTimeline,
292 MatrixSdkUiNotificationClient,
293}
294
295impl LogTarget {
296 fn as_str(&self) -> &'static str {
297 match self {
298 LogTarget::Hyper => "hyper",
299 LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
300 LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
301 LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
302 LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
303 LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
304 LogTarget::MatrixSdk => "matrix_sdk",
305 LogTarget::MatrixSdkClient => "matrix_sdk::client",
306 LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
307 LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account",
308 LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
309 LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
310 LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
311 LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
312 LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
313 LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
314 LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
315 LogTarget::MatrixSdkUiNotificationClient => "matrix_sdk_ui::notification_client",
316 }
317 }
318}
319
320const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
321 (LogTarget::Hyper, LogLevel::Warn),
322 (LogTarget::MatrixSdkFfi, LogLevel::Info),
323 (LogTarget::MatrixSdk, LogLevel::Info),
324 (LogTarget::MatrixSdkClient, LogLevel::Trace),
325 (LogTarget::MatrixSdkCrypto, LogLevel::Debug),
326 (LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace),
327 (LogTarget::MatrixSdkOidc, LogLevel::Trace),
328 (LogTarget::MatrixSdkHttpClient, LogLevel::Debug),
329 (LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
330 (LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
331 (LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
332 (LogTarget::MatrixSdkSendQueue, LogLevel::Info),
333 (LogTarget::MatrixSdkEventCache, LogLevel::Info),
334 (LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
335 (LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
336 (LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
337 (LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
338 (LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
339];
340
341const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
342 LogTarget::Hyper, LogTarget::MatrixSdk, LogTarget::MatrixSdkFfi, LogTarget::MatrixSdkCommonStoreLocks, LogTarget::MatrixSdkBaseStoreAmbiguityMap, ];
348
349#[derive(uniffi::Enum)]
352pub enum TraceLogPacks {
353 EventCache,
355 SendQueue,
357 Timeline,
359 NotificationClient,
361}
362
363impl TraceLogPacks {
364 fn targets(&self) -> &[LogTarget] {
367 match self {
368 TraceLogPacks::EventCache => &[
369 LogTarget::MatrixSdkEventCache,
370 LogTarget::MatrixSdkBaseEventCache,
371 LogTarget::MatrixSdkEventCacheStore,
372 ],
373 TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
374 TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
375 TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
376 }
377 }
378}
379
380#[cfg(feature = "sentry")]
381struct SentryLoggingCtx {
382 _guard: sentry::ClientInitGuard,
384
385 enabled: Arc<AtomicBool>,
387}
388
389struct LoggingCtx {
390 reload_handle: Option<ReloadHandle>,
391 #[cfg(feature = "sentry")]
392 sentry: Option<SentryLoggingCtx>,
393}
394
395static LOGGING: OnceLock<LoggingCtx> = OnceLock::new();
396
397#[derive(uniffi::Record)]
398pub struct TracingConfiguration {
399 log_level: LogLevel,
401
402 trace_log_packs: Vec<TraceLogPacks>,
404
405 extra_targets: Vec<String>,
411
412 write_to_stdout_or_system: bool,
414
415 write_to_files: Option<TracingFileConfiguration>,
417
418 #[cfg(feature = "sentry")]
420 sentry_dsn: Option<String>,
421}
422
423impl TracingConfiguration {
424 #[cfg_attr(not(feature = "sentry"), allow(unused_mut))]
427 fn build(mut self) -> LoggingCtx {
428 std::env::set_var("RUST_BACKTRACE", "1");
430
431 log_panics::init();
433
434 let env_filter = build_tracing_filter(&self);
435
436 let logging_ctx;
437 #[cfg(feature = "sentry")]
438 {
439 let (sentry_layer, sentry_logging_ctx) =
441 if let Some(sentry_dsn) = self.sentry_dsn.take() {
442 let sentry_guard = sentry::init((
444 sentry_dsn,
445 sentry::ClientOptions {
446 traces_sample_rate: 0.0,
447 attach_stacktrace: true,
448 release: Some(env!("VERGEN_GIT_SHA").into()),
449 ..sentry::ClientOptions::default()
450 },
451 ));
452
453 let sentry_enabled = Arc::new(AtomicBool::new(true));
454
455 let sentry_layer = sentry_tracing::layer()
461 .event_filter({
462 let enabled = sentry_enabled.clone();
463
464 move |metadata| {
465 if enabled.load(std::sync::atomic::Ordering::SeqCst)
466 && metadata.fields().field("sentry").is_some()
467 {
468 sentry_tracing::default_event_filter(metadata)
469 } else {
470 sentry_tracing::EventFilter::Ignore
472 }
473 }
474 })
475 .span_filter({
476 let enabled = sentry_enabled.clone();
477
478 move |metadata| {
479 if enabled.load(std::sync::atomic::Ordering::SeqCst) {
480 sentry_tracing::default_span_filter(metadata)
481 } else {
482 false
484 }
485 }
486 });
487
488 (
489 Some(sentry_layer),
490 Some(SentryLoggingCtx { _guard: sentry_guard, enabled: sentry_enabled }),
491 )
492 } else {
493 (None, None)
494 };
495 let (text_layers, reload_handle) = crate::platform::text_layers(self);
496
497 tracing_subscriber::registry()
498 .with(tracing_subscriber::EnvFilter::new(&env_filter))
499 .with(text_layers)
500 .with(sentry_layer)
501 .init();
502 logging_ctx = LoggingCtx { reload_handle, sentry: sentry_logging_ctx };
503 }
504 #[cfg(not(feature = "sentry"))]
505 {
506 let (text_layers, reload_handle) = crate::platform::text_layers(self);
507 tracing_subscriber::registry()
508 .with(tracing_subscriber::EnvFilter::new(&env_filter))
509 .with(text_layers)
510 .init();
511 logging_ctx = LoggingCtx { reload_handle };
512 }
513
514 tracing::info!(env_filter, "Logging has been set up");
516
517 logging_ctx
518 }
519}
520
521fn build_tracing_filter(config: &TracingConfiguration) -> String {
522 let mut filters = vec!["panic=error".to_owned()];
527
528 let global_level = config.log_level;
529
530 DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, default_level)| {
531 let level = if IMMUTABLE_LOG_TARGETS.contains(target) {
532 *default_level
534 } else if config.trace_log_packs.iter().any(|pack| pack.targets().contains(target)) {
535 LogLevel::Trace
537 } else if *default_level > global_level {
538 *default_level
540 } else {
541 global_level
543 };
544
545 filters.push(format!("{}={}", target.as_str(), level.as_str()));
546 });
547
548 for target in &config.extra_targets {
550 filters.push(format!("{}={}", target, config.log_level.as_str()));
551 }
552
553 filters.join(",")
554}
555
556#[matrix_sdk_ffi_macros::export]
563pub fn init_platform(
564 config: TracingConfiguration,
565 use_lightweight_tokio_runtime: bool,
566) -> Result<(), ClientError> {
567 #[cfg(all(feature = "js", target_family = "wasm"))]
568 {
569 console_error_panic_hook::set_once();
570 }
571 #[cfg(not(target_family = "wasm"))]
572 {
573 LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
574 msg: "logger already initialized".to_owned(),
575 details: None,
576 })?;
577
578 if use_lightweight_tokio_runtime {
579 setup_lightweight_tokio_runtime();
580 } else {
581 setup_multithreaded_tokio_runtime();
582 }
583 }
584
585 Ok(())
586}
587
588#[matrix_sdk_ffi_macros::export]
591#[cfg(feature = "sentry")]
592pub fn enable_sentry_logging(enabled: bool) {
593 if let Some(ctx) = LOGGING.get() {
594 if let Some(sentry_ctx) = &ctx.sentry {
595 sentry_ctx.enabled.store(enabled, std::sync::atomic::Ordering::SeqCst);
596 } else {
597 warn!("Sentry logging is not enabled");
598 }
599 } else {
600 eprintln!("Logging hasn't been enabled yet");
602 };
603}
604
605#[matrix_sdk_ffi_macros::export]
611pub fn reload_tracing_file_writer(
612 configuration: TracingFileConfiguration,
613) -> Result<(), ClientError> {
614 let Some(logging_context) = LOGGING.get() else {
615 return Err(ClientError::Generic {
616 msg: "Logging hasn't been initialized yet".to_owned(),
617 details: None,
618 });
619 };
620
621 let Some(reload_handle) = logging_context.reload_handle.as_ref() else {
622 return Err(ClientError::Generic {
623 msg: "Logging wasn't initialized with a file config".to_owned(),
624 details: None,
625 });
626 };
627
628 let layer = make_file_layer(configuration);
629 reload_handle.reload(layer).map_err(|error| ClientError::Generic {
630 msg: format!("Failed to reload file config: {error}"),
631 details: None,
632 })
633}
634
635#[cfg(not(target_family = "wasm"))]
636fn setup_multithreaded_tokio_runtime() {
637 async_compat::set_runtime_builder(Box::new(|| {
638 eprintln!("spawning a multithreaded tokio runtime");
639
640 let mut builder = tokio::runtime::Builder::new_multi_thread();
641 builder.enable_all();
642 builder
643 }));
644}
645
646#[cfg(not(target_family = "wasm"))]
647fn setup_lightweight_tokio_runtime() {
648 async_compat::set_runtime_builder(Box::new(|| {
649 eprintln!("spawning a lightweight tokio runtime");
650
651 let num_available_cores =
653 std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
654
655 let num_worker_threads = num_available_cores.min(4);
657
658 let num_blocking_threads = 2;
660
661 let max_memory_bytes = 1024 * 1024;
663
664 let mut builder = tokio::runtime::Builder::new_multi_thread();
665
666 builder
667 .enable_all()
668 .worker_threads(num_worker_threads)
669 .thread_stack_size(max_memory_bytes)
670 .max_blocking_threads(num_blocking_threads);
671
672 builder
673 }));
674}
675
676#[cfg(test)]
677mod tests {
678 use super::build_tracing_filter;
679 use crate::platform::TraceLogPacks;
680
681 #[test]
682 fn test_default_tracing_filter() {
683 let config = super::TracingConfiguration {
684 log_level: super::LogLevel::Error,
685 trace_log_packs: Vec::new(),
686 extra_targets: vec!["super_duper_app".to_owned()],
687 write_to_stdout_or_system: true,
688 write_to_files: None,
689 #[cfg(feature = "sentry")]
690 sentry_dsn: None,
691 };
692
693 let filter = build_tracing_filter(&config);
694
695 assert_eq!(
696 filter,
697 r#"panic=error,
698 hyper=warn,
699 matrix_sdk_ffi=info,
700 matrix_sdk=info,
701 matrix_sdk::client=trace,
702 matrix_sdk_crypto=debug,
703 matrix_sdk_crypto::olm::account=trace,
704 matrix_sdk::oidc=trace,
705 matrix_sdk::http_client=debug,
706 matrix_sdk::sliding_sync=info,
707 matrix_sdk_base::sliding_sync=info,
708 matrix_sdk_ui::timeline=info,
709 matrix_sdk::send_queue=info,
710 matrix_sdk::event_cache=info,
711 matrix_sdk_base::event_cache=info,
712 matrix_sdk_sqlite::event_cache_store=info,
713 matrix_sdk_common::store_locks=warn,
714 matrix_sdk_base::store::ambiguity_map=warn,
715 matrix_sdk_ui::notification_client=info,
716 super_duper_app=error"#
717 .split('\n')
718 .map(|s| s.trim())
719 .collect::<Vec<_>>()
720 .join("")
721 );
722 }
723
724 #[test]
725 fn test_trace_tracing_filter() {
726 let config = super::TracingConfiguration {
727 log_level: super::LogLevel::Trace,
728 trace_log_packs: Vec::new(),
729 extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
730 write_to_stdout_or_system: true,
731 write_to_files: None,
732 #[cfg(feature = "sentry")]
733 sentry_dsn: None,
734 };
735
736 let filter = build_tracing_filter(&config);
737
738 assert_eq!(
739 filter,
740 r#"panic=error,
741 hyper=warn,
742 matrix_sdk_ffi=info,
743 matrix_sdk=info,
744 matrix_sdk::client=trace,
745 matrix_sdk_crypto=trace,
746 matrix_sdk_crypto::olm::account=trace,
747 matrix_sdk::oidc=trace,
748 matrix_sdk::http_client=trace,
749 matrix_sdk::sliding_sync=trace,
750 matrix_sdk_base::sliding_sync=trace,
751 matrix_sdk_ui::timeline=trace,
752 matrix_sdk::send_queue=trace,
753 matrix_sdk::event_cache=trace,
754 matrix_sdk_base::event_cache=trace,
755 matrix_sdk_sqlite::event_cache_store=trace,
756 matrix_sdk_common::store_locks=warn,
757 matrix_sdk_base::store::ambiguity_map=warn,
758 matrix_sdk_ui::notification_client=trace,
759 super_duper_app=trace,
760 some_other_span=trace"#
761 .split('\n')
762 .map(|s| s.trim())
763 .collect::<Vec<_>>()
764 .join("")
765 );
766 }
767
768 #[test]
769 fn test_trace_log_packs() {
770 let config = super::TracingConfiguration {
771 log_level: super::LogLevel::Info,
772 trace_log_packs: vec![TraceLogPacks::EventCache, TraceLogPacks::SendQueue],
773 extra_targets: vec!["super_duper_app".to_owned()],
774 write_to_stdout_or_system: true,
775 write_to_files: None,
776 #[cfg(feature = "sentry")]
777 sentry_dsn: None,
778 };
779
780 let filter = build_tracing_filter(&config);
781
782 assert_eq!(
783 filter,
784 r#"panic=error,
785 hyper=warn,
786 matrix_sdk_ffi=info,
787 matrix_sdk=info,
788 matrix_sdk::client=trace,
789 matrix_sdk_crypto=debug,
790 matrix_sdk_crypto::olm::account=trace,
791 matrix_sdk::oidc=trace,
792 matrix_sdk::http_client=debug,
793 matrix_sdk::sliding_sync=info,
794 matrix_sdk_base::sliding_sync=info,
795 matrix_sdk_ui::timeline=info,
796 matrix_sdk::send_queue=trace,
797 matrix_sdk::event_cache=trace,
798 matrix_sdk_base::event_cache=trace,
799 matrix_sdk_sqlite::event_cache_store=trace,
800 matrix_sdk_common::store_locks=warn,
801 matrix_sdk_base::store::ambiguity_map=warn,
802 matrix_sdk_ui::notification_client=info,
803 super_duper_app=info"#
804 .split('\n')
805 .map(|s| s.trim())
806 .collect::<Vec<_>>()
807 .join("")
808 );
809 }
810}