1# -*- coding: utf-8 -*-
2# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
3# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
4#
5# This file is part of logilab-common.
6#
7# logilab-common is free software: you can redistribute it and/or modify it under
8# the terms of the GNU Lesser General Public License as published by the Free
9# Software Foundation, either version 2.1 of the License, or (at your option) any
10# later version.
11#
12# logilab-common is distributed in the hope that it will be useful, but WITHOUT
13# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
15# details.
16#
17# You should have received a copy of the GNU Lesser General Public License along
18# with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
19"""Extends the logging module from the standard library."""
20
21__docformat__ = "restructuredtext en"
22
23import os
24import sys
25import logging
26
27from six import string_types
28
29from logilab.common.textutils import colorize_ansi
30
31
32def set_log_methods(cls, logger):
33    """bind standard logger's methods as methods on the class"""
34    cls.__logger = logger
35    for attr in ('debug', 'info', 'warning', 'error', 'critical', 'exception'):
36        setattr(cls, attr, getattr(logger, attr))
37
38
39def xxx_cyan(record):
40    if 'XXX' in record.message:
41        return 'cyan'
42
43class ColorFormatter(logging.Formatter):
44    """
45    A color Formatter for the logging standard module.
46
47    By default, colorize CRITICAL and ERROR in red, WARNING in orange, INFO in
48    green and DEBUG in yellow.
49
50    self.colors is customizable via the 'color' constructor argument (dictionary).
51
52    self.colorfilters is a list of functions that get the LogRecord
53    and return a color name or None.
54    """
55
56    def __init__(self, fmt=None, datefmt=None, colors=None):
57        logging.Formatter.__init__(self, fmt, datefmt)
58        self.colorfilters = []
59        self.colors = {'CRITICAL': 'red',
60                       'ERROR': 'red',
61                       'WARNING': 'magenta',
62                       'INFO': 'green',
63                       'DEBUG': 'yellow',
64                       }
65        if colors is not None:
66            assert isinstance(colors, dict)
67            self.colors.update(colors)
68
69    def format(self, record):
70        msg = logging.Formatter.format(self, record)
71        if record.levelname in self.colors:
72            color = self.colors[record.levelname]
73            return colorize_ansi(msg, color)
74        else:
75            for cf in self.colorfilters:
76                color = cf(record)
77                if color:
78                    return colorize_ansi(msg, color)
79        return msg
80
81def set_color_formatter(logger=None, **kw):
82    """
83    Install a color formatter on the 'logger'. If not given, it will
84    defaults to the default logger.
85
86    Any additional keyword will be passed as-is to the ColorFormatter
87    constructor.
88    """
89    if logger is None:
90        logger = logging.getLogger()
91        if not logger.handlers:
92            logging.basicConfig()
93    format_msg = logger.handlers[0].formatter._fmt
94    fmt = ColorFormatter(format_msg, **kw)
95    fmt.colorfilters.append(xxx_cyan)
96    logger.handlers[0].setFormatter(fmt)
97
98
99LOG_FORMAT = '%(asctime)s - (%(name)s) %(levelname)s: %(message)s'
100LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
101
102def get_handler(debug=False, syslog=False, logfile=None, rotation_parameters=None):
103    """get an apropriate handler according to given parameters"""
104    if os.environ.get('APYCOT_ROOT'):
105        handler = logging.StreamHandler(sys.stdout)
106    if debug:
107        handler = logging.StreamHandler()
108    elif logfile is None:
109        if syslog:
110            from logging import handlers
111            handler = handlers.SysLogHandler()
112        else:
113            handler = logging.StreamHandler()
114    else:
115        try:
116            if rotation_parameters is None:
117                if os.name == 'posix' and sys.version_info >= (2, 6):
118                    from logging.handlers import WatchedFileHandler
119                    handler = WatchedFileHandler(logfile)
120                else:
121                    handler = logging.FileHandler(logfile)
122            else:
123                from logging.handlers import TimedRotatingFileHandler
124                handler = TimedRotatingFileHandler(
125                    logfile, **rotation_parameters)
126        except IOError:
127            handler = logging.StreamHandler()
128    return handler
129
130def get_threshold(debug=False, logthreshold=None):
131    if logthreshold is None:
132        if debug:
133            logthreshold = logging.DEBUG
134        else:
135            logthreshold = logging.ERROR
136    elif isinstance(logthreshold, string_types):
137        logthreshold = getattr(logging, THRESHOLD_MAP.get(logthreshold,
138                                                          logthreshold))
139    return logthreshold
140
141def _colorable_terminal():
142    isatty = hasattr(sys.__stdout__, 'isatty') and sys.__stdout__.isatty()
143    if not isatty:
144        return False
145    if os.name == 'nt':
146        try:
147            from colorama import init as init_win32_colors
148        except ImportError:
149            return False
150        init_win32_colors()
151    return True
152
153def get_formatter(logformat=LOG_FORMAT, logdateformat=LOG_DATE_FORMAT):
154    if _colorable_terminal():
155        fmt = ColorFormatter(logformat, logdateformat)
156        def col_fact(record):
157            if 'XXX' in record.message:
158                return 'cyan'
159            if 'kick' in record.message:
160                return 'red'
161        fmt.colorfilters.append(col_fact)
162    else:
163        fmt = logging.Formatter(logformat, logdateformat)
164    return fmt
165
166def init_log(debug=False, syslog=False, logthreshold=None, logfile=None,
167             logformat=LOG_FORMAT, logdateformat=LOG_DATE_FORMAT, fmt=None,
168             rotation_parameters=None, handler=None):
169    """init the log service"""
170    logger = logging.getLogger()
171    if handler is None:
172        handler = get_handler(debug, syslog, logfile, rotation_parameters)
173    # only addHandler and removeHandler method while I would like a setHandler
174    # method, so do it this way :$
175    logger.handlers = [handler]
176    logthreshold = get_threshold(debug, logthreshold)
177    logger.setLevel(logthreshold)
178    if fmt is None:
179        if debug:
180            fmt = get_formatter(logformat=logformat, logdateformat=logdateformat)
181        else:
182            fmt = logging.Formatter(logformat, logdateformat)
183    handler.setFormatter(fmt)
184    return handler
185
186# map logilab.common.logger thresholds to logging thresholds
187THRESHOLD_MAP = {'LOG_DEBUG':   'DEBUG',
188                 'LOG_INFO':    'INFO',
189                 'LOG_NOTICE':  'INFO',
190                 'LOG_WARN':    'WARNING',
191                 'LOG_WARNING': 'WARNING',
192                 'LOG_ERR':     'ERROR',
193                 'LOG_ERROR':   'ERROR',
194                 'LOG_CRIT':    'CRITICAL',
195                 }
196