@@ -31,11 +31,14 @@ use std::{
3131 fmt:: { self , Debug } ,
3232 fs:: { self , File , OpenOptions } ,
3333 io:: { self , Write } ,
34- path:: Path ,
34+ path:: { Path , PathBuf } ,
3535 sync:: atomic:: { AtomicUsize , Ordering } ,
3636} ;
3737use time:: { format_description, Duration , OffsetDateTime , Time } ;
3838
39+ mod builder;
40+ pub use builder:: { Builder , InitError } ;
41+
3942/// A file appender with the ability to rotate log files at a fixed schedule.
4043///
4144/// `RollingFileAppender` implements the [`std:io::Write` trait][write] and will
@@ -98,8 +101,8 @@ pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
98101
99102#[ derive( Debug ) ]
100103struct Inner {
101- log_directory : String ,
102- log_filename_prefix : String ,
104+ log_directory : PathBuf ,
105+ log_filename_prefix : Option < String > ,
103106 rotation : Rotation ,
104107 next_date : AtomicUsize ,
105108}
@@ -122,8 +125,10 @@ impl RollingFileAppender {
122125 /// - [`Rotation::daily()`][daily],
123126 /// - [`Rotation::never()`][never()]
124127 ///
128+ /// Additional parameters can be configured using [`RollingFileAppender::builder`].
125129 ///
126130 /// # Examples
131+ ///
127132 /// ```rust
128133 /// # fn docs() {
129134 /// use tracing_appender::rolling::{RollingFileAppender, Rotation};
@@ -133,16 +138,63 @@ impl RollingFileAppender {
133138 pub fn new (
134139 rotation : Rotation ,
135140 directory : impl AsRef < Path > ,
136- file_name_prefix : impl AsRef < Path > ,
141+ filename_prefix : impl AsRef < Path > ,
137142 ) -> RollingFileAppender {
143+ let filename_prefix = filename_prefix
144+ . as_ref ( )
145+ . to_str ( )
146+ . expect ( "filename prefix must be a valid UTF-8 string" ) ;
147+ Self :: builder ( )
148+ . rotation ( rotation)
149+ . filename_prefix ( filename_prefix)
150+ . build ( directory)
151+ . expect ( "initializing rolling file appender failed" )
152+ }
153+
154+ /// Returns a new [`Builder`] for configuring a `RollingFileAppender`.
155+ ///
156+ /// The builder interface can be used to set additional configuration
157+ /// parameters when constructing a new appender.
158+ ///
159+ /// Unlike [`RollingFileAppender::new`], the [`Builder::build`] method
160+ /// returns a `Result` rather than panicking when the appender cannot be
161+ /// initialized. Therefore, the builder interface can also be used when
162+ /// appender initialization errors should be handled gracefully.
163+ ///
164+ /// # Examples
165+ ///
166+ /// ```rust
167+ /// # fn docs() {
168+ /// use tracing_appender::rolling::{RollingFileAppender, Rotation};
169+ ///
170+ /// let file_appender = RollingFileAppender::builder()
171+ /// .rotation(Rotation::HOURLY) // rotate log files once every hour
172+ /// .filename_prefix("myapp") // log file names will be prefixed with `myapp.`
173+ /// .build("/var/log") // try to build an appender that stores log files in `/var/log`
174+ /// .expect("initializing rolling file appender failed");
175+ /// # drop(file_appender);
176+ /// # }
177+ /// ```
178+ #[ must_use]
179+ pub fn builder ( ) -> Builder {
180+ Builder :: new ( )
181+ }
182+
183+ fn from_builder ( builder : & Builder , directory : impl AsRef < Path > ) -> Result < Self , InitError > {
184+ let Builder {
185+ ref rotation,
186+ ref prefix,
187+ } = builder;
188+ let filename_prefix = prefix. clone ( ) ;
189+ let directory = directory. as_ref ( ) . to_path_buf ( ) ;
138190 let now = OffsetDateTime :: now_utc ( ) ;
139- let ( state, writer) = Inner :: new ( now, rotation, directory, file_name_prefix ) ;
140- Self {
191+ let ( state, writer) = Inner :: new ( now, rotation. clone ( ) , directory, filename_prefix ) ? ;
192+ Ok ( Self {
141193 state,
142194 writer,
143195 #[ cfg( test) ]
144196 now : Box :: new ( OffsetDateTime :: now_utc) ,
145- }
197+ } )
146198 }
147199
148200 #[ inline]
@@ -428,35 +480,42 @@ impl Rotation {
428480 }
429481 }
430482
431- pub ( crate ) fn join_date ( & self , filename : & str , date : & OffsetDateTime ) -> String {
432- match * self {
483+ pub ( crate ) fn join_date ( & self , filename : Option < & str > , date : & OffsetDateTime ) -> String {
484+ let date = match * self {
433485 Rotation :: MINUTELY => {
434486 let format = format_description:: parse ( "[year]-[month]-[day]-[hour]-[minute]" )
435487 . expect ( "Unable to create a formatter; this is a bug in tracing-appender" ) ;
436-
437- let date = date
438- . format ( & format)
439- . expect ( "Unable to format OffsetDateTime; this is a bug in tracing-appender" ) ;
440- format ! ( "{}.{}" , filename, date)
488+ date. format ( & format)
489+ . expect ( "Unable to format OffsetDateTime; this is a bug in tracing-appender" )
441490 }
442491 Rotation :: HOURLY => {
443492 let format = format_description:: parse ( "[year]-[month]-[day]-[hour]" )
444493 . expect ( "Unable to create a formatter; this is a bug in tracing-appender" ) ;
445-
446- let date = date
447- . format ( & format)
448- . expect ( "Unable to format OffsetDateTime; this is a bug in tracing-appender" ) ;
449- format ! ( "{}.{}" , filename, date)
494+ date. format ( & format)
495+ . expect ( "Unable to format OffsetDateTime; this is a bug in tracing-appender" )
450496 }
451497 Rotation :: DAILY => {
452498 let format = format_description:: parse ( "[year]-[month]-[day]" )
453499 . expect ( "Unable to create a formatter; this is a bug in tracing-appender" ) ;
454- let date = date
455- . format ( & format)
456- . expect ( "Unable to format OffsetDateTime; this is a bug in tracing-appender" ) ;
457- format ! ( "{}.{}" , filename, date)
500+ date. format ( & format)
501+ . expect ( "Unable to format OffsetDateTime; this is a bug in tracing-appender" )
502+ }
503+ Rotation :: NEVER => {
504+ // If there's a name prefix, use that.
505+ if let Some ( filename) = filename {
506+ return filename. to_owned ( ) ;
507+ }
508+
509+ // Otherwise, just use the date.
510+ let format = format_description:: parse ( "[year]-[month]-[day]" )
511+ . expect ( "Unable to create a formatter; this is a bug in tracing-appender" ) ;
512+ date. format ( & format)
513+ . expect ( "Unable to format OffsetDateTime; this is a bug in tracing-appender" )
458514 }
459- Rotation :: NEVER => filename. to_string ( ) ,
515+ } ;
516+ match filename {
517+ Some ( filename) => format ! ( "{}.{}" , filename, date) ,
518+ None => date,
460519 }
461520 }
462521}
@@ -480,32 +539,30 @@ impl Inner {
480539 now : OffsetDateTime ,
481540 rotation : Rotation ,
482541 directory : impl AsRef < Path > ,
483- file_name_prefix : impl AsRef < Path > ,
484- ) -> ( Self , RwLock < File > ) {
485- let log_directory = directory. as_ref ( ) . to_str ( ) . unwrap ( ) ;
486- let log_filename_prefix = file_name_prefix. as_ref ( ) . to_str ( ) . unwrap ( ) ;
487-
488- let filename = rotation. join_date ( log_filename_prefix, & now) ;
542+ log_filename_prefix : Option < String > ,
543+ ) -> Result < ( Self , RwLock < File > ) , builder:: InitError > {
544+ let log_directory = directory. as_ref ( ) . to_path_buf ( ) ;
545+ let filename = rotation. join_date ( log_filename_prefix. as_deref ( ) , & now) ;
489546 let next_date = rotation. next_date ( & now) ;
490- let writer = RwLock :: new (
491- create_writer ( log_directory, & filename) . expect ( "failed to create appender" ) ,
492- ) ;
547+ let writer = RwLock :: new ( create_writer ( log_directory. as_ref ( ) , & filename) ?) ;
493548
494549 let inner = Inner {
495- log_directory : log_directory . to_string ( ) ,
496- log_filename_prefix : log_filename_prefix . to_string ( ) ,
550+ log_directory,
551+ log_filename_prefix,
497552 next_date : AtomicUsize :: new (
498553 next_date
499554 . map ( |date| date. unix_timestamp ( ) as usize )
500555 . unwrap_or ( 0 ) ,
501556 ) ,
502557 rotation,
503558 } ;
504- ( inner, writer)
559+ Ok ( ( inner, writer) )
505560 }
506561
507562 fn refresh_writer ( & self , now : OffsetDateTime , file : & mut File ) {
508- let filename = self . rotation . join_date ( & self . log_filename_prefix , & now) ;
563+ let filename = self
564+ . rotation
565+ . join_date ( self . log_filename_prefix . as_deref ( ) , & now) ;
509566
510567 match create_writer ( & self . log_directory , & filename) {
511568 Ok ( new_file) => {
@@ -552,20 +609,22 @@ impl Inner {
552609 }
553610}
554611
555- fn create_writer ( directory : & str , filename : & str ) -> io :: Result < File > {
556- let path = Path :: new ( directory) . join ( filename) ;
612+ fn create_writer ( directory : & Path , filename : & str ) -> Result < File , InitError > {
613+ let path = directory. join ( filename) ;
557614 let mut open_options = OpenOptions :: new ( ) ;
558615 open_options. append ( true ) . create ( true ) ;
559616
560617 let new_file = open_options. open ( path. as_path ( ) ) ;
561618 if new_file. is_err ( ) {
562619 if let Some ( parent) = path. parent ( ) {
563- fs:: create_dir_all ( parent) ?;
564- return open_options. open ( path) ;
620+ fs:: create_dir_all ( parent) . map_err ( InitError :: ctx ( "failed to create log directory" ) ) ?;
621+ return open_options
622+ . open ( path)
623+ . map_err ( InitError :: ctx ( "failed to create initial log file" ) ) ;
565624 }
566625 }
567626
568- new_file
627+ new_file. map_err ( InitError :: ctx ( "failed to create initial log file" ) )
569628}
570629
571630#[ cfg( test) ]
@@ -673,19 +732,19 @@ mod test {
673732 let now = OffsetDateTime :: parse ( "2020-02-01 10:01:00 +00:00:00" , & format) . unwrap ( ) ;
674733
675734 // per-minute
676- let path = Rotation :: MINUTELY . join_date ( "app.log" , & now) ;
735+ let path = Rotation :: MINUTELY . join_date ( Some ( "app.log" ) , & now) ;
677736 assert_eq ! ( "app.log.2020-02-01-10-01" , path) ;
678737
679738 // per-hour
680- let path = Rotation :: HOURLY . join_date ( "app.log" , & now) ;
739+ let path = Rotation :: HOURLY . join_date ( Some ( "app.log" ) , & now) ;
681740 assert_eq ! ( "app.log.2020-02-01-10" , path) ;
682741
683742 // per-day
684- let path = Rotation :: DAILY . join_date ( "app.log" , & now) ;
743+ let path = Rotation :: DAILY . join_date ( Some ( "app.log" ) , & now) ;
685744 assert_eq ! ( "app.log.2020-02-01" , path) ;
686745
687746 // never
688- let path = Rotation :: NEVER . join_date ( "app.log" , & now) ;
747+ let path = Rotation :: NEVER . join_date ( Some ( "app.log" ) , & now) ;
689748 assert_eq ! ( "app.log" , path) ;
690749 }
691750
@@ -702,8 +761,13 @@ mod test {
702761
703762 let now = OffsetDateTime :: parse ( "2020-02-01 10:01:00 +00:00:00" , & format) . unwrap ( ) ;
704763 let directory = tempfile:: tempdir ( ) . expect ( "failed to create tempdir" ) ;
705- let ( state, writer) =
706- Inner :: new ( now, Rotation :: HOURLY , directory. path ( ) , "test_make_writer" ) ;
764+ let ( state, writer) = Inner :: new (
765+ now,
766+ Rotation :: HOURLY ,
767+ directory. path ( ) ,
768+ Some ( "test_make_writer" . to_string ( ) ) ,
769+ )
770+ . unwrap ( ) ;
707771
708772 let clock = Arc :: new ( Mutex :: new ( now) ) ;
709773 let now = {
0 commit comments