# Perforce Defect Tracking Integration Project # # # LOGGER.PY -- PROGRAM LOGGING CLASSES # # Gareth Rees, Ravenbrook Limited, 2000-10-16 # # # 1. INTRODUCTION # # This Python module implements classes for program logging -- recording # information about the activity of a program. # # Logging is primarily intended to record what the integration does so # that the administrator can: # # 1. undo it in an emergency [Requirements, 67]; # # 2. debug their configuration [Requirements, 63]; # # 3. remove the integration [Requirements, 64]; # # and so that developers can: # # 4. debug modififcations of the system [Requirements, 25]; # # 5. debug their extensions to new defect trackers [Requirements, 21]. # # The intended readership of this document is project developers. # # This document is not confidential. import catalog import message import os import sys import time import types from dt_tracker import date_translator # 2. ABSTRACT LOGGER CLASS # # "logger" is an abstract class for message logs, providing uniform # message formatting and control of logging based on the priority of # messages, but no actual logging. # # When constructing an instance of this class or a subclass, pass the # minimum priority of message that should be written to the log (e.g., # message.DEBUG to see all debug-level messages, or message.ERROR to # only see errors). class logger: # Experienced a failure? has_failed = 0 # Minimum priority of messages to log. priority = message.INFO # Maximum length of a log message. max_length = 10000 # Translator to generate date time in appropriate format date_time_translator = None def __init__(self, priority = message.INFO, max_length = 10000): assert isinstance(priority, types.IntType) self.priority = priority self.has_failed = 0 self.max_length = max_length self.date_time_translator = date_translator() # 2.1. Convert a message to a string with truncation # # Truncate message to max_length to avoid very large log messages # causing the Windows event log or Linux system log to crash. def stringify_message(self, msg): assert isinstance(msg, message.message) return str(msg)[0:self.max_length] # 2.2. Format a message with date # # format_with_date(msg). Format log message and return the message # as a string. Messages are prefixed with the current date and time # in UTC and the message id. See message.py for details of the # formatting of message ids. # # The reason for the existence of this method is that not all # logging methods record the date (in particular, file_logger # doesn't). def format_with_date(self, msg): assert isinstance(msg, message.message) date = self.date_time_translator.translate_time_to_dt(time.localtime(time.time())) return "%s %s" % (date, self.stringify_message(msg)) # 2.3. Maybe log a message # # maybe_log(msg). Write the message to the log (using the write() # method), but only if its priority is higher than self.priority. def maybe_log(self, msg): assert isinstance(msg, message.message) # Higher priorities have lower numbers, hence the sense of # this test. if msg.priority <= self.priority: self.write(msg) # 2.4. Write a message to the log # # write(msg). Write the message to the log. There is no # implementation in the logger class (it's an abstract class). # Subclasses should provide the appopriate mechanism. def write(self, msg): assert isinstance(msg, message.message) # logger is an abstract class; no implementation of write(). assert 0 # 2.5. Log a message # # log(msg). Invoke maybe_log(), with error handling. This method # is the external interface to the logger. You may not override # this method in a subclass unless you support identical error # handling. Override the maybe_log method instead. def log(self, msg): assert isinstance(msg, message.message) try: self.maybe_log(msg) except: type, value = sys.exc_info()[:2] self.advise_failed('%s: %s' % (type, value)) # 2.6. Handle a log failure # # advise_failed(err). Handle the error message by calling # log_failed_hook(). But only once per logger. We will only # advise that a log has failed once. Otherwise the administrator # could be bombarded with failure messages. def advise_failed(self, err): # Is this the first time this logger has run into problems? if not self.has_failed: self.has_failed = 1 context = self.failure_context() self.log_failed_hook(err, context) # 2.7. User hook for log failure # # By default we just re-raise the error. This hook can be set # elsewhere (by the replicator, for instance, to a method that # sends mail to an administrator). For example, in a method in # the replicator class one could write: # # def log_failed_hook(err, context, r=self): # r.mail_report(...) # self.config.logger.set_log_failed_hook(log_failed_hook) # # in order to mail the failure report to the administrator. def log_failed_hook(self, err, context): raise def set_log_failed_hook(self, hook): assert type(hook) in [types.FunctionType, types.MethodType] self.log_failed_hook = hook # 2.8. Describe context of log failure # # failure_context(). Return a message object describing the # context of the log failure, suitable for using as an # introduction to a failure report. def failure_context(self): # "An attempt to write a log message to %s failed." return catalog.msg(1018, self) # 3. FILE LOGGER CLASS # # This subclass of logger appends messages to a file stream, or to the # standard output if no file is specified when the instance is created. # The output buffer is flushed after each message is written so that the # log can be recovered even if the program crashes. class file_logger(logger): file = None def __init__(self, file = sys.stdout, priority = message.INFO, max_length = 10000): # Test interface, not type, since sys.stdout is not a native # file objectin environments like PythonWin. assert hasattr(file, 'write') and hasattr(file, 'flush') logger.__init__(self, priority, max_length) self.file = file def write(self, msg): assert isinstance(msg, message.message) self.file.write(self.format_with_date(msg)) self.file.write('\n') self.file.flush() def failure_context(self): if self.file is sys.stdout: # "An attempt to write a log message to standard output # failed." return catalog.msg(1017) else: destination = getattr(self.file, 'name', str(self.file)) # "An attempt to write a log message to %s failed." return catalog.msg(1018, destination) # 4. SYSTEM LOGGER CLASS # # This subclass of logger logs messages to the system log on Unix (using # syslog). On other operating systems, it does nothing. class sys_logger(logger): def __init__(self, priority = message.INFO, max_length = 10000): logger.__init__(self, priority, max_length) if os.name == 'posix': import syslog self.syslog = syslog.syslog syslog.openlog('p4dti', syslog.LOG_PID, syslog.LOG_DAEMON) syslog.setlogmask(syslog.LOG_UPTO(priority)) def syslog(self, priority, text): pass # 4.1. Log a message to the system log # # We override the maybe_log() method rather than the write() # method because we use syslog's logmask feature instead of # checking the priority ourselves. def maybe_log(self, msg): assert isinstance(msg, message.message) self.syslog(msg.priority, self.stringify_message(msg)) def failure_context(self): # "An attempt to write a log message to the system log failed." return catalog.msg(1019) # 5. LOGGER CLASS FOR MULTIPLE LOGS # # This is a meta-logger that writes the message to each of a list of # other loggers. This is so that the administrator can arrange for # messages to go to several places in the integration configuration [RB # 2000-08-10, 5.1]. class multi_logger(logger): loggers = [] def __init__(self, loggers = [], priority = message.INFO, max_length = 10000): assert isinstance(loggers, types.ListType) for l in loggers: assert(isinstance(l, logger)) logger.__init__(self, priority, max_length) self.loggers = loggers def maybe_log(self, msg): assert isinstance(msg, message.message) for l in self.loggers: l.log(msg) def set_log_failed_hook(self, hook): assert type(hook) in [types.FunctionType, types.MethodType] # Call superclass method. logger.set_log_failed_hook(self, hook) for l in self.loggers: l.set_log_failed_hook(hook) # 6. WINDOWS EVENT LOGGER CLASS class win32_event_logger(logger): # Application name. application = None # Map from message priority to Windows event type. event_type = None def __init__(self, rid, priority = message.INFO, max_length = 10000): logger.__init__(self, priority, max_length) self.application = "P4DTI-" + rid # The "Event Message File" is eventlog.dll, in the same # directory as the message.py module. emf = os.path.join( os.path.dirname(os.path.abspath(message.__file__)), "eventlog.dll") import win32evtlogutil win32evtlogutil.AddSourceToRegistry(self.application, emf) import win32evtlog self.event_type = { message.EMERG: win32evtlog.EVENTLOG_ERROR_TYPE, message.ALERT: win32evtlog.EVENTLOG_ERROR_TYPE, message.CRIT: win32evtlog.EVENTLOG_ERROR_TYPE, message.ERR: win32evtlog.EVENTLOG_ERROR_TYPE, message.WARNING: win32evtlog.EVENTLOG_WARNING_TYPE, message.NOTICE: win32evtlog.EVENTLOG_WARNING_TYPE, message.INFO: win32evtlog.EVENTLOG_INFORMATION_TYPE, message.DEBUG: win32evtlog.EVENTLOG_INFORMATION_TYPE, } def write(self, msg): assert isinstance(msg, message.message) import win32evtlogutil win32evtlogutil.ReportEvent(self.application, 0, 0, self.event_type[msg.priority], [self.stringify_message(msg)]) def failure_context(self): # "An attempt to write a log message to the NT event log # failed." return catalog.msg(1020) # A. REFERENCES # # [RB 2000-08-10] "Perforce Defect Tracking Integration Administrator's # Guide"; Richard Brooksby; Ravenbrook Limited; 2000-08-10; # . # # [Requirements] "Perforce Defect Tracking Integration Project # Requirements"; Gareth Rees; Ravenbrook Limited; 2000-05-24; # . # # # B. DOCUMENT HISTORY # # 2000-10-16 GDR Created. # # 2000-11-30 GDR Added some type checking. # # 2001-01-19 NB Added sys_logger. # # 2001-01-26 NB Removed extra \n. # # 2001-03-01 NB Fix for job000237. # # 2001-03-02 RB Transferred copyright to Perforce under their license. # # 2001-03-11 GDR Formatted as a document. Uses the message class. # Check digit computation moved to message.py. Loggers pay attention to # message priority. # # 2001-04-10 NB job000292: Added call to str() as our message objects # were upsetting syslog.syslog. # # 2001-09-12 GDR Added logger class for the Windows event log. # # 2001-11-06 NDL File logger works in PythonWin. # # 2001-11-20 NDL Added log_failed_hook. # # 2001-11-26 GDR Truncate log messages to a specified length. # # 2001-12-07 GDR Test files by interface, not type; don't assume it has # a 'name' attribute. Allow log failure hooks to be methods. # # # C. COPYRIGHT AND LICENSE # # This file is copyright (c) 2001 Perforce Software, Inc. All rights # reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. # # # $Id: //info.ravenbrook.com/project/p4dti/version/2.1/code/replicator/logger.py#2 $