# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import inspect import logging import logging.handlers import numbers import os import sys try: import syslog except ImportError: syslog = None from daiquiri import formatter from daiquiri import handlers def get_program_name(): return os.path.basename(inspect.stack()[-1][1]) class Output(object): """Generic log output.""" def __init__(self, handler, formatter=formatter.TEXT_FORMATTER, level=None): self.handler = handler self.handler.setFormatter(formatter) if level is not None: self.handler.setLevel(level) def add_to_logger(self, logger): """Add this output to a logger.""" logger.addHandler(self.handler) def _get_log_file_path(logfile=None, logdir=None, program_name=None, logfile_suffix=".log"): ret_path = None if not logdir: ret_path = logfile if not ret_path and logfile and logdir: ret_path = os.path.join(logdir, logfile) if not ret_path and logdir: program_name = program_name or get_program_name() ret_path = os.path.join(logdir, program_name) + logfile_suffix if not ret_path: raise ValueError("Unable to determine log file destination") return ret_path class File(Output): """Ouput to a file.""" def __init__(self, filename=None, directory=None, suffix=".log", program_name=None, formatter=formatter.TEXT_FORMATTER, level=None): """Log file output. :param filename: The log file path to write to. If directory is also specified, both will be combined. :param directory: The log directory to write to. If no filename is specified, the program name and suffix will be used to contruct the full path relative to the directory. :param suffix: The log file name suffix. This will be only used if no filename has been provided. :param program_name: Program name. Autodetected by default. """ logpath = _get_log_file_path(filename, directory, program_name, suffix) handler = logging.handlers.WatchedFileHandler(logpath) super(File, self).__init__(handler, formatter, level) class RotatingFile(Output): """Output to a file, rotating after a certain size.""" def __init__(self, filename=None, directory=None, suffix='.log', program_name=None, formatter=formatter.TEXT_FORMATTER, level=None, max_size_bytes=0, backup_count=0): """Rotating log file output. :param filename: The log file path to write to. If directory is also specified, both will be combined. :param directory: The log directory to write to. If no filename is specified, the program name and suffix will be used to contruct the full path relative to the directory. :param suffix: The log file name suffix. This will be only used if no filename has been provided. :param program_name: Program name. Autodetected by default. :param max_size_bytes: allow the file to rollover at a predetermined size. :param backup_count: the maximum number of files to rotate logging output between. """ logpath = _get_log_file_path(filename, directory, program_name, suffix) handler = logging.handlers.RotatingFileHandler( logpath, maxBytes=max_size_bytes, backupCount=backup_count) super(RotatingFile, self).__init__(handler, formatter, level) def do_rollover(self): """Manually forces a log file rotation.""" return self.handler.doRollover() class TimedRotatingFile(Output): """Rotating log file output, triggered by a fixed interval.""" def __init__(self, filename=None, directory=None, suffix='.log', program_name=None, formatter=formatter.TEXT_FORMATTER, level=None, interval=datetime.timedelta(hours=24), backup_count=0): """Rotating log file output, triggered by a fixed interval. :param filename: The log file path to write to. If directory is also specified, both will be combined. :param directory: The log directory to write to. If no filename is specified, the program name and suffix will be used to contruct the full path relative to the directory. :param suffix: The log file name suffix. This will be only used if no filename has been provided. :param program_name: Program name. Autodetected by default. :param interval: datetime.timedelta instance representing how often a new log file should be created. :param backup_count: the maximum number of files to rotate logging output between. """ logpath = _get_log_file_path(filename, directory, program_name, suffix) handler = logging.handlers.TimedRotatingFileHandler( logpath, when='S', interval=self._timedelta_to_seconds(interval), backupCount=backup_count) super(TimedRotatingFile, self).__init__(handler, formatter, level) def do_rollover(self): """Manually forces a log file rotation.""" return self.handler.doRollover() @staticmethod def _timedelta_to_seconds(td): """Convert a datetime.timedelta object into a seconds interval for rotating file ouput. :param td: datetime.timedelta :return: time in seconds :rtype: int """ if isinstance(td, numbers.Real): td = datetime.timedelta(seconds=td) return td.total_seconds() class Stream(Output): """Generic stream output.""" def __init__(self, stream=sys.stderr, formatter=formatter.TEXT_FORMATTER, level=None): super(Stream, self).__init__(handlers.TTYDetectorStreamHandler(stream), formatter, level) STDERR = Stream() STDOUT = Stream(sys.stdout) class Journal(Output): def __init__(self, program_name=None, formatter=formatter.TEXT_FORMATTER, level=None): program_name = program_name or get_program_name super(Journal, self).__init__(handlers.JournalHandler(program_name), formatter, level) class Syslog(Output): def __init__(self, program_name=None, facility="user", formatter=formatter.TEXT_FORMATTER, level=None): if syslog is None: # FIXME(jd) raise something more specific raise RuntimeError("syslog is not available on this platform") super(Syslog, self).__init__( handlers.SyslogHandler( program_name=program_name or get_program_name(), facility=self._find_facility(facility)), formatter, level) @staticmethod def _find_facility(facility): # NOTE(jd): Check the validity of facilities at run time as they differ # depending on the OS and Python version being used. valid_facilities = [f for f in ["LOG_KERN", "LOG_USER", "LOG_MAIL", "LOG_DAEMON", "LOG_AUTH", "LOG_SYSLOG", "LOG_LPR", "LOG_NEWS", "LOG_UUCP", "LOG_CRON", "LOG_AUTHPRIV", "LOG_FTP", "LOG_LOCAL0", "LOG_LOCAL1", "LOG_LOCAL2", "LOG_LOCAL3", "LOG_LOCAL4", "LOG_LOCAL5", "LOG_LOCAL6", "LOG_LOCAL7"] if getattr(syslog, f, None)] facility = facility.upper() if not facility.startswith("LOG_"): facility = "LOG_" + facility if facility not in valid_facilities: raise TypeError('syslog facility must be one of: %s' % ', '.join("'%s'" % fac for fac in valid_facilities)) return getattr(syslog, facility) preconfigured = { 'stderr': STDERR, 'stdout': STDOUT, } if syslog is not None: preconfigured['syslog'] = Syslog() if handlers.journal is not None: preconfigured['journal'] = Journal()