#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015-2015: Alignak team, see AUTHORS.txt file for contributors
#
# This file is part of Alignak.
#
# Alignak is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Alignak is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Alignak. If not, see <http://www.gnu.org/licenses/>.
#
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright (C) 2009-2014:
# Hartmut Goebel, h.goebel@goebel-consult.de
# Guillaume Bour, guillaume@bour.cc
# xkilian, fmikus@acktomic.com
# Nicolas Dupeux, nicolas@dupeux.net
# Zoran Zaric, zz@zoranzaric.de
# Jan Ulferts, jan.ulferts@xing.com
# Grégory Starck, g.starck@gmail.com
# Frédéric Pégé, frederic.pege@gmail.com
# Sebastien Coavoux, s.coavoux@free.fr
# Thibault Cohen, titilambert@gmail.com
# Jean Gabes, naparuba@gmail.com
# Olivier Hanesse, olivier.hanesse@gmail.com
# Gerhard Lausser, gerhard.lausser@consol.de
# This file is part of Shinken.
#
# Shinken is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Shinken is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Shinken. If not, see <http://www.gnu.org/licenses/>.
"""
This module provide logging facilities for Alignak.
There is a custom log handler that create broks for every log emited with level < debug
"""
import logging
import sys
import os
import stat
from logging import Handler, Formatter, StreamHandler, NOTSET, FileHandler
from logging.handlers import TimedRotatingFileHandler
from termcolor import cprint
from alignak.brok import Brok
# obj = None
# name = None
HUMAN_TIMESTAMP_LOG = False
__brokhandler__ = None
DEFAULT_FORMATTER = Formatter('[%(created)i] %(levelname)s: %(message)s')
DEFAULT_FORMATTER_NAMED = Formatter('[%(created)i] %(levelname)s: [%(name)s] %(message)s')
HUMAN_FORMATTER = Formatter('[%(asctime)s] %(levelname)s: %(message)s', '%a %b %d %H:%M:%S %Y')
HUMAN_FORMATTER_NAMED = Formatter('[%(asctime)s] %(levelname)s: [%(name)s] %(message)s',
'%a %b %d %H:%M:%S %Y')
NAG_FORMATTER = Formatter('[%(created)i] %(message)s')
[docs]class BrokHandler(Handler):
"""
This log handler is forwarding log messages as broks to the broker.
Only messages of level higher than DEBUG are send to other
satellite to not risk overloading them.
"""
def __init__(self, broker):
# Only messages of level INFO or higher are passed on to the
# broker. Other handlers have a different level.
Handler.__init__(self, logging.INFO)
self._broker = broker
[docs] def emit(self, record):
try:
msg = self.format(record)
brok = Brok('log', {'log': msg + '\n'})
self._broker.add(brok)
except Exception:
self.handleError(record)
[docs]class ColorStreamHandler(StreamHandler):
"""
This log handler provides colored logs when logs are emitted to a tty.
"""
[docs] def emit(self, record):
try:
msg = self.format(record)
colors = {'DEBUG': 'cyan', 'INFO': 'magenta',
'WARNING': 'yellow', 'CRITICAL': 'magenta', 'ERROR': 'red'}
cprint(msg, colors[record.levelname])
except UnicodeEncodeError:
print msg.encode('ascii', 'ignore')
except Exception:
self.handleError(record)
[docs]class Log(logging.Logger):
"""
Alignak logger class, wrapping access to Python logging standard library.
See : https://docs.python.org/2/howto/logging.html#logging-flow for more detail about
how log are handled"""
def __init__(self, name="Alignak", level=NOTSET, log_set=False):
logging.Logger.__init__(self, name, level)
self.pre_log_buffer = []
self.log_set = log_set
[docs] def setLevel(self, level):
""" Set level of logger and handlers.
The logger need the lowest level (see link above)
:param level: logger/handler level
:type level: int
:return: None
"""
if not isinstance(level, int):
level = getattr(logging, level, None)
if not level or not isinstance(level, int):
raise TypeError('log level must be an integer')
# Not very useful, all we have to do is no to set the level > info for the brok handler
self.level = min(level, logging.INFO)
# Only set level to file and/or console handler
for handler in self.handlers:
if isinstance(handler, BrokHandler):
continue
handler.setLevel(level)
[docs] def load_obj(self, obj, name_=None):
""" We load the object where we will put log broks
with the 'add' method
:param obj: object instance
:type obj: object
:param name_: name of object
:type name_: str | None
:return: None
"""
global __brokhandler__
__brokhandler__ = BrokHandler(obj)
if name_ is not None or self.name is not None:
if name_ is not None:
self.name = name_
# We need to se the name format to all other handlers
for handler in self.handlers:
handler.setFormatter(DEFAULT_FORMATTER_NAMED)
__brokhandler__.setFormatter(DEFAULT_FORMATTER_NAMED)
else:
__brokhandler__.setFormatter(DEFAULT_FORMATTER)
self.addHandler(__brokhandler__)
[docs] def register_local_log(self, path, level=None, purge_buffer=True):
"""The alignak logging wrapper can write to a local file if needed
and return the file descriptor so we can avoid to
close it.
Add logging to a local log-file.
The file will be rotated once a day
:param path: path of log
:type path: str
:param level: level of log
:type level: None | int
:param purge_buffer: True if want purge the buffer, otherwise False
:type purge_buffer: bool
:return:
"""
self.log_set = True
# Todo : Create a config var for backup count
if os.path.exists(path) and not stat.S_ISREG(os.stat(path).st_mode):
# We don't have a regular file here. Rotate may fail
# It can be one of the stat.S_IS* (FIFO? CHR?)
handler = FileHandler(path)
else:
handler = TimedRotatingFileHandler(path, 'midnight', backupCount=5)
if level is not None:
handler.setLevel(level)
if self.name is not None:
handler.setFormatter(DEFAULT_FORMATTER_NAMED)
else:
handler.setFormatter(DEFAULT_FORMATTER)
self.addHandler(handler)
# Ok now unstack all previous logs
if purge_buffer:
self._destack()
# Todo : Do we need this now we use logging?
return handler.stream.fileno()
def _stack(self, level, args, kwargs):
"""
Stack logs if we don't open a log file so we will be able to flush them
Stack max 500 logs (no memory leak please...)
:param level: level log
:type level: int
:param args: arguments
:type args:
:param kwargs:
:type kwargs:
:return: None
"""
if self.log_set:
return
self.pre_log_buffer.append((level, args, kwargs))
if len(self.pre_log_buffer) > 500:
self.pre_log_buffer = self.pre_log_buffer[2:]
def _destack(self):
"""
DIRTY HACK : log should be always written to a file.
we are opening a log file, flush all the logs now
:return: None
"""
for (level, args, kwargs) in self.pre_log_buffer:
fun = getattr(logging.Logger, level, None)
if fun is None:
self.warning('Missing level for a log? %s', level)
continue
fun(self, *args, **kwargs)
[docs] def debug(self, *args, **kwargs):
self._stack('debug', args, kwargs)
logging.Logger.debug(self, *args, **kwargs)
[docs] def info(self, *args, **kwargs):
self._stack('info', args, kwargs)
# super(logging.Logger, self).info(*args, **kwargs)
logging.Logger.info(self, *args, **kwargs)
[docs] def warning(self, *args, **kwargs):
self._stack('warning', args, kwargs)
logging.Logger.warning(self, *args, **kwargs)
[docs] def error(self, *args, **kwargs):
self._stack('error', args, kwargs)
logging.Logger.error(self, *args, **kwargs)
# --- create the main logger ---
logging.setLoggerClass(Log)
# pylint: disable=C0103
logger = logging.getLogger('Alignak')
if hasattr(sys.stdout, 'isatty'):
CSH = ColorStreamHandler(sys.stdout)
if logger.name is not None:
CSH.setFormatter(DEFAULT_FORMATTER_NAMED)
else:
CSH.setFormatter(DEFAULT_FORMATTER)
logger.addHandler(CSH)
[docs]def naglog_result(level, result, *args):
"""
Function use for old Nag compatibility. We to set format properly for this call only.
Dirty Hack to keep the old format, we should have another logger and
use one for Alignak logs and another for monitoring data
"""
prev_formatters = []
for handler in logger.handlers:
prev_formatters.append(handler.formatter)
handler.setFormatter(NAG_FORMATTER)
log_fun = getattr(logger, level)
if log_fun:
log_fun(result)
for index, handler in enumerate(logger.handlers):
handler.setFormatter(prev_formatters[index])