Skip to main content

loglite/
lib.rs

1//! # loglite
2//! Simple and lite logging facility with different log levels and only few dependencies
3
4mod global;
5pub use global::*;
6pub use syslog_client::syslog::Facility;
7
8use chrono::prelude::*;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13use std::fmt::Debug;
14use std::fs::File;
15use std::io::{self, BufWriter, Write};
16use std::os::unix::fs::PermissionsExt;
17use std::path::Path;
18use std::sync::{Arc, Mutex};
19
20use syslog_client::syslog::header::*;
21use syslog_client::writer::transport::*;
22use syslog_client::*;
23
24/// Logging Level
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
27pub enum LogLevel {
28	Debug,
29	Info,
30	Warning,
31	Error,
32	Critical,
33}
34
35pub trait LogOutput: Debug + Write + Send + Sync {}
36impl<T: Debug + Write + Send + Sync> LogOutput for T {}
37
38pub trait Logging: Send + Sync {
39	/// Default output fn for the log, which has to be implemented
40	fn output(&self, level: LogLevel, text: &str) -> Result<(), io::Error>;
41
42	/// Flushes any remaining data to the file
43	fn flush(&self) -> Result<(), io::Error>;
44
45	/// Writes out a message as [LogLevel::Error]
46	fn error(&self, text: &str) -> Result<(), io::Error> {
47		self.output(LogLevel::Error, text)
48	}
49
50	/// Writes out a message as [LogLevel::Warning]
51	fn warning(&self, text: &str) -> Result<(), io::Error> {
52		self.output(LogLevel::Warning, text)
53	}
54
55	/// Writes out a message as [LogLevel::Info]
56	fn info(&self, text: &str) -> Result<(), io::Error> {
57		self.output(LogLevel::Info, text)
58	}
59
60	/// Writes out a message as [LogLevel::Debug]
61	fn debug(&self, text: &str) -> Result<(), io::Error> {
62		self.output(LogLevel::Debug, text)
63	}
64
65	/// Writes out a message as [LogLevel::Critical]
66	fn critical(&self, text: &str) -> Result<(), io::Error> {
67		self.output(LogLevel::Critical, text)
68	}
69}
70
71/// Creates a new instance of [Logging] which voids the data written to
72/// Comparable to [io::Sink]
73#[derive(Default)]
74pub struct VoidLog {}
75
76impl Logging for VoidLog {
77	fn output(&self, _: LogLevel, _: &str) -> Result<(), io::Error> {
78		Ok(())
79	}
80
81	fn flush(&self) -> Result<(), io::Error> {
82		Ok(())
83	}
84}
85
86// syslog protocol
87pub struct SysLog {
88	writer: Mutex<Rfc3164BufferedLogger<Unix<'static>>>,
89	min_level: LogLevel,
90}
91
92impl SysLog {
93	const MAPPING: [Severity; 5] = [
94		Severity::LOG_DEBUG,
95		Severity::LOG_INFO,
96		Severity::LOG_WARNING,
97		Severity::LOG_ERR,
98		Severity::LOG_CRIT,
99	];
100	/// Creates a new log from any type implementing [Write] and [Debug]
101	pub fn new(facility: Facility, tag: &str, min_logging_level: LogLevel) -> Option<Self> {
102		let hostname = hostname::get().unwrap().into_string().ok()?;
103		let syslog = Syslog::new(
104			facility,
105			Hostname::new(&hostname)?,
106			Tag::new(tag)?,
107		);
108		Some(Self {
109			min_level: min_logging_level,
110			writer: Mutex::new(
111				Rfc3164Logger::new(syslog, Unix::new_system()?).with_buffer(),
112			),
113		})
114	}
115}
116
117impl Logging for SysLog {
118	fn output(&self, level: LogLevel, text: &str) -> Result<(), io::Error> {
119		if level < self.min_level {
120			return Ok(());
121		}
122		let writer = &mut *self.writer.lock().unwrap();
123		writer.write_str(Self::MAPPING[level as usize], text)
124	}
125
126	fn flush(&self) -> Result<(), io::Error> {
127		Ok(())
128	}
129}
130
131pub struct GenericLog {
132	file: Arc<Mutex<dyn LogOutput>>,
133	timestamp: bool,
134	min_logging_level: LogLevel,
135}
136
137impl GenericLog {
138	/// Creates a new log from any type implementing [Write] and [Debug]
139	pub fn from_writer<T: LogOutput + 'static>(
140		writer: T,
141		timestamp: bool,
142		min_logging_level: LogLevel,
143	) -> Self {
144		Self {
145			file: Arc::new(Mutex::new(writer)),
146			timestamp,
147			min_logging_level,
148		}
149	}
150}
151
152impl Logging for GenericLog {
153	fn output(&self, level: LogLevel, text: &str) -> Result<(), io::Error> {
154		if level < self.min_logging_level {
155			return Ok(());
156		}
157		let writer = &mut *self.file.lock().unwrap();
158		match self.timestamp {
159			true => writer.write_all(
160				format!(
161					"({}) [{level:?}]: {text}\n",
162					Utc::now().format("%Y.%m.%d-%H:%M:%S")
163				)
164				.as_bytes(),
165			)?,
166			false => writer.write_all(format!("[{level:?}]: {text}\n").as_bytes())?,
167		};
168		Ok(())
169	}
170
171	fn flush(&self) -> Result<(), io::Error> {
172		(*self.file.lock().unwrap()).flush()
173	}
174}
175
176impl Drop for GenericLog {
177	fn drop(&mut self) {
178		self.flush()
179			.map_err(|e| eprintln!("Error flushing log file! {e}"))
180			.ok();
181	}
182}
183
184/// Log instance
185pub struct FileLog {
186	file: Mutex<BufWriter<File>>,
187	timestamp: bool,
188	min_logging_level: LogLevel,
189}
190
191impl FileLog {
192	#[cfg(target_family = "unix")]
193	const FILE_MODE: u32 = 0o664;
194
195	/// Creates a new [Logging] instance with a given path to write to
196	/// `timestamp`: determins whether or not to write the current timestamp into the log
197	pub fn new<P: AsRef<Path> + ?Sized>(
198		file: &P,
199		timestamp: bool,
200		min_logging_level: LogLevel,
201	) -> Result<Self, io::Error> {
202		let fd = Self::get_file(file).map_err(|e| {
203			io::Error::new(
204				e.kind(),
205				format!(
206					"Logfile '{}' couldn't be opened/created.",
207					file.as_ref().display()
208				),
209			)
210		})?;
211		Ok(Self {
212			file: Mutex::new(BufWriter::new(fd)),
213			timestamp,
214			min_logging_level,
215		})
216	}
217
218	fn get_file<P: AsRef<Path> + ?Sized>(file: &P) -> Result<File, io::Error> {
219		match file.as_ref().exists() {
220			true => std::fs::OpenOptions::new()
221				.append(true)
222				.read(false)
223				.open(file),
224			false => {
225				let f = File::create(file)?;
226				#[cfg(target_family = "unix")]
227				Self::set_perms(file)?;
228				Ok(f)
229			}
230		}
231	}
232
233	#[cfg(target_family = "unix")]
234	fn set_perms<P: AsRef<Path> + ?Sized>(path: &P) -> Result<(), io::Error> {
235		let perms = PermissionsExt::from_mode(Self::FILE_MODE);
236		std::fs::set_permissions(path, perms)
237	}
238
239	/// Writes out a custom message without any [LogLevel]
240	pub fn custom<S: AsRef<str>>(&self, text: S) -> Result<(), io::Error> {
241		let writer = &mut *self.file.lock().unwrap();
242		writer.write_all(text.as_ref().as_bytes())
243	}
244}
245
246impl Logging for FileLog {
247	fn output(&self, level: LogLevel, text: &str) -> Result<(), io::Error> {
248		if level < self.min_logging_level {
249			return Ok(());
250		}
251		let writer = &mut *self.file.lock().unwrap();
252		match self.timestamp {
253			true => writer.write_all(
254				format!(
255					"({}) [{level:?}]: {text}\n",
256					Utc::now().format("%Y.%m.%d-%H:%M:%S")
257				)
258				.as_bytes(),
259			)?,
260			false => writer.write_all(format!("[{level:?}]: {text}\n").as_bytes())?,
261		}
262		Ok(())
263	}
264
265	fn flush(&self) -> Result<(), io::Error> {
266		(*self.file.lock().unwrap()).flush()
267	}
268}
269
270impl Drop for FileLog {
271	fn drop(&mut self) {
272		self.flush()
273			.map_err(|e| eprintln!("Error flushing log file! {e}"))
274			.ok();
275	}
276}
277
278#[cfg(test)]
279mod test {
280	use crate::LogLevel;
281	#[test]
282	fn check_level() {
283		assert!(LogLevel::Info < LogLevel::Warning);
284		assert!(LogLevel::Error < LogLevel::Critical);
285		assert!(LogLevel::Error > LogLevel::Warning);
286		assert!(LogLevel::Info > LogLevel::Debug);
287	}
288}