matrix_sdk_ffi/
tracing.rs

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