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