spdlog/sink/
android_sink.rs

1use std::{ffi::CString, io, ptr::null, result::Result as StdResult};
2
3use libc::EPERM;
4
5use crate::{
6    formatter::{Formatter, FormatterContext, FullFormatter},
7    prelude::*,
8    sink::{GetSinkProp, Sink, SinkProp},
9    sync::*,
10    Error, ErrorHandler, Record, Result, StringBuf,
11};
12
13#[cfg(not(doc))]
14mod ffi {
15    use android_log_sys::{LogPriority, __android_log_write, c_int};
16
17    use super::*;
18
19    pub(super) struct AndroidLevelsMapping([LogPriority; Level::count()]);
20
21    impl AndroidLevelsMapping {
22        #[must_use]
23        pub(super) const fn new() -> Self {
24            Self([
25                LogPriority::FATAL,   // spdlog::Critical
26                LogPriority::ERROR,   // spdlog::Error
27                LogPriority::WARN,    // spdlog::Warn
28                LogPriority::INFO,    // spdlog::Info
29                LogPriority::DEBUG,   // spdlog::Debug
30                LogPriority::VERBOSE, // spdlog::Trace
31            ])
32        }
33
34        #[must_use]
35        pub(super) fn level(&self, level: Level) -> LogPriority {
36            self.0[level as usize]
37        }
38    }
39
40    pub(super) fn android_log_write(
41        priority: LogPriority,
42        tag: Option<&str>,
43        text: &str,
44    ) -> StdResult<(), io::Error> {
45        let tag = tag
46            .map(CString::new)
47            .transpose()
48            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
49        let text =
50            CString::new(text).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
51
52        let tag_ptr = tag.as_deref().map(|tag| tag.as_ptr()).unwrap_or_else(null);
53        let text_ptr = text.as_ptr();
54
55        let result = unsafe { __android_log_write(priority as c_int, tag_ptr, text_ptr) };
56
57        // Explicitly drop to ensure that they have not been moved to cause dangling
58        // pointers.
59        drop((tag, text));
60
61        // Although the documentation [^1] says that:
62        //   1 if the message was written to the log, or -EPERM if it was not;
63        //
64        // It doesn't point out that the behavior differs between versions. The above
65        // behavior is available since Android 11 (API 30). Before that, the behavior of
66        // the return value was not clarified in the documentation, but referring to the
67        // implementation, for a successful log write, the number of bytes written is
68        // actually returned. This behavior is changed in this commit [^2].
69        //
70        // For compatible with more versions, we do not use `result == 1` as the success
71        // condition, but `result >= 0` instead.
72        //
73        // [^1]: https://developer.android.com/ndk/reference/group/logging#group___logging_1ga32a7173b092ec978b50490bd12ee523b
74        // [^2]: https://android.googlesource.com/platform/system/logging/+/c17613c4582d4f6eecb3965bb96584f25762b827%5E%21/
75        //
76        // ---
77        //
78        // For the condition `result == -EPERM`, see
79        // https://github.com/gabime/spdlog/commit/01b3724c484eebb42d83fa21aa8d71a57b2b8fb6
80        if result >= 0 || /* !__android_log_is_loggable */ result == -EPERM {
81            Ok(())
82        } else {
83            Err(io::Error::from_raw_os_error(-result))
84        }
85    }
86}
87
88/// Represents how to choose a tag for Android logs.
89///
90/// # Log Level Mapping
91///
92/// | spdlog-rs  | Android NDK |
93/// |------------|-------------|
94/// | `Critical` | `FATAL`     |
95/// | `Error`    | `ERROR`     |
96/// | `Warn`     | `WARN`      |
97/// | `Info`     | `INFO`      |
98/// | `Debug`    | `DEBUG`     |
99/// | `Trace`    | `VERBOSE`   |
100///
101/// # Note
102///
103/// It requires linking to Android NDK `liblog`.
104pub enum AndroidLogTag {
105    /// The default tag determined by Android NDK.
106    Default,
107    /// The name of the `spdlog-rs` logger that generated the log.
108    LoggerName,
109    /// A custom string.
110    Custom(String),
111}
112
113#[allow(missing_docs)]
114pub struct AndroidSinkBuilder {
115    prop: SinkProp,
116    tag: AndroidLogTag,
117}
118
119impl AndroidSinkBuilder {
120    /// Specifies how to choose a tag for Android logs.
121    ///
122    /// This parameter is **optional**, and defaults to
123    /// [`AndroidLogTag::Default`].
124    #[must_use]
125    pub fn tag(mut self, tag: AndroidLogTag) -> Self {
126        self.tag = tag;
127        self
128    }
129
130    // Prop
131    //
132
133    /// Specifies a log level filter.
134    ///
135    /// This parameter is **optional**, and defaults to [`LevelFilter::All`].
136    #[must_use]
137    pub fn level_filter(self, level_filter: LevelFilter) -> Self {
138        self.prop.set_level_filter(level_filter);
139        self
140    }
141
142    /// Specifies a formatter.
143    ///
144    /// This parameter is **optional**, and defaults to [`FullFormatter`]
145    /// `(!time !level !eol)`.
146    #[must_use]
147    pub fn formatter<F>(self, formatter: F) -> Self
148    where
149        F: Formatter + 'static,
150    {
151        self.prop.set_formatter(formatter);
152        self
153    }
154
155    /// Specifies an error handler.
156    ///
157    /// This parameter is **optional**, and defaults to
158    /// [`ErrorHandler::default()`].
159    #[must_use]
160    pub fn error_handler<F: Into<ErrorHandler>>(self, handler: F) -> Self {
161        self.prop.set_error_handler(handler);
162        self
163    }
164
165    //
166
167    /// Constructs a `AndroidSink`.
168    pub fn build(self) -> Result<AndroidSink> {
169        Ok(AndroidSink {
170            prop: self.prop,
171            tag: self.tag,
172        })
173    }
174
175    /// Builds a `Arc<AndroidSink>`.
176    ///
177    /// This is a shorthand method for `.build().map(Arc::new)`.
178    pub fn build_arc(self) -> Result<Arc<AndroidSink>> {
179        self.build().map(Arc::new)
180    }
181}
182
183/// A sink with Android NDK API `__android_log_write` as the target.
184pub struct AndroidSink {
185    prop: SinkProp,
186    tag: AndroidLogTag,
187}
188
189impl AndroidSink {
190    #[cfg(not(doc))]
191    const LEVELS_MAPPING: ffi::AndroidLevelsMapping = ffi::AndroidLevelsMapping::new();
192
193    /// Gets a builder of `AndroidSink` with default parameters:
194    ///
195    /// | Parameter       | Default Value                           |
196    /// |-----------------|-----------------------------------------|
197    /// | [level_filter]  | [`LevelFilter::All`]                    |
198    /// | [formatter]     | [`FullFormatter`] `(!time !level !eol)` |
199    /// | [error_handler] | [`ErrorHandler::default()`]             |
200    /// |                 |                                         |
201    /// | [tag]           | [`AndroidLogTag::Default`]              |
202    ///
203    /// [level_filter]: AndroidSinkBuilder::level_filter
204    /// [formatter]: AndroidSinkBuilder::formatter
205    /// [error_handler]: AndroidSinkBuilder::error_handler
206    /// [tag]: AndroidSinkBuilder::tag
207    #[must_use]
208    pub fn builder() -> AndroidSinkBuilder {
209        let prop = SinkProp::default();
210        prop.set_formatter(
211            FullFormatter::builder()
212                .time(false)
213                .level(false)
214                .eol(false)
215                .build(),
216        );
217
218        AndroidSinkBuilder {
219            prop,
220            tag: AndroidLogTag::Default,
221        }
222    }
223
224    /// Gets how to choose a tag for Android logs.
225    #[must_use]
226    pub fn tag(&self) -> &AndroidLogTag {
227        &self.tag
228    }
229
230    /// Sets how to choose a tag for Android logs.
231    pub fn set_tag(&mut self, tag: AndroidLogTag) {
232        self.tag = tag;
233    }
234}
235
236impl GetSinkProp for AndroidSink {
237    fn prop(&self) -> &SinkProp {
238        &self.prop
239    }
240}
241
242impl Sink for AndroidSink {
243    fn log(&self, record: &Record) -> Result<()> {
244        let mut string_buf = StringBuf::new();
245        let mut ctx = FormatterContext::new();
246        self.prop
247            .formatter()
248            .format(record, &mut string_buf, &mut ctx)?;
249
250        let priority = Self::LEVELS_MAPPING.level(record.level());
251        let tag = match &self.tag {
252            AndroidLogTag::Default => None,
253            AndroidLogTag::LoggerName => record.logger_name(),
254            AndroidLogTag::Custom(tag) => Some(tag.as_str()),
255        };
256        ffi::android_log_write(priority, tag, &string_buf).map_err(Error::WriteRecord)
257    }
258
259    fn flush(&self) -> Result<()> {
260        Ok(())
261    }
262}