matrix_sdk_ffi/
tracing.rs

1use std::{
2    collections::BTreeMap,
3    sync::{Arc, Mutex},
4};
5
6use once_cell::sync::OnceCell;
7use tracing::{callsite::DefaultCallsite, field::FieldSet, Callsite};
8use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
9
10/// Log an event.
11///
12/// The target should be something like a module path, and can be referenced in
13/// the filter string given to `setup_tracing`. `level` and `target` for a
14/// callsite are fixed at the first `log_event` call for that callsite and can
15/// not be changed afterwards, i.e. the level and target passed for second and
16/// following `log_event`s with the same callsite will be ignored.
17///
18/// This function leaks a little bit of memory for each unique (file + line +
19/// level + target) it is called with. Please make sure that the number of
20/// different combinations of those parameters this can be called with is
21/// constant in the final executable.
22#[matrix_sdk_ffi_macros::export]
23fn log_event(file: String, line: Option<u32>, level: LogLevel, target: String, message: String) {
24    static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
25        Mutex::new(BTreeMap::new());
26
27    let id = MetadataId { file, line, level, target, name: None };
28    let callsite = get_or_init_metadata(&CALLSITES, id, &["message"], MetadataKind::EVENT);
29
30    if span_or_event_enabled(callsite) {
31        let metadata = callsite.metadata();
32        let fields = metadata.fields();
33        let message_field = fields.field("message").unwrap();
34        #[allow(trivial_casts)] // The compiler is lying, it can't infer this cast
35        let values = [(&message_field, Some(&message as &dyn tracing::Value))];
36
37        // This function is hidden from docs, but we have to use it
38        // because there is no other way of obtaining a `ValueSet`.
39        // It's not entirely clear why it is private. See this issue:
40        // https://github.com/tokio-rs/tracing/issues/2363
41        let values = fields.value_set(&values);
42        tracing::Event::dispatch(metadata, &values);
43    }
44}
45
46type FieldNames = &'static [&'static str];
47
48fn get_or_init_metadata(
49    mutex: &Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>>,
50    id: MetadataId,
51    field_names: FieldNames,
52    meta_kind: MetadataKind,
53) -> &'static DefaultCallsite {
54    mutex.lock().unwrap().entry(id).or_insert_with_key(|id| {
55        let callsite = Box::leak(Box::new(LateInitCallsite(OnceCell::new())));
56        let metadata = Box::leak(Box::new(tracing::Metadata::new(
57            Box::leak(
58                id.name
59                    .clone()
60                    .unwrap_or_else(|| match id.line {
61                        Some(line) => format!("event {}:{line}", id.file),
62                        None => format!("event {}", id.file),
63                    })
64                    .into_boxed_str(),
65            ),
66            Box::leak(id.target.as_str().into()),
67            id.level.to_tracing_level(),
68            Some(Box::leak(Box::from(id.file.as_str()))),
69            id.line,
70            None, // module path
71            FieldSet::new(field_names, identify_callsite!(callsite)),
72            meta_kind,
73        )));
74        callsite.0.try_insert(DefaultCallsite::new(metadata)).expect("callsite was not set before")
75    })
76}
77
78fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
79    use tracing::{
80        dispatcher,
81        level_filters::{LevelFilter, STATIC_MAX_LEVEL},
82    };
83
84    let meta = callsite.metadata();
85    let level = *meta.level();
86
87    if level > STATIC_MAX_LEVEL || level > LevelFilter::current() {
88        false
89    } else {
90        let interest = callsite.interest();
91        interest.is_always()
92            || !interest.is_never() && dispatcher::get_default(|default| default.enabled(meta))
93    }
94}
95
96#[derive(uniffi::Object)]
97pub struct Span(tracing::Span);
98
99#[matrix_sdk_ffi_macros::export]
100impl Span {
101    /// Create a span originating at the given callsite (file, line and column).
102    ///
103    /// The target should be something like a module path, and can be referenced
104    /// in the filter string given to `setup_tracing`. `level` and `target`
105    /// for a callsite are fixed at the first creation of a span for that
106    /// callsite and can not be changed afterwards, i.e. the level and
107    /// target passed for second and following creation of a span with the same
108    /// callsite will be ignored.
109    ///
110    /// This function leaks a little bit of memory for each unique (file +
111    /// line + level + target + name) it is called with. Please make sure that
112    /// the number of different combinations of those parameters this can be
113    /// called with is constant in the final executable.
114    ///
115    /// For a span to have an effect, you must `.enter()` it at the start of a
116    /// logical unit of work and `.exit()` it at the end of the same (including
117    /// on failure). Entering registers the span in thread-local storage, so
118    /// future calls to `log_event` on the same thread are able to attach the
119    /// events they create to the span, exiting unregisters it. For this to
120    /// work, exiting a span must be done on the same thread where it was
121    /// entered. It is possible to enter a span on multiple threads, in which
122    /// case it should also be exited on all of them individually; that is,
123    /// unless you *want* the span to be attached to all further events created
124    /// on that thread.
125    #[uniffi::constructor]
126    pub fn new(
127        file: String,
128        line: Option<u32>,
129        level: LogLevel,
130        target: String,
131        name: String,
132    ) -> Arc<Self> {
133        static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
134            Mutex::new(BTreeMap::new());
135
136        let loc = MetadataId { file, line, level, target, name: Some(name) };
137        let callsite = get_or_init_metadata(&CALLSITES, loc, &[], MetadataKind::SPAN);
138        let metadata = callsite.metadata();
139
140        let span = if span_or_event_enabled(callsite) {
141            // This function is hidden from docs, but we have to use it (see above).
142            let values = metadata.fields().value_set(&[]);
143            tracing::Span::new(metadata, &values)
144        } else {
145            tracing::Span::none()
146        };
147
148        Arc::new(Self(span))
149    }
150
151    #[uniffi::constructor]
152    pub fn current() -> Arc<Self> {
153        Arc::new(Self(tracing::Span::current()))
154    }
155
156    fn enter(&self) {
157        self.0.with_subscriber(|(id, dispatch)| dispatch.enter(id));
158    }
159
160    fn exit(&self) {
161        self.0.with_subscriber(|(id, dispatch)| dispatch.exit(id));
162    }
163
164    fn is_none(&self) -> bool {
165        self.0.is_none()
166    }
167}
168
169#[derive(PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)]
170pub enum LogLevel {
171    Error,
172    Warn,
173    Info,
174    Debug,
175    Trace,
176}
177
178impl LogLevel {
179    fn to_tracing_level(&self) -> tracing::Level {
180        match self {
181            LogLevel::Error => tracing::Level::ERROR,
182            LogLevel::Warn => tracing::Level::WARN,
183            LogLevel::Info => tracing::Level::INFO,
184            LogLevel::Debug => tracing::Level::DEBUG,
185            LogLevel::Trace => tracing::Level::TRACE,
186        }
187    }
188
189    pub(crate) fn as_str(&self) -> &'static str {
190        match self {
191            LogLevel::Error => "error",
192            LogLevel::Warn => "warn",
193            LogLevel::Info => "info",
194            LogLevel::Debug => "debug",
195            LogLevel::Trace => "trace",
196        }
197    }
198}
199
200#[derive(PartialEq, Eq, PartialOrd, Ord)]
201struct MetadataId {
202    file: String,
203    line: Option<u32>,
204    level: LogLevel,
205    target: String,
206    name: Option<String>,
207}
208
209struct LateInitCallsite(OnceCell<DefaultCallsite>);
210
211impl Callsite for LateInitCallsite {
212    fn set_interest(&self, interest: tracing_core::Interest) {
213        self.0
214            .get()
215            .expect("Callsite impl must not be used before initialization")
216            .set_interest(interest)
217    }
218
219    fn metadata(&self) -> &tracing::Metadata<'_> {
220        self.0.get().expect("Callsite impl must not be used before initialization").metadata()
221    }
222}