Source code for logger_server.server

#!/usr/bin/env python
#coding=utf-8
"""
@Project: LoggerServer
@Filename: server.py
@Author: Kehr <kehr.china@gmail.com>
@Created Date:   2017-11-14T19:20:37+08:00
@Last modified time: 2017-11-20T16:19:06+08:00
@License: Apache License <http://www.apache.org/licenses/LICENSE-2.0>
"""
import os
import sys
import socket
import pickle
import struct
import functools
import logging
import logging.handlers
import datetime
import argparse
import json
import tornado.ioloop

from tornado import gen
from tornado.log import LogFormatter
from tornado.tcpserver import TCPServer
from tornado.iostream import StreamClosedError

version = __version__ = '1.0.5'

DEFAULT_DATE_FORMAT = '%Y-%m-%d %H:%M:%S.%f'
DEFAULT_LOG_FORMAT = '[%(levelname)1.1s %(asctime)s %(ip)s %(name)s %(module)s:%(lineno)d] %(message)s'


[docs]class AttrDict(dict): """Attribute dict Make ``dict`` value can be accessed by attribute:: from logger_server import AttrDict people = AttrDict({'name':'Joe', 'age': 23}) print people.name print people.age """ def __setattr__(self, key, value): self[key] = value def __getattr__(self, key): return self[key]
[docs]class LoggerFormatter(LogFormatter): """format """
[docs] def formatTime(self, record, datefmt=None): """Rewrite default `formatTime` to support `%f` :arg logging.LogRecord record: ``logging.LogRecord`` object which Contains all the information pertinent to the event being logged. :arg datefmt: Datetime format string. """ ct = datetime.datetime.fromtimestamp(record.created) datefmt = DEFAULT_DATE_FORMAT if not datefmt else datefmt return ct.strftime(datefmt)
[docs]class LoggerServer(TCPServer): """A logging server serve for ``logging.handlers.SocketHandler`` :arg argparse.Namespace config: The command line parse result. Reference: `tornado.tcpserver.TCPServer <http://www.tornadoweb.org/en/stable/tcpserver.html#tornado.tcpserver.TCPServer>`_ """ def __init__(self, config=None, *args, **kwargs): super(LoggerServer, self).__init__(*args, **kwargs) self.logger = logging.getLogger('LoggerServer') self.options = AttrDict() self.init_config_options(config) self._init_root_logger() self.ioloop = tornado.ioloop.IOLoop.current()
[docs] @gen.coroutine def handle_stream(self, stream, address): """Reslove Socket stream data. :arg stream: scoket stream :arg tuple address: client resquest ip and port ``(ip, port)`` """ self.request_ip, self.request_port = address while True: try: chunk = yield stream.read_bytes(4) if len(chunk) < 4: break slen = struct.unpack('>L', chunk)[0] chunk = yield stream.read_bytes(slen) while len(chunk) < slen: chunk += yield stream.read_bytes(slen - len(chunk)) except StreamClosedError: break data = pickle.loads(chunk) self.handleLogRecord(logging.makeLogRecord(data), address)
[docs] def handleLogRecord(self, record, address): """Config the decoded `record` This will add ``ip`` and ``port`` param by ``logging.Filter``. You can add them into logging format:: [%(levelname)1.1s %(asctime)s %(ip)s %(port)s %(name)s %(module)s:%(lineno)d] %(message)s :arg logging.LogRecord record: ``logging.LogRecord`` object which Contains all the information pertinent to the event being logged. :arg tuple address: client resquest ip and port `(ip, port)` """ class ContextFilter(logging.Filter): """为日志增加额外信息""" def filter(self, record): record.ip, record.port = address return True logger = logging.getLogger(record.name) if self.options.separated is True: # Add `TimedRotatingFileHandler` for empty `logger.handlers` if len(logger.handlers) == 0: self.add_logger_file_handler(logger, record.name) else: for handler in logger.handlers: if not isinstance(handler, logging.handlers.TimedRotatingFileHandler): self.add_logger_file_handler(logger, record.name) logger.addFilter(ContextFilter()) logger.handle(record)
[docs] def start(self): """Start LoggerServer""" if self.options.conf: print '> Use config: {}'.format(self.options.conf) print '> LoggerServer is binding on 0.0.0.0:{}, pid:{}'.format(self.options.port, os.getpid()) self.listen(self.options.port) self.ioloop.start()
[docs] def init_config_options(self, config=None): """Initialize `logger-server` config. All settings will be merged in ``self.options`` :arg argparse.Namespace config: The command line parse result. """ if config is None: config = parse_command() if config.version: print 'version: {}'.format(version) sys.exit(0) self.options.update(config.__dict__) if config.conf: self.options.update(self._parse_config_file(config.conf))
def _init_root_logger(self): """init global logger""" if self.options.separated is False: self.add_logger_file_handler()
[docs] def add_logger_file_handler(self, logger=None, name=''): """Add `TimedRotatingFileHandler` for every single logger""" if logger is None: logger = logging.getLogger() # Disable propagate logger.propagate = False if name and name.strip(): filename = '{}.{}.log'.format(self.options.log, name) else: filename = '{}.log'.format(self.options.log) channel = logging.handlers.TimedRotatingFileHandler( filename=filename, when=self.options.when, interval=self.options.interval, backupCount=self.options.backup) channel.setFormatter(LoggerFormatter(fmt=self.options.fmt, datefmt=self.options.datefmt, color=False)) logger.addHandler(channel)
def _parse_config_file(self, path): """Parses the config file at the given path""" with open(os.path.abspath(path)) as f: return json.load(f, encoding='utf-8')
[docs]def parse_command(): """Parses the command args""" p = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='LoggerServer help documentation') p.add_argument('-f', '--conf', required=False, default=None, help='The config file path for LoggerServer.') p.add_argument('-p', '--port', required=False, type=int, default=9876, help='LoggerServer port.') p.add_argument('--log', required=False, default='./logserver', help='The log output file.') p.add_argument('--when', required=False, default='midnight', help=('specify the type of TimedRotatingFileHandler interval.' "other options:('S', 'M', 'H', 'D', 'W0'-'W6')")) p.add_argument('--interval', required=False, type=int, default=1, help='The interval value of timed rotating.') p.add_argument('--backup', required=False, type=int, default=14, help='Number of log files to keep.') p.add_argument('--fmt', required=False, default=DEFAULT_LOG_FORMAT, help='The log output formatter of logging.') p.add_argument('--datefmt', required=False, default=DEFAULT_DATE_FORMAT, help='The log output date formatter of logging.') p.add_argument('--detached', required=False, action='store_true', help='Running on detached mode.') p.add_argument('--version', required=False, action='store_true', help='Print version code.') p.add_argument('--separated', required=False, action='store_true', help='Output log into every single file. logger name will in the suffix of every logger file.') return p.parse_args()
[docs]def main(): """LoggerServer Entry""" config = parse_command() if config.detached is True: try: if os.fork() > 0: sys.exit(0) os.setsid() os.umask(0) except OSError as e: print 'Create daemon mode failed!' sys.exit(1) LoggerServer(config).start()
if __name__ == '__main__': main()