diff options
Diffstat (limited to 'Lib/logging/handlers.py')
| -rw-r--r-- | Lib/logging/handlers.py | 223 |
1 files changed, 177 insertions, 46 deletions
diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index 1e3e298..756baf0 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -19,20 +19,23 @@ Additional handlers for the logging package for Python. The core package is based on PEP 282 and comments thereto in comp.lang.python, and influenced by Apache's log4j system. -Should work under Python versions >= 1.5.2, except that source line -information is not available unless 'sys._getframe()' is. +Copyright (C) 2001-2009 Vinay Sajip. All Rights Reserved. -Copyright (C) 2001-2007 Vinay Sajip. All Rights Reserved. - -To use, simply 'import logging' and log away! +To use, simply 'import logging.handlers' and log away! """ -import sys, logging, socket, types, os, string, cPickle, struct, time, glob +import logging, socket, types, os, string, cPickle, struct, time, re +from stat import ST_DEV, ST_INO try: import codecs except ImportError: codecs = None +try: + unicode + _unicode = True +except NameError: + _unicode = False # # Some constants... @@ -52,13 +55,13 @@ class BaseRotatingHandler(logging.FileHandler): Not meant to be instantiated directly. Instead, use RotatingFileHandler or TimedRotatingFileHandler. """ - def __init__(self, filename, mode, encoding=None): + def __init__(self, filename, mode, encoding=None, delay=0): """ Use the specified filename for streamed logging """ if codecs is None: encoding = None - logging.FileHandler.__init__(self, filename, mode, encoding) + logging.FileHandler.__init__(self, filename, mode, encoding, delay) self.mode = mode self.encoding = encoding @@ -83,7 +86,7 @@ class RotatingFileHandler(BaseRotatingHandler): Handler for logging to a set of files, which switches from one file to the next when the current file reaches a certain size. """ - def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None): + def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=0): """ Open the specified file and use it as the stream for logging. @@ -106,7 +109,7 @@ class RotatingFileHandler(BaseRotatingHandler): """ if maxBytes > 0: mode = 'a' # doesn't make sense otherwise! - BaseRotatingHandler.__init__(self, filename, mode, encoding) + BaseRotatingHandler.__init__(self, filename, mode, encoding, delay) self.maxBytes = maxBytes self.backupCount = backupCount @@ -130,10 +133,8 @@ class RotatingFileHandler(BaseRotatingHandler): os.remove(dfn) os.rename(self.baseFilename, dfn) #print "%s -> %s" % (self.baseFilename, dfn) - if self.encoding: - self.stream = codecs.open(self.baseFilename, 'w', self.encoding) - else: - self.stream = open(self.baseFilename, 'w') + self.mode = 'w' + self.stream = self._open() def shouldRollover(self, record): """ @@ -142,6 +143,8 @@ class RotatingFileHandler(BaseRotatingHandler): Basically, see if the supplied record would cause the file to exceed the size limit we have. """ + if self.stream is None: # delay was set... + self.stream = self._open() if self.maxBytes > 0: # are we rolling over? msg = "%s\n" % self.format(record) self.stream.seek(0, 2) #due to non-posix-compliant Windows feature @@ -157,10 +160,11 @@ class TimedRotatingFileHandler(BaseRotatingHandler): If backupCount is > 0, when rollover is done, no more than backupCount files are kept - the oldest ones are deleted. """ - def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None): - BaseRotatingHandler.__init__(self, filename, 'a', encoding) + def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=0, utc=0): + BaseRotatingHandler.__init__(self, filename, 'a', encoding, delay) self.when = string.upper(when) self.backupCount = backupCount + self.utc = utc # Calculate the real rollover interval, which is just the number of # seconds between rollovers. Also set the filename suffix used when # a rollover occurs. Current 'when' events supported: @@ -177,15 +181,19 @@ class TimedRotatingFileHandler(BaseRotatingHandler): if self.when == 'S': self.interval = 1 # one second self.suffix = "%Y-%m-%d_%H-%M-%S" + self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$" elif self.when == 'M': self.interval = 60 # one minute self.suffix = "%Y-%m-%d_%H-%M" + self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$" elif self.when == 'H': self.interval = 60 * 60 # one hour self.suffix = "%Y-%m-%d_%H" + self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}$" elif self.when == 'D' or self.when == 'MIDNIGHT': self.interval = 60 * 60 * 24 # one day self.suffix = "%Y-%m-%d" + self.extMatch = r"^\d{4}-\d{2}-\d{2}$" elif self.when.startswith('W'): self.interval = 60 * 60 * 24 * 7 # one week if len(self.when) != 2: @@ -194,12 +202,21 @@ class TimedRotatingFileHandler(BaseRotatingHandler): raise ValueError("Invalid day specified for weekly rollover: %s" % self.when) self.dayOfWeek = int(self.when[1]) self.suffix = "%Y-%m-%d" + self.extMatch = r"^\d{4}-\d{2}-\d{2}$" else: raise ValueError("Invalid rollover interval specified: %s" % self.when) + self.extMatch = re.compile(self.extMatch) self.interval = self.interval * interval # multiply by units requested - self.rolloverAt = currentTime + self.interval + self.rolloverAt = self.computeRollover(int(time.time())) + + #print "Will rollover at %d, %d seconds from now" % (self.rolloverAt, self.rolloverAt - currentTime) + def computeRollover(self, currentTime): + """ + Work out the rollover time based on the specified time. + """ + result = currentTime + self.interval # If we are rolling over at midnight or weekly, then the interval is already known. # What we need to figure out is WHEN the next interval is. In other words, # if you are rolling over at midnight, then your base interval is 1 day, @@ -209,14 +226,17 @@ class TimedRotatingFileHandler(BaseRotatingHandler): # the rest. Note that this code doesn't care about leap seconds. :) if self.when == 'MIDNIGHT' or self.when.startswith('W'): # This could be done with less code, but I wanted it to be clear - t = time.localtime(currentTime) + if self.utc: + t = time.gmtime(currentTime) + else: + t = time.localtime(currentTime) currentHour = t[3] currentMinute = t[4] currentSecond = t[5] # r is the number of seconds left between now and midnight r = _MIDNIGHT - ((currentHour * 60 + currentMinute) * 60 + currentSecond) - self.rolloverAt = currentTime + r + result = currentTime + r # If we are rolling over on a certain day, add in the number of days until # the next rollover, but offset by 1 since we just calculated the time # until the next day starts. There are three cases: @@ -232,23 +252,31 @@ class TimedRotatingFileHandler(BaseRotatingHandler): # The calculations described in 2) and 3) above need to have a day added. # This is because the above time calculation takes us to midnight on this # day, i.e. the start of the next day. - if when.startswith('W'): + if self.when.startswith('W'): day = t[6] # 0 is Monday if day != self.dayOfWeek: if day < self.dayOfWeek: daysToWait = self.dayOfWeek - day else: daysToWait = 6 - day + self.dayOfWeek + 1 - self.rolloverAt = self.rolloverAt + (daysToWait * (60 * 60 * 24)) - - #print "Will rollover at %d, %d seconds from now" % (self.rolloverAt, self.rolloverAt - currentTime) + newRolloverAt = result + (daysToWait * (60 * 60 * 24)) + if not self.utc: + dstNow = t[-1] + dstAtRollover = time.localtime(newRolloverAt)[-1] + if dstNow != dstAtRollover: + if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour + newRolloverAt = newRolloverAt - 3600 + else: # DST bows out before next rollover, so we need to add an hour + newRolloverAt = newRolloverAt + 3600 + result = newRolloverAt + return result def shouldRollover(self, record): """ - Determine if rollover should occur + Determine if rollover should occur. record is not used, as we are just comparing times, but it is needed so - the method siguratures are the same + the method signatures are the same """ t = int(time.time()) if t >= self.rolloverAt: @@ -256,6 +284,29 @@ class TimedRotatingFileHandler(BaseRotatingHandler): #print "No need to rollover: %d, %d" % (t, self.rolloverAt) return 0 + def getFilesToDelete(self): + """ + Determine the files to delete when rolling over. + + More specific than the earlier method, which just used glob.glob(). + """ + dirName, baseName = os.path.split(self.baseFilename) + fileNames = os.listdir(dirName) + result = [] + prefix = baseName + "." + plen = len(prefix) + for fileName in fileNames: + if fileName[:plen] == prefix: + suffix = fileName[plen:] + if self.extMatch.match(suffix): + result.append(os.path.join(dirName, fileName)) + result.sort() + if len(result) < self.backupCount: + result = [] + else: + result = result[:len(result) - self.backupCount] + return result + def doRollover(self): """ do a rollover; in this case, a date/time stamp is appended to the filename @@ -264,26 +315,94 @@ class TimedRotatingFileHandler(BaseRotatingHandler): then we have to get a list of matching filenames, sort them and remove the one with the oldest suffix. """ - self.stream.close() + if self.stream: + self.stream.close() # get the time that this sequence started at and make it a TimeTuple t = self.rolloverAt - self.interval - timeTuple = time.localtime(t) + if self.utc: + timeTuple = time.gmtime(t) + else: + timeTuple = time.localtime(t) dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple) if os.path.exists(dfn): os.remove(dfn) os.rename(self.baseFilename, dfn) if self.backupCount > 0: # find the oldest log file and delete it - s = glob.glob(self.baseFilename + ".20*") - if len(s) > self.backupCount: - s.sort() - os.remove(s[0]) + #s = glob.glob(self.baseFilename + ".20*") + #if len(s) > self.backupCount: + # s.sort() + # os.remove(s[0]) + for s in self.getFilesToDelete(): + os.remove(s) #print "%s -> %s" % (self.baseFilename, dfn) - if self.encoding: - self.stream = codecs.open(self.baseFilename, 'w', self.encoding) + self.mode = 'w' + self.stream = self._open() + currentTime = int(time.time()) + newRolloverAt = self.computeRollover(currentTime) + while newRolloverAt <= currentTime: + newRolloverAt = newRolloverAt + self.interval + #If DST changes and midnight or weekly rollover, adjust for this. + if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc: + dstNow = time.localtime(currentTime)[-1] + dstAtRollover = time.localtime(newRolloverAt)[-1] + if dstNow != dstAtRollover: + if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour + newRolloverAt = newRolloverAt - 3600 + else: # DST bows out before next rollover, so we need to add an hour + newRolloverAt = newRolloverAt + 3600 + self.rolloverAt = newRolloverAt + +class WatchedFileHandler(logging.FileHandler): + """ + A handler for logging to a file, which watches the file + to see if it has changed while in use. This can happen because of + usage of programs such as newsyslog and logrotate which perform + log file rotation. This handler, intended for use under Unix, + watches the file to see if it has changed since the last emit. + (A file has changed if its device or inode have changed.) + If it has changed, the old file stream is closed, and the file + opened to get a new stream. + + This handler is not appropriate for use under Windows, because + under Windows open files cannot be moved or renamed - logging + opens the files with exclusive locks - and so there is no need + for such a handler. Furthermore, ST_INO is not supported under + Windows; stat always returns zero for this value. + + This handler is based on a suggestion and patch by Chad J. + Schroeder. + """ + def __init__(self, filename, mode='a', encoding=None, delay=0): + logging.FileHandler.__init__(self, filename, mode, encoding, delay) + if not os.path.exists(self.baseFilename): + self.dev, self.ino = -1, -1 else: - self.stream = open(self.baseFilename, 'w') - self.rolloverAt = self.rolloverAt + self.interval + stat = os.stat(self.baseFilename) + self.dev, self.ino = stat[ST_DEV], stat[ST_INO] + + def emit(self, record): + """ + Emit a record. + + First check if the underlying file has changed, and if it + has, close the old stream and reopen the file to get the + current stream. + """ + if not os.path.exists(self.baseFilename): + stat = None + changed = 1 + else: + stat = os.stat(self.baseFilename) + changed = (stat[ST_DEV] != self.dev) or (stat[ST_INO] != self.ino) + if changed and self.stream is not None: + self.stream.flush() + self.stream.close() + self.stream = self._open() + if stat is None: + stat = os.stat(self.baseFilename) + self.dev, self.ino = stat[ST_DEV], stat[ST_INO] + logging.FileHandler.emit(self, record) class SocketHandler(logging.Handler): """ @@ -319,12 +438,14 @@ class SocketHandler(logging.Handler): self.retryMax = 30.0 self.retryFactor = 2.0 - def makeSocket(self): + def makeSocket(self, timeout=1): """ A factory method which allows subclasses to define the precise type of socket they want. """ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if hasattr(s, 'settimeout'): + s.settimeout(timeout) s.connect((self.host, self.port)) return s @@ -346,7 +467,7 @@ class SocketHandler(logging.Handler): try: self.sock = self.makeSocket() self.retryTime = None # next time, no delay before trying - except: + except socket.error: #Creation failed, so set the retry time and return. if self.retryTime is None: self.retryPeriod = self.retryStart @@ -661,6 +782,11 @@ class SysLogHandler(logging.Handler): self.encodePriority(self.facility, self.mapPriority(record.levelname)), msg) + # Treat unicode messages as required by RFC 5424 + if _unicode and type(msg) is unicode: + msg = msg.encode('utf-8') + if codecs: + msg = codecs.BOM_UTF8 + msg try: if self.unixsocket: try: @@ -679,22 +805,25 @@ class SMTPHandler(logging.Handler): """ A handler class which sends an SMTP email for each logging event. """ - def __init__(self, mailhost, fromaddr, toaddrs, subject): + def __init__(self, mailhost, fromaddr, toaddrs, subject, credentials=None): """ Initialize the handler. Initialize the instance with the from and to addresses and subject line of the email. To specify a non-standard SMTP port, use the - (host, port) tuple format for the mailhost argument. + (host, port) tuple format for the mailhost argument. To specify + authentication credentials, supply a (username, password) tuple + for the credentials argument. """ logging.Handler.__init__(self) if type(mailhost) == types.TupleType: - host, port = mailhost - self.mailhost = host - self.mailport = port + self.mailhost, self.mailport = mailhost + else: + self.mailhost, self.mailport = mailhost, None + if type(credentials) == types.TupleType: + self.username, self.password = credentials else: - self.mailhost = mailhost - self.mailport = None + self.username = None self.fromaddr = fromaddr if type(toaddrs) == types.StringType: toaddrs = [toaddrs] @@ -737,8 +866,8 @@ class SMTPHandler(logging.Handler): try: import smtplib try: - from email.Utils import formatdate - except: + from email.utils import formatdate + except ImportError: formatdate = self.date_time port = self.mailport if not port: @@ -750,6 +879,8 @@ class SMTPHandler(logging.Handler): string.join(self.toaddrs, ","), self.getSubject(record), formatdate(), msg) + if self.username: + smtp.login(self.username, self.password) smtp.sendmail(self.fromaddr, self.toaddrs, msg) smtp.quit() except (KeyboardInterrupt, SystemExit): |
