pinenut_log/
lib.rs

1//! An extremely high performance logging system for clients (iOS, Android, Desktop),
2//! written in Rust.
3//!
4//! ### Compression
5//!
6//! Pinenut supports streaming log compression, it uses the `Zstandard (aka zstd)`, a
7//! high performance compression algorithm that has a good balance between
8//! compression rate and speed.
9//!
10//! ### Encryption
11//!
12//! Pinenut uses the `AES 128` algorithm for symmetric encryption during logging. To
13//! prevent embedding the symmetric key directly into the code, Pinenut uses `ECDH`
14//! for key negotiation (RSA is not used  because its key are too long). When
15//! initializing the Logger, there is no need to provide the symmetric encryption
16//! key, instead the ECDH public key should be passed.
17//!
18//! Pinenut uses `secp256r1` elliptic curve for ECDH. You can generate the secret and
19//! public keys for encryption yourself, or use Pinenut's built-in command line tool:
20//! `pinenut-cli`.
21//!
22//! ### Buffering
23//!
24//! In order to minimize IO frequency, Pinenut buffers the log data before writing to
25//! the file. Client programs may exit unexpectedly (e.g., crash), Pinenut uses
26//! `mmap` as buffer support, so that if the program unexpectedly exits, the OS can
27//! still help to persist the buffered data. The next time the Logger is initialized,
28//! the buffered data is automatically read and written back to the log file.
29//!
30//! In addition, Pinenut implements a `double-buffering` system to improve buffer
31//! read/write performance and prevent asynchronous IOs from affecting logging of the
32//! current thread.
33//!
34//! ### Extraction
35//!
36//! With Pinenut, we don't need to retrieve all the log files in the directory to
37//! extract logs, it provides convenient extraction capabilities and supports
38//! extraction in time ranges with minute granularity.
39//!
40//! ### Parsing
41//!
42//! The content of Pinenut log files is a special binary sequence after encoding,
43//! compression and encryption, and we can parse the log files using the parsing
44//! capabilities provided by Pinenut.
45//!
46//! ## Usage
47//!
48//! Pinenut's APIs are generally similar regardless of the language used.
49//!
50//! ### Logger Initialization
51//!
52//! Pinenut uses a `Logger` instance for logging. Before we initialize the Logger, we
53//! need to pass in the logger identifier and the path to the directory where the log
54//! files are stored to construct the `Domain` structure.
55//!
56//! We can customize the Logger by explicitly specifying `Config`, see the API
57//! documentation for details.
58//!
59//! ```rust,no_run
60//! # use pinenut_log::{Domain, Config, Logger};
61//! let domain = Domain::new("MyApp".into(), "/path/to/dir".into());
62//! let config = Config::new().key_str(Some("Public Key Base64")).compression_level(10);
63//! let logger = Logger::new(domain, config);
64//! ```
65//!
66//! ### Logging
67//!
68//! Just construct the `Record` and call the `log` method.
69//!
70//! Records can be constructed in `Rust` via the Builder pattern:
71//!
72//! ```rust,no_run
73//! # use pinenut_log::{Meta, Level, Record, Domain};
74//! # let logger = Domain::new("".into(), "".into()).logger_with_default_config();
75//! // Builds `Meta` & `Record`.
76//! let meta = Meta::builder().level(Level::Info).build();
77//! let record = Record::builder().meta(meta).content("Hello World").build();
78//! logger.log(&record);
79//!
80//! // Flushes any buffered records asynchronously.
81//! logger.flush();
82//! ```
83//!
84//! See the API documentation for details.
85//!
86//! ### Extraction
87//!
88//! Just call the `extract` method to extract the logs for the specified time range
89//! (with minute granularity) and write them to the destination file.
90//!
91//! ```rust,no_run
92//! # use std::ops::Sub;
93//! # use std::time::Duration;
94//! # use pinenut_log::Domain;
95//! let domain = Domain::new("MyApp".into(), "/path/to/dir".into());
96//! let now = chrono::Utc::now();
97//! let range = now.sub(Duration::from_secs(1800))..=now;
98//!
99//! if let Err(err) = pinenut_log::extract(domain, range, "/path/to/destination") {
100//!     println!("Error: {err}");
101//! }
102//! ```
103//!
104//!
105//! Note: The content of the extracted file is still a binary sequence that has been
106//! encoded, compressed, and encrypted. We need to parse it to see the log text
107//! content that is easy to read.
108//!
109//! ### Parsing
110//!
111//! You can use the `parse` function for log parsing, **and you can specify the
112//! format of the log parsed text**. See the API documentation for details.
113//!
114//!
115//! ```rust,no_run
116//! // Specifies the `DefaultFormater` as the log formatter.
117//! # use pinenut_log::DefaultFormatter;
118//! # let (path, output) = ("", "");
119//! # let secret_key = None;    
120//! if let Err(err) = pinenut_log::parse_to_file(&path, &output, secret_key, DefaultFormatter) {
121//!     println!("Error: {err}");
122//! }
123//! ```
124//!
125//! Or use the built-in command line tool `pinenut-cli`:
126//!
127//! ```plain
128//! $ pinenut-cli parse ./my_log.pine \
129//!     --output ./plain.log          \
130//!     --secret-key XXXXXXXXXXX
131//! ```
132//!
133//! ### Keys Generation
134//!
135//! Before initializing the Logger or parsing the logs, you need to have the public
136//! and secret keys ready (The public key is used to initialize the Logger and the
137//! secret key is used to parse the logs).
138//!
139//! You can use `pinenut-cli` to generate this pair of keys:
140//!
141//! ``` plain
142//! $ pinenut-cli gen-keys
143//! ```
144
145#![feature(trait_alias)]
146#![feature(let_chains)]
147#![feature(option_take_if)]
148#![feature(result_option_inspect)]
149
150use std::path::PathBuf;
151
152use base64::{prelude::BASE64_STANDARD, Engine};
153use chrono::Timelike;
154
155use crate::compress::ZstdCompressor;
156
157pub mod record;
158pub use record::*;
159
160pub mod compress;
161pub use compress::{CompressionError, DecompressionError};
162
163pub mod encrypt;
164pub use encrypt::{
165    DecryptionError, EncryptionError, EncryptionKey, PublicKey, SecretKey, PUBLIC_KEY_LEN,
166};
167
168pub mod codec;
169pub use codec::{DecodingError, EncodingError};
170
171pub mod chunk;
172pub use chunk::Error as ChunkError;
173
174pub mod runloop;
175pub use runloop::Error as RunloopError;
176
177mod logger;
178pub use logger::{Error as LoggerError, Logger};
179
180mod extract;
181pub use extract::{extract, Error as ExtractionError};
182
183mod parse;
184pub use parse::{parse, parse_to_file, DefaultFormatter, Error as ParsingError, Format};
185
186mod common;
187use common::*;
188
189mod buffer;
190mod logfile;
191mod mmap;
192
193/// The current format version of the Pinenut log structure.
194///
195/// The current version of Pinenut will use the `zstd` compression algorithm and
196/// `AES` encryption algorithm to process the logs.
197pub const FORMAT_VERSION: u16 = 1;
198
199/// The extension of the Pinenut mmap buffer file.
200pub const MMAP_BUFFER_EXTENSION: &str = "pinebuf";
201
202/// The extension of the Pinenut log file.
203pub const FILE_EXTENSION: &str = "pine";
204
205/// The extension of the Pinenut plain log file.
206pub const PLAIN_FILE_EXTENSION: &str = "log";
207
208/// The default buffer length (320 KB) for Pinenut.
209pub const BUFFER_LEN: usize = 320 * 1024;
210
211/// Represents the domain to which the logger belongs, the logs will be organized by
212/// domain.
213#[derive(Clone, Debug)]
214pub struct Domain {
215    /// Used to identity a specific domain for logger.
216    pub identifier: String,
217    /// Used to specify the directory where the log files for this domian are stored.
218    pub directory: PathBuf,
219}
220
221impl Domain {
222    /// Constructs a new `Domain`.
223    #[inline]
224    pub fn new(identifier: String, directory: PathBuf) -> Self {
225        Self { identifier, directory }
226    }
227
228    /// Obtains a logger with a specified configuration.
229    #[inline]
230    pub fn logger(self, config: Config) -> Logger {
231        Logger::new(self, config)
232    }
233
234    /// Obtains a logger with the default configuration.
235    #[inline]
236    pub fn logger_with_default_config(self) -> Logger {
237        self.logger(Config::default())
238    }
239}
240
241/// Represents the dimension of datetime, used for log rotation.
242#[repr(u8)]
243#[non_exhaustive]
244#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
245pub enum TimeDimension {
246    Day = 1,
247    Hour,
248    Minute,
249}
250
251impl TimeDimension {
252    /// Checks whether two datetimes match on a specified dimension.
253    fn check_match(self, left: DateTime, right: DateTime) -> bool {
254        let (left, right) = (left.naive_local(), right.naive_local());
255        let mut is_matched = true;
256
257        if self >= Self::Day {
258            is_matched &= left.date() == right.date();
259        }
260        if self >= Self::Hour {
261            is_matched &= left.hour() == right.hour();
262        }
263        if self >= Self::Minute {
264            is_matched &= left.minute() == right.minute();
265        }
266
267        is_matched
268    }
269}
270
271/// Represents a tracker used to track errors occurred from the logger operations.
272pub trait Track {
273    /// Handles the error on the code location.
274    fn track(&self, error: LoggerError, file: &'static str, line: u32);
275}
276
277impl<F> Track for F
278where
279    F: Fn(LoggerError, &'static str, u32),
280{
281    #[inline]
282    fn track(&self, error: LoggerError, file: &'static str, line: u32) {
283        self(error, file, line)
284    }
285}
286
287/// Trait object type for [`Track`].
288pub type Tracker = Box<dyn Track + Send + Sync>;
289
290/// Configuration of a logger instance.
291pub struct Config {
292    use_mmap: bool,
293    buffer_len: usize,
294    rotation: TimeDimension,
295    key: Option<PublicKey>,
296    compression_level: i32,
297    tracker: Option<Tracker>,
298}
299
300impl Config {
301    /// Constructs a new `Config`.
302    #[inline]
303    pub fn new() -> Self {
304        Default::default()
305    }
306
307    /// Whether or not to use `mmap` as the underlying storage for the buffer.
308    ///
309    /// With mmap, if the application terminates unexpectedly, the log data in the
310    /// buffer is written to the mmap buffer file by the OS at a certain time, and
311    /// then when the logger is restarted, the log data is written back to the log
312    /// file, avoiding loss of log data.
313    ///
314    /// It is enabled by default.
315    #[inline]
316    pub fn use_mmap(mut self, flag: bool) -> Self {
317        self.use_mmap = flag;
318        self
319    }
320
321    /// The buffer length.
322    ///
323    /// If mmap is used, it is rounded up to a multiple of pagesize.
324    /// Pinenut uses a double cache system, so the buffer that is actually written
325    /// to will be less than half of this.
326    ///
327    /// The default value is `320 KB`.
328    #[inline]
329    pub fn buffer_len(mut self, len: usize) -> Self {
330        self.buffer_len = len;
331        self
332    }
333
334    /// Time granularity of log extraction.
335    ///
336    /// The default value is `Minute`.
337    #[inline]
338    pub fn rotation(mut self, rotation: TimeDimension) -> Self {
339        self.rotation = rotation;
340        self
341    }
342
343    /// The encryption key, the public key in ECDH.
344    ///
345    /// It is used to negotiate the key for symmetric encryption of the log.
346    /// If the value is `None`, there is no encryption.
347    ///
348    /// The default value is `None`.
349    #[inline]
350    pub fn key(mut self, key: Option<PublicKey>) -> Self {
351        self.key = key;
352        self
353    }
354
355    /// The encryption key, the public key in ECDH, represented in `Base64`.
356    ///
357    /// It is used to negotiate the key for symmetric encryption of the log.
358    /// If the value is `None` or invalid, there is no encryption.
359    ///
360    /// The default value is `None`.
361    #[inline]
362    pub fn key_str(self, key: Option<impl AsRef<[u8]>>) -> Self {
363        let key = key.and_then(|k| BASE64_STANDARD.decode(k).ok()).and_then(|k| k.try_into().ok());
364        self.key(key)
365    }
366
367    /// The compression level.
368    ///
369    /// Pinenut uses `zstd` as the compression algorithm, which supports compression
370    /// levels from 1 up to 22, it also offers negative compression levels.
371    ///
372    /// As the `std`'s documentation says: The lower the level, the faster the
373    /// speed (at the cost of compression).
374    ///
375    /// The default value is `10`.
376    #[inline]
377    pub fn compression_level(mut self, level: i32) -> Self {
378        self.compression_level = level;
379        self
380    }
381
382    /// The tracker used to track errors occurred from the logger operations.
383    ///
384    /// Errors are printed to standard output by default.
385    #[inline]
386    pub fn tracker(mut self, tracker: Option<Tracker>) -> Self {
387        self.tracker = tracker;
388        self
389    }
390
391    /// Obtains a logger with a specified domain.
392    #[inline]
393    pub fn logger(self, domain: Domain) -> Logger {
394        Logger::new(domain, self)
395    }
396}
397
398impl Default for Config {
399    #[inline]
400    fn default() -> Self {
401        Self {
402            use_mmap: true,
403            buffer_len: BUFFER_LEN,
404            rotation: TimeDimension::Minute,
405            key: None,
406            compression_level: ZstdCompressor::DEFAULT_LEVEL,
407            tracker: Some(Box::new(|err, file, line| {
408                println!("[Pinenut Error] {file}:{line} | {err}")
409            })),
410        }
411    }
412}