tarolog/
lib.rs

1mod injector;
2mod json;
3mod plain;
4
5pub use json::Injector as JsonInjector;
6pub use plain::Injector as PlainInjector;
7
8use libc::size_t;
9use serde_json::{json, Value};
10use std::ffi::{c_char, c_int, CStr};
11use std::io;
12use std::io::ErrorKind;
13use tarantool::ffi;
14use tarantool::ffi::tarantool::{LogFormatFn, VaList};
15use tarantool::fiber::FiberId;
16use tarantool::log::SayLevel;
17
18extern "C" {
19    #[allow(dead_code)]
20    fn vsprintf(s: *mut c_char, format: *const c_char, ap: VaList) -> c_int;
21    fn vsnprintf(s: *mut c_char, n: size_t, format: *const c_char, ap: VaList) -> c_int;
22}
23
24fn level_to_string(lvl: SayLevel) -> &'static str {
25    match lvl {
26        SayLevel::Fatal => "FATAL",
27        SayLevel::System => "SYSERROR",
28        SayLevel::Error => "ERROR",
29        SayLevel::Crit => "CRIT",
30        SayLevel::Warn => "WARN",
31        SayLevel::Info => "INFO",
32        SayLevel::Verbose => "VERBOSE",
33        SayLevel::Debug => "DEBUG",
34    }
35}
36
37fn level_to_char(lvl: SayLevel) -> &'static str {
38    match lvl {
39        SayLevel::Fatal => "F",
40        SayLevel::System => "!",
41        SayLevel::Error => "E",
42        SayLevel::Crit => "C",
43        SayLevel::Warn => "W",
44        SayLevel::Info => "I",
45        SayLevel::Verbose => "V",
46        SayLevel::Debug => "D",
47    }
48}
49
50fn json_escape_c(c_str: &CStr) -> io::Result<String> {
51    json_escape(
52        c_str
53            .to_str()
54            .map_err(|_| io::Error::new(ErrorKind::InvalidData, "invalid utf8 string"))?,
55    )
56}
57
58fn json_escape(str: &str) -> io::Result<String> {
59    let Value::String(escaped) = json!(str) else {
60        unreachable!()
61    };
62    Ok(escaped)
63}
64
65/// Format is a callback that using by tarantool logger for format all log records.
66pub enum Format {
67    /// A json format, generate log record in json format without any external dependencies (like `serde`).
68    /// This format is as close as possible to a original tarantool 'json' format.
69    ///
70    /// Example: `{"time": "2023-11-07T12:57:47.923+0300", "level": "ERROR", "message": "this is test log record from test `test_json_raw_format`", "pid": 37752, "cord_name": "main", "fiber_id": 103, "fiber_name": "init.lua", "file": "tests/src/test.rs", "line": 38, "field2":100,"field1":true}`
71    JsonTarantool(Option<JsonInjector>),
72    /// A json format, generate log record in json format using `serde`.
73    ///
74    /// Example: `{"time":"2023-11-07T12:57:48.007+0300","level":"ERROR","message":"this is test log record from test `test_json_serde_format`","pid":37752,"cord_name":"main","fiber_id":103,"fiber_name":"init.lua","file":"tests/src/test.rs","line":66,"field2":200,"field1":"true"}`
75    Json(Option<JsonInjector>),
76    /// A plain format.
77    /// This format is as close as possible to a original tarantool 'plain' format.
78    ///
79    /// Example: `2023-11-07 12:57:48.206 [37752] main/103/init.lua test.rs:83 W> this is test log record from test `test_plain_format` (counter: 38)`
80    PlainTarantool(Option<PlainInjector>),
81    /// User defined format.
82    Custom(LogFormatFn),
83}
84
85/// Set a new formation function to a default tarantool logger.
86///
87/// # Arguments
88///
89/// * `format`: a format function, can be one of pre-defined formats (plain or json) or fully user
90///   defined.
91///
92/// # Examples
93///
94/// ```no_run
95/// use std::sync::atomic::{AtomicU64, Ordering};
96/// use tarolog::Format;
97///
98/// static COUNTER: AtomicU64 = AtomicU64::new(1);
99/// tarolog::set_default_logger_format(Format::PlainTarantool(Some(|| {
100///     format!("counter: {}", COUNTER.fetch_add(1, Ordering::SeqCst))
101/// })));
102/// ```
103pub fn set_default_logger_format(format: Format) {
104    // SAFETY: always safe.
105    let default_logger = unsafe { &mut *tarantool::ffi::tarantool::log_default_logger() };
106
107    set_logger_format(default_logger, format)
108}
109
110/// Set a new formation function to a tarantool logger.
111///
112/// # Arguments
113///
114/// * `format`: a format function, can be one of pre-defined formats (plain or json) or fully user
115///   defined.
116///
117/// # Examples
118///
119/// ```no_run
120/// use std::sync::atomic::{AtomicU64, Ordering};
121/// use tarolog::Format;
122///
123/// // get default logger
124/// let logger = unsafe { &mut *tarantool::ffi::tarantool::log_default_logger() };
125/// static COUNTER: AtomicU64 = AtomicU64::new(1);
126/// tarolog::set_logger_format(logger, Format::PlainTarantool(Some(|| {
127///     format!("counter: {}", COUNTER.fetch_add(1, Ordering::SeqCst))
128/// })));
129/// ```
130pub fn set_logger_format(logger: &mut ffi::tarantool::Logger, format: Format) {
131    let log_format = match format {
132        Format::JsonTarantool(injector) => json::raw::make_custom_json_format(logger, injector),
133        Format::Json(injector) => json::serde::make_custom_json_format_serde(logger, injector),
134        Format::PlainTarantool(injector) => plain::make_custom_plain_format(logger, injector),
135        Format::Custom(f) => f,
136    };
137
138    // SAFETY: always safe.
139    unsafe { tarantool::ffi::tarantool::log_set_format(logger, log_format) };
140}
141
142fn get_cord_name() -> &'static str {
143    #[cfg(not(test))]
144    {
145        let cord_name_ptr = unsafe { tarantool::ffi::tarantool::current_cord_name() };
146        if cord_name_ptr.is_null() {
147            "unknown"
148        } else {
149            unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(cord_name_ptr).to_bytes()) }
150        }
151    }
152    #[cfg(test)]
153    {
154        "unknown"
155    }
156}
157
158struct FiberInfo {
159    id: FiberId,
160    name: &'static str,
161}
162
163#[allow(unused)]
164fn get_fiber_info(cord_name: &str) -> Option<FiberInfo> {
165    #[cfg(not(test))]
166    {
167        use tarantool::fiber;
168
169        // from tarantool sources
170        const FIBER_ID_SCHED: u64 = 1;
171
172        if cord_name != "unknown" {
173            let fiber_id = fiber::id();
174            if fiber_id != FIBER_ID_SCHED {
175                let fiber_name = unsafe {
176                    std::str::from_utf8_unchecked(fiber::name_raw(None).unwrap_or_default())
177                };
178                return Some(FiberInfo {
179                    id: fiber_id,
180                    name: fiber_name,
181                });
182            }
183        };
184        None
185    }
186
187    #[cfg(test)]
188    None
189}
190
191#[cfg(test)]
192mod helper {
193    use std::ffi::c_int;
194    use std::ffi::{c_char, CString};
195    use std::ptr;
196    use tarantool::ffi::tarantool::VaList;
197    use tarantool::log::SayLevel;
198
199    // Using for generate a test va_list on C-side and use it in rust-side tests.
200    #[link(name = "va_list_test", kind = "static")]
201    extern "C" {
202        pub fn dispatch(context: *mut u8, count: ::libc::c_uint, ...);
203    }
204
205    pub type CbType<'a> = &'a mut dyn FnMut(VaList<'a>);
206
207    // Method called by 'dispatch', call rust function with va_list as argument.
208    #[no_mangle]
209    pub extern "C" fn inbound(context: *mut u8, _count: u32, args: VaList) {
210        let cb_ptr = unsafe { ptr::read(context as *mut CbType) };
211        (cb_ptr)(args);
212    }
213
214    // Call `code` with `args` packed in va_list.
215    #[macro_export]
216    macro_rules! with_va_list {
217        (($($args:expr),*), $code:expr) => ({
218            let mut cb = $code;
219            let mut cb_dyn: $crate::helper::CbType = &mut cb;
220
221            unsafe {
222                $crate::helper::dispatch(
223                    &mut cb_dyn as *mut _ as *mut u8,
224                    0,
225                    $($args),*
226                );
227            }
228        });
229    }
230
231    pub enum MessageType {
232        TwoInts(i32, i32),
233        String(&'static str),
234    }
235
236    pub struct TestCase<I> {
237        pub level: c_int,
238        module: Option<CString>,
239        filename: Option<CString>,
240        pub line: c_int,
241        error: Option<CString>,
242        format: Option<CString>,
243        pub message_type: MessageType,
244        pub injector: Option<I>,
245        pub expected_record_re: &'static str,
246    }
247
248    #[allow(clippy::too_many_arguments)]
249    impl<I> TestCase<I> {
250        pub fn new(
251            level: SayLevel,
252            module: Option<&str>,
253            filename: Option<&str>,
254            line: i32,
255            error: Option<&str>,
256            format: Option<&str>,
257            message_type: MessageType,
258            injector: Option<I>,
259            expected_re: &'static str,
260        ) -> Self {
261            Self {
262                level: level as i32,
263                module: module.map(|m| CString::new(m).unwrap()),
264                filename: filename.map(|f| CString::new(f).unwrap()),
265                line,
266                error: error.map(|e| CString::new(e).unwrap()),
267                format: format.map(|f| CString::new(f).unwrap()),
268                message_type,
269                expected_record_re: expected_re,
270                injector,
271            }
272        }
273
274        pub fn module_ptr(&self) -> *const c_char {
275            self.module
276                .as_ref()
277                .map(|m| m.as_ptr())
278                .unwrap_or(ptr::null())
279        }
280
281        pub fn filename_ptr(&self) -> *const c_char {
282            self.filename
283                .as_ref()
284                .map(|f| f.as_ptr())
285                .unwrap_or(ptr::null())
286        }
287
288        pub fn error_ptr(&self) -> *const c_char {
289            self.error
290                .as_ref()
291                .map(|e| e.as_ptr())
292                .unwrap_or(ptr::null())
293        }
294
295        pub fn format_ptr(&self) -> *const c_char {
296            self.format
297                .as_ref()
298                .map(|f| f.as_ptr())
299                .unwrap_or(ptr::null())
300        }
301    }
302}