@@ -31,11 +31,14 @@ use std::{
31
31
fmt:: { self , Debug } ,
32
32
fs:: { self , File , OpenOptions } ,
33
33
io:: { self , Write } ,
34
- path:: Path ,
34
+ path:: { Path , PathBuf } ,
35
35
sync:: atomic:: { AtomicUsize , Ordering } ,
36
36
} ;
37
37
use time:: { format_description, Duration , OffsetDateTime , Time } ;
38
38
39
+ mod builder;
40
+ pub use builder:: { Builder , InitError } ;
41
+
39
42
/// A file appender with the ability to rotate log files at a fixed schedule.
40
43
///
41
44
/// `RollingFileAppender` implements the [`std:io::Write` trait][write] and will
@@ -98,8 +101,8 @@ pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
98
101
99
102
#[ derive( Debug ) ]
100
103
struct Inner {
101
- log_directory : String ,
102
- log_filename_prefix : String ,
104
+ log_directory : PathBuf ,
105
+ log_filename_prefix : Option < String > ,
103
106
rotation : Rotation ,
104
107
next_date : AtomicUsize ,
105
108
}
@@ -122,8 +125,10 @@ impl RollingFileAppender {
122
125
/// - [`Rotation::daily()`][daily],
123
126
/// - [`Rotation::never()`][never()]
124
127
///
128
+ /// Additional parameters can be configured using [`RollingFileAppender::builder`].
125
129
///
126
130
/// # Examples
131
+ ///
127
132
/// ```rust
128
133
/// # fn docs() {
129
134
/// use tracing_appender::rolling::{RollingFileAppender, Rotation};
@@ -133,16 +138,63 @@ impl RollingFileAppender {
133
138
pub fn new (
134
139
rotation : Rotation ,
135
140
directory : impl AsRef < Path > ,
136
- file_name_prefix : impl AsRef < Path > ,
141
+ filename_prefix : impl AsRef < Path > ,
137
142
) -> 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 ( ) ;
138
190
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 {
141
193
state,
142
194
writer,
143
195
#[ cfg( test) ]
144
196
now : Box :: new ( OffsetDateTime :: now_utc) ,
145
- }
197
+ } )
146
198
}
147
199
148
200
#[ inline]
@@ -428,35 +480,42 @@ impl Rotation {
428
480
}
429
481
}
430
482
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 {
433
485
Rotation :: MINUTELY => {
434
486
let format = format_description:: parse ( "[year]-[month]-[day]-[hour]-[minute]" )
435
487
. 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" )
441
490
}
442
491
Rotation :: HOURLY => {
443
492
let format = format_description:: parse ( "[year]-[month]-[day]-[hour]" )
444
493
. 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" )
450
496
}
451
497
Rotation :: DAILY => {
452
498
let format = format_description:: parse ( "[year]-[month]-[day]" )
453
499
. 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" )
458
514
}
459
- Rotation :: NEVER => filename. to_string ( ) ,
515
+ } ;
516
+ match filename {
517
+ Some ( filename) => format ! ( "{}.{}" , filename, date) ,
518
+ None => date,
460
519
}
461
520
}
462
521
}
@@ -480,32 +539,30 @@ impl Inner {
480
539
now : OffsetDateTime ,
481
540
rotation : Rotation ,
482
541
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) ;
489
546
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) ?) ;
493
548
494
549
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,
497
552
next_date : AtomicUsize :: new (
498
553
next_date
499
554
. map ( |date| date. unix_timestamp ( ) as usize )
500
555
. unwrap_or ( 0 ) ,
501
556
) ,
502
557
rotation,
503
558
} ;
504
- ( inner, writer)
559
+ Ok ( ( inner, writer) )
505
560
}
506
561
507
562
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) ;
509
566
510
567
match create_writer ( & self . log_directory , & filename) {
511
568
Ok ( new_file) => {
@@ -552,20 +609,22 @@ impl Inner {
552
609
}
553
610
}
554
611
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) ;
557
614
let mut open_options = OpenOptions :: new ( ) ;
558
615
open_options. append ( true ) . create ( true ) ;
559
616
560
617
let new_file = open_options. open ( path. as_path ( ) ) ;
561
618
if new_file. is_err ( ) {
562
619
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" ) ) ;
565
624
}
566
625
}
567
626
568
- new_file
627
+ new_file. map_err ( InitError :: ctx ( "failed to create initial log file" ) )
569
628
}
570
629
571
630
#[ cfg( test) ]
@@ -673,19 +732,19 @@ mod test {
673
732
let now = OffsetDateTime :: parse ( "2020-02-01 10:01:00 +00:00:00" , & format) . unwrap ( ) ;
674
733
675
734
// per-minute
676
- let path = Rotation :: MINUTELY . join_date ( "app.log" , & now) ;
735
+ let path = Rotation :: MINUTELY . join_date ( Some ( "app.log" ) , & now) ;
677
736
assert_eq ! ( "app.log.2020-02-01-10-01" , path) ;
678
737
679
738
// per-hour
680
- let path = Rotation :: HOURLY . join_date ( "app.log" , & now) ;
739
+ let path = Rotation :: HOURLY . join_date ( Some ( "app.log" ) , & now) ;
681
740
assert_eq ! ( "app.log.2020-02-01-10" , path) ;
682
741
683
742
// per-day
684
- let path = Rotation :: DAILY . join_date ( "app.log" , & now) ;
743
+ let path = Rotation :: DAILY . join_date ( Some ( "app.log" ) , & now) ;
685
744
assert_eq ! ( "app.log.2020-02-01" , path) ;
686
745
687
746
// never
688
- let path = Rotation :: NEVER . join_date ( "app.log" , & now) ;
747
+ let path = Rotation :: NEVER . join_date ( Some ( "app.log" ) , & now) ;
689
748
assert_eq ! ( "app.log" , path) ;
690
749
}
691
750
@@ -702,8 +761,13 @@ mod test {
702
761
703
762
let now = OffsetDateTime :: parse ( "2020-02-01 10:01:00 +00:00:00" , & format) . unwrap ( ) ;
704
763
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 ( ) ;
707
771
708
772
let clock = Arc :: new ( Mutex :: new ( now) ) ;
709
773
let now = {
0 commit comments