1use super::error::Error;
2use crate::logging::config::LogConfig;
3use file_rotate::suffix::{AppendTimestamp, FileLimit};
4use file_rotate::{ContentLimit, FileRotate, TimeFrequency};
5use reopen::Reopen;
6use std::fmt::{Debug, Display};
7use std::fs::{File, OpenOptions};
8use std::io::Write;
9use std::os::unix::prelude::*;
10use std::path::{Path, PathBuf};
11#[cfg(test)]
12use std::str::from_utf8;
13use tracing_subscriber::fmt::writer::TestWriter;
14
15use serde::de::Visitor;
16use serde::{de, Deserialize, Deserializer, Serialize};
17use std::sync::{Arc, Mutex};
18use std::{fmt, fs, path};
19
20#[derive(Default, Serialize, Clone, Debug)]
22pub enum LogDestination {
23 StdErr,
25 #[default]
26 StdOut,
28 TestWriter,
30 File(PathBuf),
32 #[cfg(test)]
34 Bytes,
35}
36
37impl Display for LogDestination {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 LogDestination::StdErr => write!(f, "stderr"),
41 LogDestination::StdOut => write!(f, "stdout"),
42 LogDestination::TestWriter => write!(f, "testwriter"),
43 LogDestination::File(path) => write!(f, "{}", path.to_string_lossy()),
44 #[cfg(test)]
45 LogDestination::Bytes => write!(f, "bytes"),
46 }
47 }
48}
49
50impl From<&str> for LogDestination {
52 fn from(value: &str) -> Self {
53 match value {
54 "stdout" => LogDestination::StdOut,
55 "stderr" => LogDestination::StdErr,
56 "testwriter" => LogDestination::TestWriter,
57 #[cfg(test)]
58 "bytes" => LogDestination::Bytes,
59 file_path => LogDestination::File(PathBuf::from(file_path)),
60 }
61 }
62}
63
64struct LogDestinationVisitor;
65
66impl Visitor<'_> for LogDestinationVisitor {
67 type Value = LogDestination;
68
69 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
70 formatter.write_str("stdout, stderr, or absolute path to a file log destination")
71 }
72
73 fn visit_str<E>(self, value: &str) -> Result<LogDestination, E>
74 where
75 E: de::Error,
76 {
77 let destination = LogDestination::from(value);
78
79 Ok(destination)
80 }
81}
82
83impl<'de> Deserialize<'de> for LogDestination {
84 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
85 where
86 D: Deserializer<'de>,
87 {
88 deserializer.deserialize_str(LogDestinationVisitor)
89 }
90}
91
92pub(super) struct Writer<T>(Arc<Mutex<T>>)
96where
97 T: Write;
98
99impl<T> Write for Writer<T>
100where
101 T: Write,
102{
103 delegate::delegate! {
104 to self.0.lock().expect("logging mutex poisoned") {
105 #[inline]
106 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize>;
107
108 #[inline]
109 fn flush(&mut self) -> std::io::Result<()> ;
110
111 #[inline]
112 fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize>;
113
114 #[inline]
115 fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()>;
116
117 #[inline]
118 fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()>;
119 }
120 }
121}
122
123impl<T> Writer<T>
124where
125 T: Write,
126{
127 pub(super) fn new(write: Arc<Mutex<T>>) -> Self {
129 Self(write)
130 }
131}
132
133impl<T: Write> From<T> for Writer<T> {
134 fn from(value: T) -> Self {
135 Self(Arc::new(Mutex::new(value)))
136 }
137}
138
139impl<T: Write> Clone for Writer<T> {
140 fn clone(&self) -> Self {
141 Self(self.0.clone())
142 }
143}
144
145pub(super) enum LogDestinationWriter {
147 StdErr,
149 StdOut,
151 TestWriter,
153 File(Writer<Reopen<File>>),
155 RotationWriter(Writer<FileRotate<AppendTimestamp>>),
157 #[cfg(test)]
158 Bytes(Writer<Vec<u8>>),
160}
161
162impl LogDestinationWriter {
163 pub fn to_write(&self) -> Box<dyn Write> {
165 match self {
166 LogDestinationWriter::StdErr => Box::new(std::io::stderr()) as Box<dyn Write>,
167 LogDestinationWriter::StdOut => Box::new(std::io::stdout()) as Box<dyn Write>,
168 LogDestinationWriter::TestWriter => Box::new(TestWriter::new()) as Box<dyn Write>,
169 LogDestinationWriter::File(f) => Box::new(f.clone()) as Box<dyn Write>,
170 LogDestinationWriter::RotationWriter(w) => Box::new(w.clone()) as Box<dyn Write>,
171 #[cfg(test)]
172 LogDestinationWriter::Bytes(w) => Box::new(w.clone()) as Box<dyn Write>,
173 }
174 }
175
176 pub fn name(&self) -> String {
178 let s = match self {
179 LogDestinationWriter::StdOut => "stdout",
180 LogDestinationWriter::StdErr => "stderr",
181 LogDestinationWriter::TestWriter => "testwriter",
182 LogDestinationWriter::File(_) => "file",
183 LogDestinationWriter::RotationWriter(_) => "RotationWriter",
184 #[cfg(test)]
185 LogDestinationWriter::Bytes(_) => "ByteBuffer",
186 };
187
188 String::from(s)
189 }
190
191 pub fn rotate(&self) -> Result<(), Error> {
193 match self {
194 LogDestinationWriter::RotationWriter(ref writer) => {
195 let mut file_rotate_guard = writer.0.lock().expect("logging lock poisoned");
196
197 file_rotate_guard.rotate().map_err(Error::FileRotate)?;
198 }
199 LogDestinationWriter::File(ref f) => {
200 let mut file_reopen_guard = f.0.lock().expect("logging lock poisoned");
201
202 file_reopen_guard
203 .flush()
204 .map_err(Error::FileRotate)
205 .map(|_| {
206 file_reopen_guard.handle().reopen();
207 })?
208 }
209 _ => {}
210 };
211
212 Ok(())
213 }
214
215 #[cfg(test)]
221 pub fn read_as_string(&self) -> String {
222 match self {
223 LogDestinationWriter::Bytes(b) => {
224 let guard = b.0.lock().unwrap();
225 let b = guard.clone();
226
227 from_utf8(b.as_slice()).unwrap().to_string()
228 }
229 LogDestinationWriter::RotationWriter(w) => {
230 let paths = w.0.lock().unwrap().log_paths();
231 let path = paths.first().expect("exactly one path excepted");
232 fs::read_to_string(path).unwrap()
233 }
234 _ => todo!(),
235 }
236 }
237}
238
239impl TryFrom<&LogConfig> for FileRotate<AppendTimestamp> {
240 type Error = Error;
241 fn try_from(config: &LogConfig) -> Result<Self, Self::Error> {
245 let suffix_scheme = AppendTimestamp::default(FileLimit::MaxFiles(config.max_files));
246 let content_limit = ContentLimit::Time(TimeFrequency::Daily);
247 let compression = file_rotate::compression::Compression::OnRotate(2);
248 let mode = Some(0o600);
250 let path = PathBuf::from(&config.destination.to_string());
251
252 let f = FileRotate::new(path, suffix_scheme, content_limit, compression, mode);
253
254 Ok(f)
255 }
256}
257
258impl TryFrom<&LogConfig> for Reopen<File> {
259 type Error = Error;
260 fn try_from(config: &LogConfig) -> Result<Self, Self::Error> {
262 let mode = 0o600;
264 let path = PathBuf::from(&config.destination.to_string());
265
266 let opened_path = path.clone();
267 let open_fn = move || {
268 OpenOptions::new()
269 .create(true)
270 .append(true)
271 .mode(mode)
272 .open(&opened_path)
273 };
274
275 Reopen::new(Box::new(open_fn)).map_err(|e| Error::FileCreate(path, e))
276 }
277}
278
279impl TryFrom<&LogConfig> for LogDestinationWriter {
280 type Error = Error;
281
282 fn try_from(value: &LogConfig) -> Result<Self, Self::Error> {
283 let destination = match &value.destination {
284 LogDestination::StdOut => LogDestinationWriter::StdOut,
285 LogDestination::StdErr => LogDestinationWriter::StdErr,
286 LogDestination::TestWriter => LogDestinationWriter::TestWriter,
287 #[cfg(test)]
288 LogDestination::Bytes => LogDestinationWriter::Bytes(Vec::<u8>::new().into()),
289 LogDestination::File(path_string) => {
290 let path = PathBuf::from(path_string);
291
292 validate_log_path(path)?;
293
294 if value.max_files > 0 {
295 let file: FileRotate<AppendTimestamp> = FileRotate::try_from(value)?;
296 LogDestinationWriter::RotationWriter(file.into())
297 } else {
298 let file: Reopen<File> = value.try_into()?;
299 LogDestinationWriter::File(file.into())
300 }
301 }
302 };
303
304 Ok(destination)
305 }
306}
307
308impl Debug for LogDestinationWriter {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 f.write_str(&self.name())
311 }
312}
313
314impl Write for LogDestinationWriter {
316 delegate::delegate! {
317 to self.to_write() {
318 #[inline]
319 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> ;
320
321 #[inline]
322 fn flush(&mut self) -> std::io::Result<()> ;
323
324 #[inline]
325 fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> ;
326
327 #[inline]
328 fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> ;
329
330 #[inline]
331 fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> ;
332 }
333 }
334}
335
336fn validate_log_path<T: AsRef<Path>>(log_file_path: T) -> Result<(), Error> {
344 let log_file_path = log_file_path.as_ref();
345
346 if !log_file_path.is_absolute() {
347 return Err(Error::FilePath(
348 log_file_path.to_owned(),
349 "log file path must be absolute".to_string(),
350 ));
351 }
352
353 if log_file_path.exists() {
354 if log_file_path.is_dir() {
356 return Err(Error::FilePath(
357 log_file_path.to_owned(),
358 "log file path must point to file".to_string(),
359 ));
360 }
361
362 let md = fs::metadata(log_file_path).map_err(|e| {
363 Error::FilePath(
364 log_file_path.to_owned(),
365 format!("cannot read log file metadata: {}", e),
366 )
367 })?;
368
369 if md.permissions().readonly() {
370 return Err(Error::FilePath(
371 log_file_path.to_owned(),
372 "log file is readonly".to_string(),
373 ));
374 }
375 } else if log_file_path.ends_with(String::from(path::MAIN_SEPARATOR)) {
376 return Err(Error::FilePath(
378 log_file_path.to_owned(),
379 "log file path must point to file".to_string(),
380 ));
381 }
382
383 let parent_dir = log_file_path
385 .parent()
386 .expect("absolute log file path will always have parent");
387
388 let md = fs::metadata(parent_dir).map_err(|e| {
389 Error::FilePath(
390 log_file_path.to_owned(),
391 format!("cannot read parent directory: {}", e),
392 )
393 })?;
394
395 let permissions = md.permissions();
396 if permissions.readonly() {
397 return Err(Error::FilePath(
398 log_file_path.to_owned(),
399 "parent directory is readonly".to_string(),
400 ));
401 }
402
403 Ok(())
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 use std::fs;
411 use std::fs::OpenOptions;
412 use std::path::Path;
413 use tempfile::tempdir;
414
415 #[test]
416 fn test_validate_log_path_file_exists_but_readonly() {
417 let dir = tempdir().unwrap();
418 let file_path = dir.path().join("log.txt");
419 OpenOptions::new()
420 .write(true)
421 .create(true)
422 .open(&file_path)
423 .unwrap();
424 let mut perms = fs::metadata(&file_path).unwrap().permissions();
425 perms.set_mode(0o444);
426 fs::set_permissions(&file_path, perms).unwrap();
427
428 assert!(
429 matches!(validate_log_path(&file_path), Err(Error::FilePath(_, message)) if message == "log file is readonly")
430 );
431 }
432
433 #[test]
434 fn test_validate_log_path_parent_directory_not_writable() {
435 let dir = tempdir().unwrap();
436 let file_path = dir.path().join("log.txt");
437 let mut perms = fs::metadata(dir.path()).unwrap().permissions();
438 perms.set_mode(0o555);
439 fs::set_permissions(dir.path(), perms).unwrap();
440
441 assert!(
442 matches!(validate_log_path(file_path), Err(Error::FilePath(_, message)) if message == "parent directory is readonly")
443 );
444 }
445
446 #[test]
447 fn test_validate_log_path_points_to_directory() {
448 let dir = tempdir().unwrap();
449
450 assert!(
451 matches!(validate_log_path(dir.path()), Err(Error::FilePath(_, message)) if message == "log file path must point to file")
452 );
453 }
454
455 #[test]
456 fn test_validate_log_path_not_absolute() {
457 let relative_path = Path::new("log.txt");
458
459 assert!(
460 matches!(validate_log_path(relative_path), Err(Error::FilePath(_, message)) if message == "log file path must be absolute")
461 );
462 }
463
464 #[test]
465 fn test_validate_log_path_file_exists_and_writable() {
466 let dir = tempdir().unwrap();
467 let file_path = dir.path().join("log.txt");
468 OpenOptions::new()
469 .write(true)
470 .create(true)
471 .open(&file_path)
472 .unwrap();
473
474 assert!(validate_log_path(&file_path).is_ok());
475 }
476}