tracing_logcat/
lib.rs

1// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
2// SPDX-License-Identifier: Apache-2.0
3
4//! `tracing` writer for logging into Android's logcat. Instead of linking
5//! liblog, which isn't available as a static library in the NDK, this library
6//! directly connects to logd and sends messages via the [documented protocol].
7//!
8//! There are a few behavioral differences compared to liblog:
9//!
10//! * In the very unlikely event that Android's logd crashes, logging will stop
11//!   working because tracing-logcat does not attempt to reconnect to the logd
12//!   socket.
13//! * Only Android 5 and newer are supported. Previous versions of Android did
14//!   not use logd and implemented logcat without a userspace daemon.
15//! * Log messages longer than `4068 - <tag length> - 2` bytes are split into
16//!   multiple messages instead of being truncated. If the original message is
17//!   valid UTF-8, then the message is split at a code point boundary (not at a
18//!   grapheme cluster boundary). Otherwise, the message is split exactly at the
19//!   length limit.
20//!
21//! [documented protocol]: https://cs.android.com/android/platform/superproject/main/+/main:system/logging/liblog/README.protocol.md
22//!
23//! ## Examples
24//!
25//! ### Using a fixed tag
26//!
27//! ```no_run
28//! use tracing::Level;
29//! use tracing_logcat::{LogcatMakeWriter, LogcatTag};
30//! use tracing_subscriber::fmt::format::Format;
31//!
32//! let tag = LogcatTag::Fixed(env!("CARGO_PKG_NAME").to_owned());
33//! let writer = LogcatMakeWriter::new(tag)
34//!    .expect("Failed to initialize logcat writer");
35//!
36//! tracing_subscriber::fmt()
37//!     .event_format(Format::default().with_level(false).without_time())
38//!     .with_writer(writer)
39//!     .with_ansi(false)
40//!     .with_max_level(Level::TRACE)
41//!     .init();
42//! ```
43//!
44//! ### Using the tracing target as the tag
45//!
46//! ```no_run
47//! use tracing::Level;
48//! use tracing_logcat::{LogcatMakeWriter, LogcatTag};
49//! use tracing_subscriber::fmt::format::Format;
50//!
51//! let writer = LogcatMakeWriter::new(LogcatTag::Target)
52//!    .expect("Failed to initialize logcat writer");
53//!
54//! tracing_subscriber::fmt()
55//!     .event_format(Format::default().with_level(false).with_target(false).without_time())
56//!     .with_writer(writer)
57//!     .with_ansi(false)
58//!     .with_max_level(Level::TRACE)
59//!     .init();
60//! ```
61
62use std::{
63    borrow::Cow,
64    io::{self, IoSlice, Write},
65    ops::DerefMut,
66    os::unix::net::UnixDatagram,
67    str,
68    sync::Mutex,
69    time::SystemTime,
70};
71
72use tracing::{Level, Metadata};
73use tracing_subscriber::fmt::MakeWriter;
74
75/// Truncate a string so that it doesn't exceed n bytes without producing
76/// invalid UTF-8 sequences. However, this does not necessarily truncate at a
77/// grapheme cluster boundary.
78fn truncate_floor(s: &str, n: usize) -> &str {
79    // str::floor_char_boundary() has not been stablized yet.
80
81    let bound = if n >= s.len() {
82        s.len()
83    } else {
84        let lower_bound = n.saturating_sub(3);
85        let new_index = (lower_bound..=n).rfind(|i| s.is_char_boundary(*i));
86
87        // SAFETY: str is guaranteed to contain well-formed UTF-8.
88        unsafe { new_index.unwrap_unchecked() }
89    };
90
91    &s[..bound]
92}
93
94enum MaybeUtf8Buf<'a> {
95    Bytes(&'a [u8]),
96    String(&'a str),
97}
98
99impl<'a> MaybeUtf8Buf<'a> {
100    fn new(data: &'a [u8]) -> Self {
101        match str::from_utf8(data) {
102            Ok(s) => Self::String(s),
103            Err(_) => Self::Bytes(data),
104        }
105    }
106
107    fn split_floor(&self, limit: usize) -> (Self, Self) {
108        match self {
109            Self::Bytes(b) => {
110                let chunk = &b[..limit.min(b.len())];
111                let remain = &b[chunk.len()..];
112
113                (Self::Bytes(chunk), Self::Bytes(remain))
114            }
115            Self::String(s) => {
116                let chunk = truncate_floor(s, limit);
117                let remain = &s[chunk.len()..];
118
119                (Self::String(chunk), Self::String(remain))
120            }
121        }
122    }
123
124    fn as_slice(&self) -> &'a [u8] {
125        match self {
126            Self::Bytes(b) => b,
127            Self::String(s) => s.as_bytes(),
128        }
129    }
130}
131
132/// Iterator that chunks a byte array at the nearest code unit boundary below
133/// the specified limit if it is valid UTF-8. If the byte array is not valid
134/// UTF-8, then it is split exactly at the limit.
135struct Chunker<'a> {
136    data: MaybeUtf8Buf<'a>,
137    limit: usize,
138}
139
140impl<'a> Chunker<'a> {
141    fn new(data: &'a [u8], limit: usize) -> Self {
142        assert!(
143            limit >= 4,
144            "Limit cannot be smaller than largest UTF-8 code unit"
145        );
146
147        Self {
148            data: MaybeUtf8Buf::new(data),
149            limit,
150        }
151    }
152}
153
154impl<'a> Iterator for Chunker<'a> {
155    type Item = &'a [u8];
156
157    fn next(&mut self) -> Option<Self::Item> {
158        let (chunk, remain) = self.data.split_floor(self.limit);
159        let chunk = chunk.as_slice();
160
161        if chunk.is_empty() {
162            None
163        } else {
164            self.data = remain;
165
166            Some(chunk)
167        }
168    }
169}
170
171/// Tag string to use for log messages.
172#[derive(Debug, Clone)]
173pub enum LogcatTag {
174    /// Log all messages with a fixed tag.
175    Fixed(String),
176    /// Use the `tracing` event target as the tag.
177    Target,
178}
179
180/// An [`io::Write`] instance that outputs to Android's logcat.
181#[derive(Debug)]
182pub struct LogcatWriter<'a> {
183    socket: &'a Mutex<UnixDatagram>,
184    tag: Cow<'a, str>,
185    level: Level,
186}
187
188impl Write for LogcatWriter<'_> {
189    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
190        // Implicitly uses LOG_ID_MAIN, which is 0.
191        let mut header = [0u8; 11];
192
193        let thread_id = rustix::thread::gettid().as_raw_nonzero();
194        header[1..3].copy_from_slice(&(thread_id.get() as u16).to_le_bytes());
195
196        let timestamp = SystemTime::now()
197            .duration_since(SystemTime::UNIX_EPOCH)
198            .unwrap_or_default();
199        header[3..7].copy_from_slice(&(timestamp.as_secs() as u32).to_le_bytes());
200        header[7..11].copy_from_slice(&timestamp.subsec_nanos().to_le_bytes());
201
202        let priority = match self.level {
203            Level::TRACE => [2u8], // ANDROID_LOG_VERBOSE
204            Level::DEBUG => [3u8], // ANDROID_LOG_DEBUG
205            Level::INFO => [4u8],  // ANDROID_LOG_INFO
206            Level::WARN => [5u8],  // ANDROID_LOG_WARN
207            Level::ERROR => [6u8], // ANDROID_LOG_ERROR
208        };
209
210        // This is truncated to guarantee that we can make progress writing the
211        // message.
212        let tag = truncate_floor(&self.tag, 128);
213
214        // Everything must be sent as a single datagram.
215        let mut iovecs = [
216            IoSlice::new(&header),
217            IoSlice::new(&priority),
218            IoSlice::new(tag.as_bytes()),
219            IoSlice::new(&[0]),
220            IoSlice::new(&[]),
221            IoSlice::new(&[0]),
222        ];
223        let message_index = 4;
224
225        // Max payload size excludes the header.
226        let max_message_len = 4068 - iovecs[1..].iter().map(|v| v.len()).sum::<usize>();
227
228        // Lock once. We don't interleave chunked messages.
229        let mut socket = self.socket.lock().unwrap();
230
231        // Remove the implicit trailing newline that tracing adds.
232        let no_newline = buf.strip_suffix(b"\n").unwrap_or(buf);
233
234        // Unlike liblog, we split long messages instead of truncating them.
235        for chunk in Chunker::new(no_newline, max_message_len) {
236            iovecs[message_index] = IoSlice::new(chunk);
237
238            // UnixDatagram does not have a send_vectored().
239            let n = rustix::io::writev(socket.deref_mut(), &iovecs)?;
240            if n != iovecs.iter().map(|v| v.len()).sum() {
241                return Err(io::Error::new(
242                    io::ErrorKind::UnexpectedEof,
243                    "logcat datagram was truncated",
244                ));
245            }
246        }
247
248        Ok(buf.len())
249    }
250
251    fn flush(&mut self) -> io::Result<()> {
252        Ok(())
253    }
254}
255
256/// A [`MakeWriter`] type that creates [`LogcatWriter`] instances that output to
257/// Android's logcat.
258#[derive(Debug)]
259pub struct LogcatMakeWriter {
260    tag: LogcatTag,
261    socket: Mutex<UnixDatagram>,
262}
263
264impl LogcatMakeWriter {
265    /// Return a new instance with the specified tag source.
266    pub fn new(tag: LogcatTag) -> io::Result<Self> {
267        let socket = UnixDatagram::unbound()?;
268        socket.connect("/dev/socket/logdw")?;
269
270        Ok(Self {
271            tag,
272            socket: Mutex::new(socket),
273        })
274    }
275
276    fn get_tag(&self, meta: Option<&Metadata>) -> Cow<str> {
277        match &self.tag {
278            LogcatTag::Fixed(s) => Cow::Borrowed(s),
279            LogcatTag::Target => match meta {
280                Some(m) => Cow::Owned(m.target().to_owned()),
281                None => Cow::Borrowed(""),
282            },
283        }
284    }
285}
286
287impl<'a> MakeWriter<'a> for LogcatMakeWriter {
288    type Writer = LogcatWriter<'a>;
289
290    fn make_writer(&'a self) -> Self::Writer {
291        LogcatWriter {
292            socket: &self.socket,
293            tag: self.get_tag(None),
294            level: Level::INFO,
295        }
296    }
297
298    fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
299        LogcatWriter {
300            socket: &self.socket,
301            tag: self.get_tag(Some(meta)),
302            level: *meta.level(),
303        }
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn chunker() {
313        let mut chunker = Chunker::new(b"", 4);
314        assert_eq!(chunker.next(), None);
315
316        chunker = Chunker::new(b"abcd", 4);
317        assert_eq!(chunker.next(), Some(&b"abcd"[..]));
318        assert_eq!(chunker.next(), None);
319
320        chunker = Chunker::new(b"foobar", 4);
321        assert_eq!(chunker.next(), Some(&b"foob"[..]));
322        assert_eq!(chunker.next(), Some(&b"ar"[..]));
323        assert_eq!(chunker.next(), None);
324
325        for limit in [4, 5] {
326            chunker = Chunker::new("你好".as_bytes(), limit);
327            assert_eq!(chunker.next(), Some("你".as_bytes()));
328            assert_eq!(chunker.next(), Some("好".as_bytes()));
329            assert_eq!(chunker.next(), None);
330        }
331
332        chunker = Chunker::new(b"\xffNon-UTF8 \xe4\xbd\xa0\xe5\xa5\xbd", 4);
333        assert_eq!(chunker.next(), Some(&b"\xffNon"[..]));
334        assert_eq!(chunker.next(), Some(&b"-UTF"[..]));
335        assert_eq!(chunker.next(), Some(&b"8 \xe4\xbd"[..]));
336        assert_eq!(chunker.next(), Some(&b"\xa0\xe5\xa5\xbd"[..]));
337        assert_eq!(chunker.next(), None);
338    }
339}