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}