44# SPDX-License-Identifier: BSD-3-Clause
55
66import abc
7+ import atexit
78import itertools
89import logging
910import logging .handlers
@@ -149,7 +150,8 @@ class MultiFileHandler(logging.FileHandler):
149150 '''
150151
151152 def __init__ (self , prefix , mode = 'a' , encoding = None , fmt = None ,
152- perffmt = None , ignore_keys = None ):
153+ perffmt = None , ignore_keys = None , use_locking = False ,
154+ lockfile_mode = None ):
153155 super ().__init__ (prefix , mode , encoding , delay = True )
154156
155157 # Reset FileHandler's filename
@@ -164,6 +166,9 @@ def __init__(self, prefix, mode='a', encoding=None, fmt=None,
164166 self .__perffmt = perffmt
165167 self .__attr_patt = re .compile (r'\%\((.*?)\)s(.*?(?=%|$))?' )
166168 self .__ignore_keys = set (ignore_keys ) if ignore_keys else set ()
169+ self .__use_locking = use_locking
170+ self .__lockfile_mode = lockfile_mode
171+ self .__locks = {}
167172
168173 def __generate_header (self , record ):
169174 # Generate the header from the record and fmt
@@ -208,6 +213,14 @@ def __generate_header(self, record):
208213
209214 return header
210215
216+ def __lock_file_name (self , logfile = None ):
217+ if logfile is None :
218+ logfile = self .baseFilename
219+
220+ prefix = os .path .dirname (logfile )
221+ basename , _ = os .path .splitext (os .path .basename (logfile ))
222+ return os .path .join (prefix , f'.{ basename } .lock' )
223+
211224 def _emit_header (self , record ):
212225 if self .baseFilename in self .__streams :
213226 return
@@ -234,13 +247,27 @@ def _emit_header(self, record):
234247
235248 os .rename (self .baseFilename , self .baseFilename + f'.h{ hcnt } ' )
236249 finally :
237- # Open the file for writing and write the header
238- fp = open (self .baseFilename ,
239- mode = self .mode , encoding = self .encoding )
240- if record_header != header :
241- fp .write (f'{ record_header } \n ' )
250+ if self .__use_locking :
251+ # When using locking, we need to open, append and write to
252+ # the file at once
253+ rwlock = osext .ReadWriteFileLock (self .__lock_file_name (),
254+ self .__lockfile_mode )
255+ with rwlock .write_lock ():
256+ with open (self .baseFilename , mode = self .mode ,
257+ encoding = self .encoding ) as fp :
258+ if record_header != header :
259+ fp .write (f'{ record_header } \n ' )
260+
261+ self .__streams [self .baseFilename ] = None
262+ self .__locks [self .baseFilename ] = rwlock
263+ else :
264+ # Open the file for writing and write the header
265+ fp = open (self .baseFilename ,
266+ mode = self .mode , encoding = self .encoding )
267+ if record_header != header :
268+ fp .write (f'{ record_header } \n ' )
242269
243- self .__streams [self .baseFilename ] = fp
270+ self .__streams [self .baseFilename ] = fp
244271
245272 def emit (self , record ):
246273 try :
@@ -255,14 +282,22 @@ def emit(self, record):
255282 check_basename = type (record .__rfm_check__ ).variant_name ()
256283 self .baseFilename = os .path .join (dirname , f'{ check_basename } .log' )
257284 self ._emit_header (record )
258- self .stream = self .__streams [self .baseFilename ]
259- super ().emit (record )
285+ if self .__use_locking :
286+ with self .__locks [self .baseFilename ].write_lock ():
287+ with open (self .baseFilename , mode = self .mode ,
288+ encoding = self .encoding ) as fp :
289+ self .stream = fp
290+ super ().emit (record )
291+ else :
292+ self .stream = self .__streams [self .baseFilename ]
293+ super ().emit (record )
260294
261295 def close (self ):
262296 # Close all open streams
263- for s in self .__streams .values ():
264- self .stream = s
265- super ().close ()
297+ for stream in self .__streams .values ():
298+ if stream :
299+ self .stream = stream
300+ super ().close ()
266301
267302
268303def _format_time_rfc3339 (timestamp , datefmt ):
@@ -459,9 +494,16 @@ def _create_filelog_handler(site_config, config_prefix):
459494 format = site_config .get (f'{ config_prefix } /format' )
460495 format_perf = site_config .get (f'{ config_prefix } /format_perfvars' )
461496 ignore_keys = site_config .get (f'{ config_prefix } /ignore_keys' )
497+ use_locking = site_config .get (f'{ config_prefix } /locking_enable' )
498+ lockfile_mode = site_config .get (f'{ config_prefix } /locking_file_mode' )
499+ if lockfile_mode is not None :
500+ lockfile_mode = int (lockfile_mode , base = 8 )
501+
462502 return MultiFileHandler (filename_patt , mode = 'a+' if append else 'w+' ,
463503 fmt = format , perffmt = format_perf ,
464- ignore_keys = ignore_keys )
504+ ignore_keys = ignore_keys ,
505+ use_locking = use_locking ,
506+ lockfile_mode = lockfile_mode )
465507
466508
467509@register_log_handler ('syslog' )
@@ -806,6 +848,13 @@ def debug(self, message, *args, **kwargs):
806848 def debug2 (self , message , * args , ** kwargs ):
807849 self .log (DEBUG2 , message , * args , ** kwargs )
808850
851+ def shutdown (self ):
852+ '''Shutdown logger by removing all handlers and closing any files.'''
853+
854+ for h in list (self .handlers ):
855+ h .close ()
856+ self .removeHandler (h )
857+
809858
810859# This is a cache for warnings that we don't want to repeat
811860_WARN_ONCE = set ()
@@ -1027,6 +1076,8 @@ def configure_logging(site_config):
10271076 _context_logger = null_logger
10281077 return
10291078
1079+ # Shutdown the previously setup loggers and close any files
1080+ shutdown ()
10301081 _logger = _create_logger (site_config , 'handlers$' , 'handlers' )
10311082 _perf_logger = _create_logger (site_config , 'handlers_perflog' )
10321083 _context_logger = LoggerAdapter (_logger )
@@ -1079,6 +1130,17 @@ def _fn(*args, **kwargs):
10791130 return _fn
10801131
10811132
1133+ @atexit .register
1134+ def shutdown ():
1135+ '''Shutdown logging'''
1136+
1137+ if _logger :
1138+ _logger .shutdown ()
1139+
1140+ if _perf_logger :
1141+ _perf_logger .shutdown ()
1142+
1143+
10821144# The following is meant to be used only by the unit tests
10831145
10841146class logging_sandbox :
@@ -1098,6 +1160,7 @@ def __enter__(self):
10981160 def __exit__ (self , exc_type , exc_value , traceback ):
10991161 global _logger , _perf_logger , _context_logger
11001162
1163+ shutdown ()
11011164 _logger = self ._logger
11021165 _perf_logger = self ._perf_logger
11031166 _context_logger = self ._context_logger
0 commit comments