@@ -151,3 +151,135 @@ impl Handler for ConsoleHandler {
151151 }
152152 }
153153}
154+
155+ /// A handler that writes newline-delimited JSON log records.
156+ /// Uses minimal manual escaping to avoid external dependencies.
157+ pub struct JsonHandler {
158+ inner : Mutex < io:: BufWriter < std:: fs:: File > > ,
159+ }
160+
161+ impl JsonHandler {
162+ pub fn new ( path : String ) -> Self {
163+ let file = OpenOptions :: new ( )
164+ . create ( true )
165+ . append ( true )
166+ . open ( & path)
167+ . expect ( "open json log file" ) ;
168+ Self {
169+ inner : Mutex :: new ( io:: BufWriter :: new ( file) ) ,
170+ }
171+ }
172+
173+ fn escape_json ( s : & str ) -> String {
174+ let mut out = String :: with_capacity ( s. len ( ) + 8 ) ;
175+ for ch in s. chars ( ) {
176+ match ch {
177+ '"' => out. push_str ( "\\ \" " ) ,
178+ '\\' => out. push_str ( "\\ \\ " ) ,
179+ '\n' => out. push_str ( "\\ n" ) ,
180+ '\r' => out. push_str ( "\\ r" ) ,
181+ '\t' => out. push_str ( "\\ t" ) ,
182+ c if c. is_control ( ) => {
183+ use std:: fmt:: Write as _;
184+ let _ = write ! ( out, "\\ u{:04x}" , c as u32 ) ;
185+ }
186+ c => out. push ( c) ,
187+ }
188+ }
189+ out
190+ }
191+ }
192+
193+ impl Handler for JsonHandler {
194+ fn log ( & self , record : & Record ) {
195+ let ts = record
196+ . timestamp
197+ . duration_since ( SystemTime :: UNIX_EPOCH )
198+ . unwrap ( )
199+ . as_millis ( ) ;
200+ let msg = Self :: escape_json ( record. message ) ;
201+ let target = Self :: escape_json ( record. target ) ;
202+ let module = record. module_path . unwrap_or ( "" ) ;
203+ let file = record. file . unwrap_or ( "" ) ;
204+ let line = record. line . unwrap_or ( 0 ) ;
205+ let level = match record. level {
206+ LogLevel :: TRACE => "TRACE" ,
207+ LogLevel :: DEBUG => "DEBUG" ,
208+ LogLevel :: INFO => "INFO" ,
209+ LogLevel :: WARN => "WARN" ,
210+ LogLevel :: ERROR => "ERROR" ,
211+ LogLevel :: FATAL => "FATAL" ,
212+ } ;
213+ let json = format ! (
214+ "{{\" ts\" :{},\" level\" :\" {}\" ,\" target\" :\" {}\" ,\" message\" :\" {}\" ,\" module\" :\" {}\" ,\" file\" :\" {}\" ,\" line\" :{}}}\n " ,
215+ ts, level, target, msg, module, file, line
216+ ) ;
217+ let mut w = self . inner . lock ( ) . unwrap ( ) ;
218+ let _ = w. write_all ( json. as_bytes ( ) ) ;
219+ let _ = w. flush ( ) ;
220+ }
221+ }
222+
223+ /// A handler that writes to a file and rotates when size exceeds `max_bytes`.
224+ pub struct RotatingFileHandler {
225+ path : String ,
226+ max_bytes : u64 ,
227+ backups : usize ,
228+ lock : Mutex < ( ) > ,
229+ }
230+
231+ impl RotatingFileHandler {
232+ pub fn new ( path : String , max_bytes : u64 , backups : usize ) -> Self {
233+ Self {
234+ path,
235+ max_bytes,
236+ backups,
237+ lock : Mutex :: new ( ( ) ) ,
238+ }
239+ }
240+
241+ fn rotate ( & self ) {
242+ // Rotate: file.(n-1) -> file.n, ..., file -> file.1, delete file.n if exists
243+ for i in ( 1 ..=self . backups ) . rev ( ) {
244+ let from = if i == 1 {
245+ std:: path:: PathBuf :: from ( & self . path )
246+ } else {
247+ std:: path:: PathBuf :: from ( format ! ( "{}.{}" , & self . path, i - 1 ) )
248+ } ;
249+ let to = std:: path:: PathBuf :: from ( format ! ( "{}.{}" , & self . path, i) ) ;
250+ if from. exists ( ) {
251+ let _ = std:: fs:: rename ( & from, & to) ;
252+ }
253+ }
254+ }
255+ }
256+
257+ impl Handler for RotatingFileHandler {
258+ fn log ( & self , record : & Record ) {
259+ let _guard = self . lock . lock ( ) . unwrap ( ) ;
260+
261+ // Check file size and rotate if needed
262+ if let Ok ( meta) = std:: fs:: metadata ( & self . path ) {
263+ if meta. len ( ) >= self . max_bytes {
264+ self . rotate ( ) ;
265+ }
266+ }
267+
268+ let timestamp = record
269+ . timestamp
270+ . duration_since ( SystemTime :: UNIX_EPOCH )
271+ . unwrap ( )
272+ . as_secs ( ) ;
273+ let line = format ! (
274+ "[{}]-[{:?}]-[{}]: {}\n " ,
275+ timestamp, record. level, record. target, record. message
276+ ) ;
277+
278+ let mut f = OpenOptions :: new ( )
279+ . create ( true )
280+ . append ( true )
281+ . open ( & self . path )
282+ . expect ( "open rotating file" ) ;
283+ let _ = f. write_all ( line. as_bytes ( ) ) ;
284+ }
285+ }
0 commit comments