matrix_sdk_ffi/
platform.rs

1use tracing_appender::rolling::{RollingFileAppender, Rotation};
2use tracing_core::Subscriber;
3use tracing_subscriber::{
4    field::RecordFields,
5    fmt::{
6        self,
7        format::{DefaultFields, Writer},
8        time::FormatTime,
9        FormatEvent, FormatFields, FormattedFields,
10    },
11    layer::SubscriberExt,
12    registry::LookupSpan,
13    util::SubscriberInitExt,
14    EnvFilter, Layer,
15};
16
17use crate::tracing::LogLevel;
18
19pub fn log_panics() {
20    std::env::set_var("RUST_BACKTRACE", "1");
21
22    log_panics::init();
23}
24
25fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
26where
27    S: Subscriber + for<'a> LookupSpan<'a>,
28{
29    // Adjusted version of tracing_subscriber::fmt::Format
30    struct EventFormatter {
31        display_timestamp: bool,
32        display_level: bool,
33    }
34
35    impl EventFormatter {
36        fn new() -> Self {
37            Self { display_timestamp: true, display_level: true }
38        }
39
40        #[cfg(target_os = "android")]
41        fn for_logcat() -> Self {
42            // Level and time are already captured by logcat separately
43            Self { display_timestamp: false, display_level: false }
44        }
45
46        fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
47            if fmt::time::SystemTime.format_time(writer).is_err() {
48                writer.write_str("<unknown time>")?;
49            }
50            Ok(())
51        }
52
53        fn write_filename(
54            &self,
55            writer: &mut fmt::format::Writer<'_>,
56            filename: &str,
57        ) -> std::fmt::Result {
58            const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
59            let crates_io_filename = filename
60                .split_once(CRATES_IO_PATH_MATCHER)
61                .and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
62
63            if let Some(filename) = crates_io_filename {
64                writer.write_str("<crates.io>/")?;
65                writer.write_str(filename)
66            } else {
67                writer.write_str(filename)
68            }
69        }
70    }
71
72    impl<S, N> FormatEvent<S, N> for EventFormatter
73    where
74        S: Subscriber + for<'a> LookupSpan<'a>,
75        N: for<'a> FormatFields<'a> + 'static,
76    {
77        fn format_event(
78            &self,
79            ctx: &fmt::FmtContext<'_, S, N>,
80            mut writer: fmt::format::Writer<'_>,
81            event: &tracing_core::Event<'_>,
82        ) -> std::fmt::Result {
83            let meta = event.metadata();
84
85            if self.display_timestamp {
86                self.format_timestamp(&mut writer)?;
87                writer.write_char(' ')?;
88            }
89
90            if self.display_level {
91                // For info and warn, add a padding space to the left
92                write!(writer, "{:>5} ", meta.level())?;
93            }
94
95            write!(writer, "{}: ", meta.target())?;
96
97            ctx.format_fields(writer.by_ref(), event)?;
98
99            if let Some(filename) = meta.file() {
100                writer.write_str(" | ")?;
101                self.write_filename(&mut writer, filename)?;
102                if let Some(line_number) = meta.line() {
103                    write!(writer, ":{line_number}")?;
104                }
105            }
106
107            if let Some(scope) = ctx.event_scope() {
108                writer.write_str(" | spans: ")?;
109
110                let mut first = true;
111
112                for span in scope.from_root() {
113                    if !first {
114                        writer.write_str(" > ")?;
115                    }
116
117                    first = false;
118
119                    write!(writer, "{}", span.name())?;
120
121                    if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
122                        if !fields.is_empty() {
123                            write!(writer, "{{{fields}}}")?;
124                        }
125                    }
126                }
127            }
128
129            writeln!(writer)
130        }
131    }
132
133    let file_layer = config.write_to_files.map(|c| {
134        let mut builder = RollingFileAppender::builder()
135            .rotation(Rotation::HOURLY)
136            .filename_prefix(&c.file_prefix);
137
138        if let Some(max_files) = c.max_files {
139            builder = builder.max_log_files(max_files as usize)
140        };
141        if let Some(file_suffix) = c.file_suffix {
142            builder = builder.filename_suffix(file_suffix)
143        }
144
145        let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
146
147        // Another fields formatter is necessary because of this bug
148        // https://github.com/tokio-rs/tracing/issues/1372. Using a new
149        // formatter for the fields forces to record them in different span
150        // extensions, and thus remove the duplicated fields in the span.
151        #[derive(Default)]
152        struct FieldsFormatterForFiles(DefaultFields);
153
154        impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
155            fn format_fields<R: RecordFields>(
156                &self,
157                writer: Writer<'writer>,
158                fields: R,
159            ) -> std::fmt::Result {
160                self.0.format_fields(writer, fields)
161            }
162        }
163
164        fmt::layer()
165            .fmt_fields(FieldsFormatterForFiles::default())
166            .event_format(EventFormatter::new())
167            // EventFormatter doesn't support ANSI colors anyways, but the
168            // default field formatter does, which is unhelpful for iOS +
169            // Android logs, but enabled by default.
170            .with_ansi(false)
171            .with_writer(writer)
172    });
173
174    Layer::and_then(
175        file_layer,
176        config.write_to_stdout_or_system.then(|| {
177            // Another fields formatter is necessary because of this bug
178            // https://github.com/tokio-rs/tracing/issues/1372. Using a new
179            // formatter for the fields forces to record them in different span
180            // extensions, and thus remove the duplicated fields in the span.
181            #[derive(Default)]
182            struct FieldsFormatterFormStdoutOrSystem(DefaultFields);
183
184            impl<'writer> FormatFields<'writer> for FieldsFormatterFormStdoutOrSystem {
185                fn format_fields<R: RecordFields>(
186                    &self,
187                    writer: Writer<'writer>,
188                    fields: R,
189                ) -> std::fmt::Result {
190                    self.0.format_fields(writer, fields)
191                }
192            }
193
194            #[cfg(not(target_os = "android"))]
195            return fmt::layer()
196                .fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
197                .event_format(EventFormatter::new())
198                // See comment above.
199                .with_ansi(false)
200                .with_writer(std::io::stderr);
201
202            #[cfg(target_os = "android")]
203            return fmt::layer()
204                .fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
205                .event_format(EventFormatter::for_logcat())
206                // See comment above.
207                .with_ansi(false)
208                .with_writer(paranoid_android::AndroidLogMakeWriter::new(
209                    "org.matrix.rust.sdk".to_owned(),
210                ));
211        }),
212    )
213}
214
215/// Configuration to save logs to (rotated) log-files.
216#[derive(uniffi::Record)]
217pub struct TracingFileConfiguration {
218    /// Base location for all the log files.
219    path: String,
220
221    /// Prefix for the log files' names.
222    file_prefix: String,
223
224    /// Optional suffix for the log file's names.
225    file_suffix: Option<String>,
226
227    /// Maximum number of rotated files.
228    ///
229    /// If not set, there's no max limit, i.e. the number of log files is
230    /// unlimited.
231    max_files: Option<u64>,
232}
233
234#[derive(PartialEq, PartialOrd)]
235enum LogTarget {
236    Hyper,
237    MatrixSdkFfi,
238    MatrixSdk,
239    MatrixSdkClient,
240    MatrixSdkCrypto,
241    MatrixSdkCryptoAccount,
242    MatrixSdkOidc,
243    MatrixSdkHttpClient,
244    MatrixSdkSlidingSync,
245    MatrixSdkBaseSlidingSync,
246    MatrixSdkUiTimeline,
247    MatrixSdkEventCache,
248    MatrixSdkBaseEventCache,
249    MatrixSdkEventCacheStore,
250
251    MatrixSdkCommonStoreLocks,
252    MatrixSdkBaseStoreAmbiguityMap,
253}
254
255impl LogTarget {
256    fn as_str(&self) -> &'static str {
257        match self {
258            LogTarget::Hyper => "hyper",
259            LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
260            LogTarget::MatrixSdk => "matrix_sdk",
261            LogTarget::MatrixSdkClient => "matrix_sdk::client",
262            LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
263            LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account",
264            LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
265            LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
266            LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
267            LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
268            LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
269            LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
270            LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
271            LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
272
273            LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
274            LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
275        }
276    }
277}
278
279const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
280    (LogTarget::Hyper, LogLevel::Warn),
281    (LogTarget::MatrixSdkFfi, LogLevel::Info),
282    (LogTarget::MatrixSdk, LogLevel::Info),
283    (LogTarget::MatrixSdkClient, LogLevel::Trace),
284    (LogTarget::MatrixSdkCrypto, LogLevel::Debug),
285    (LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace),
286    (LogTarget::MatrixSdkOidc, LogLevel::Trace),
287    (LogTarget::MatrixSdkHttpClient, LogLevel::Debug),
288    (LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
289    (LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
290    (LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
291    (LogTarget::MatrixSdkEventCache, LogLevel::Info),
292    (LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
293    (LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
294    (LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
295    (LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
296];
297
298const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[
299    LogTarget::Hyper,                          // Too verbose
300    LogTarget::MatrixSdk,                      // Too generic
301    LogTarget::MatrixSdkFfi,                   // Too verbose
302    LogTarget::MatrixSdkCommonStoreLocks,      // Too verbose
303    LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
304];
305
306#[derive(uniffi::Record)]
307pub struct TracingConfiguration {
308    /// The desired log level
309    log_level: LogLevel,
310
311    /// Additional targets that the FFI client would like to use e.g.
312    /// the target names for created [`crate::tracing::Span`]
313    extra_targets: Option<Vec<String>>,
314
315    /// Whether to log to stdout, or in the logcat on Android.
316    write_to_stdout_or_system: bool,
317
318    /// If set, configures rotated log files where to write additional logs.
319    write_to_files: Option<TracingFileConfiguration>,
320}
321
322fn build_tracing_filter(config: &TracingConfiguration) -> String {
323    // We are intentionally not setting a global log level because we don't want to
324    // risk third party crates logging sensitive information.
325    // As such we need to make sure that panics will be properly logged.
326    // On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
327    let mut filters = vec!["panic=error".to_owned()];
328
329    DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| {
330        // Use the default if the log level shouldn't be changed for this target or
331        // if it's already logging more than requested
332        let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level {
333            level.as_str()
334        } else {
335            config.log_level.as_str()
336        };
337
338        filters.push(format!("{}={}", target.as_str(), level));
339    });
340
341    // Finally append the extra targets requested by the client
342    if let Some(extra_targets) = &config.extra_targets {
343        for target in extra_targets {
344            filters.push(format!("{}={}", target, config.log_level.as_str()));
345        }
346    }
347
348    filters.join(",")
349}
350
351#[matrix_sdk_ffi_macros::export]
352pub fn setup_tracing(config: TracingConfiguration) {
353    log_panics();
354
355    tracing_subscriber::registry()
356        .with(EnvFilter::new(build_tracing_filter(&config)))
357        .with(text_layers(config))
358        .init();
359}
360
361#[cfg(test)]
362mod tests {
363    use super::build_tracing_filter;
364
365    #[test]
366    fn test_default_tracing_filter() {
367        let config = super::TracingConfiguration {
368            log_level: super::LogLevel::Error,
369            extra_targets: Some(vec!["super_duper_app".to_owned()]),
370            write_to_stdout_or_system: true,
371            write_to_files: None,
372        };
373
374        let filter = build_tracing_filter(&config);
375
376        assert_eq!(
377            filter,
378            "panic=error,\
379            hyper=warn,\
380            matrix_sdk_ffi=info,\
381            matrix_sdk=info,\
382            matrix_sdk::client=trace,\
383            matrix_sdk_crypto=debug,\
384            matrix_sdk_crypto::olm::account=trace,\
385            matrix_sdk::oidc=trace,\
386            matrix_sdk::http_client=debug,\
387            matrix_sdk::sliding_sync=info,\
388            matrix_sdk_base::sliding_sync=info,\
389            matrix_sdk_ui::timeline=info,\
390            matrix_sdk::event_cache=info,\
391            matrix_sdk_base::event_cache=info,\
392            matrix_sdk_sqlite::event_cache_store=info,\
393            matrix_sdk_common::store_locks=warn,\
394            matrix_sdk_base::store::ambiguity_map=warn,\
395            super_duper_app=error"
396        );
397    }
398
399    #[test]
400    fn test_trace_tracing_filter() {
401        let config = super::TracingConfiguration {
402            log_level: super::LogLevel::Trace,
403            extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]),
404            write_to_stdout_or_system: true,
405            write_to_files: None,
406        };
407
408        let filter = build_tracing_filter(&config);
409
410        assert_eq!(
411            filter,
412            "panic=error,\
413            hyper=warn,\
414            matrix_sdk_ffi=info,\
415            matrix_sdk=info,\
416            matrix_sdk::client=trace,\
417            matrix_sdk_crypto=trace,\
418            matrix_sdk_crypto::olm::account=trace,\
419            matrix_sdk::oidc=trace,\
420            matrix_sdk::http_client=trace,\
421            matrix_sdk::sliding_sync=trace,\
422            matrix_sdk_base::sliding_sync=trace,\
423            matrix_sdk_ui::timeline=trace,\
424            matrix_sdk::event_cache=trace,\
425            matrix_sdk_base::event_cache=trace,\
426            matrix_sdk_sqlite::event_cache_store=trace,\
427            matrix_sdk_common::store_locks=warn,\
428            matrix_sdk_base::store::ambiguity_map=warn,\
429            super_duper_app=trace,\
430            some_other_span=trace"
431        );
432    }
433}