From f330c3f8c5338141a8601ac1ba12540aa20634a8 Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 11 Apr 2017 18:59:07 +0900 Subject: [PATCH] add logging based on issue #608, add default config --- environment.yml | 1 + requirements-dev.txt | 1 + requirements.txt | 1 + sanic/__main__.py | 8 ++++++- sanic/app.py | 43 +++++++++++++++++++++++++++++----- sanic/default.yml | 52 ++++++++++++++++++++++++++++++++++++++++++ sanic/defaultFilter.py | 13 +++++++++++ sanic/log.py | 1 + setup.py | 2 ++ 9 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 sanic/default.yml create mode 100644 sanic/defaultFilter.py diff --git a/environment.yml b/environment.yml index 298ea552..b22db97f 100644 --- a/environment.yml +++ b/environment.yml @@ -16,4 +16,5 @@ dependencies: - ujson>=1.35 - aiofiles>=0.3.0 - websockets>=3.2 + - pyyaml>=3.12 - https://github.com/channelcat/docutils-fork/zipball/master \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 28014eb6..2da823dc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,3 +8,4 @@ pytest tox ujson uvloop +pyyaml diff --git a/requirements.txt b/requirements.txt index e370b52f..49da374e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ httptools ujson uvloop websockets +pyyaml diff --git a/sanic/__main__.py b/sanic/__main__.py index 322d735d..b7f4c659 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -1,6 +1,9 @@ +import os + from argparse import ArgumentParser from importlib import import_module +from sanic import __path__ as lib_path from sanic.log import log from sanic.app import Sanic @@ -14,6 +17,8 @@ if __name__ == "__main__": help='location of keyfile for SSL.') parser.add_argument('--workers', dest='workers', type=int, default=1, ) parser.add_argument('--debug', dest='debug', action="store_true") + parser.add_argument('--log', dest='log_config_path', type=str, + default=os.path.join(lib_path[0], 'default.conf')) parser.add_argument('module') args = parser.parse_args() @@ -31,7 +36,8 @@ if __name__ == "__main__": app.run(host=args.host, port=args.port, workers=args.workers, debug=args.debug, - cert=args.cert, key=args.key) + cert=args.cert, key=args.key, + log_config_path=args.log_config_path) except ImportError: log.error("No module named {} found.\n" " Example File: project/sanic_server.py -> app\n" diff --git a/sanic/app.py b/sanic/app.py index 1d9a4987..aae79997 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1,6 +1,9 @@ import logging +import logging.config import re import warnings +import os +import yaml from asyncio import get_event_loop, ensure_future, CancelledError from collections import deque, defaultdict from functools import partial @@ -9,11 +12,12 @@ from traceback import format_exc from urllib.parse import urlencode, urlunparse from ssl import create_default_context +from sanic import __path__ as lib_path from sanic.config import Config from sanic.constants import HTTP_METHODS from sanic.exceptions import ServerError, URLBuildError, SanicException from sanic.handlers import ErrorHandler -from sanic.log import log +from sanic.log import log, netlog from sanic.response import HTTPResponse, StreamingHTTPResponse from sanic.router import Router from sanic.server import serve, serve_multiple, HttpProtocol @@ -26,10 +30,17 @@ from sanic.websocket import WebSocketProtocol, ConnectionClosed class Sanic: def __init__(self, name=None, router=None, error_handler=None, - load_env=True): + load_env=True, + log_config_path=os.path.join(lib_path[0], "default.conf")): + conf = None + if log_config_path and os.path.exists(log_config_path): + with open(log_config_path) as f: + conf = yaml.load(f) + conf.setdefault('version', 1) + logging.config.dictConfig(conf) # Only set up a default log handler if the # end-user application didn't set anything up. - if not logging.root.handlers and log.level == logging.NOTSET: + if not conf and log.level == logging.NOTSET: formatter = logging.Formatter( "%(asctime)s: %(levelname)s: %(message)s") handler = logging.StreamHandler() @@ -46,6 +57,7 @@ class Sanic: self.router = router or Router() self.error_handler = error_handler or ErrorHandler() self.config = Config(load_env=load_env) + self.log_config_path = log_config_path self.request_middleware = deque() self.response_middleware = deque() self.blueprints = {} @@ -442,7 +454,6 @@ class Sanic: # -------------------------------------------- # # Request Middleware # -------------------------------------------- # - request.app = self response = await self._run_request_middleware(request) # No middleware results @@ -490,6 +501,13 @@ class Sanic: log.exception( 'Exception occured in one of response middleware handlers' ) + if self.log_config_path: + netlog.info('', extra={ + "host": "%s:%d" % request.ip, + "request": "%s %s" % (request.method, request.query_string), + "status": response.status, + "byte": len(response.body) + }) # pass the response to the correct callback if isinstance(response, StreamingHTTPResponse): @@ -512,7 +530,8 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, ssl=None, sock=None, workers=1, loop=None, protocol=None, - backlog=100, stop_event=None, register_sys_signals=True): + backlog=100, stop_event=None, register_sys_signals=True, + log_config_path=os.path.join(lib_path[0], "default.yml")): """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. @@ -539,6 +558,11 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ + if log_config_path and os.path.exists(log_config_path): + with open(log_config_path) as f: + conf = yaml.load(f) + conf.setdefault('version', 1) + logging.config.dictConfig(conf) if protocol is None: protocol = (WebSocketProtocol if self.websocket_enabled else HttpProtocol) @@ -579,12 +603,19 @@ class Sanic: before_start=None, after_start=None, before_stop=None, after_stop=None, ssl=None, sock=None, loop=None, protocol=None, - backlog=100, stop_event=None): + backlog=100, stop_event=None, + log_config_path=os.path.join(lib_path[0], + "default.yml")): """Asynchronous version of `run`. NOTE: This does not support multiprocessing and is not the preferred way to run a Sanic application. """ + if log_config_path and os.path.exists(log_config_path): + with open(log_config_path) as f: + conf = yaml.load(f) + conf.setdefault('version', 1) + logging.config.dictConfig(conf) if protocol is None: protocol = (WebSocketProtocol if self.websocket_enabled else HttpProtocol) diff --git a/sanic/default.yml b/sanic/default.yml new file mode 100644 index 00000000..af69cd80 --- /dev/null +++ b/sanic/default.yml @@ -0,0 +1,52 @@ +version: 1 +filters: + access_filter: + (): sanic.defaultFilter.DefaultFilter + param: [0, 10, 20] + error_filter: + (): sanic.defaultFilter.DefaultFilter + param: [30, 40, 50] + +formatters: + simple: + format: '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s' + datefmt: '%Y-%m-%d %H:%M:%S' + access: + format: '%(asctime)s - [%(levelname)s][%(host)s]: %(request)s %(message)s %(status)s %(byte)d' + datefmt: '%Y-%m-%d %H:%M:%S' + +handlers: + internal: + class: logging.handlers.TimedRotatingFileHandler + filters: [access_filter] + formatter: simple + when: 'D' + interval: 1 + backupCount: 7 + filename: 'access.log' + + access: + class: logging.handlers.TimedRotatingFileHandler + filters: [access_filter] + formatter: access + when: 'D' + interval: 1 + backupCount: 7 + filename: 'access.log' + + error: + class: logging.handlers.TimedRotatingFileHandler + filters: [error_filter] + filename: 'error.log' + when: 'D' + interval: 1 + backupCount: 7 + formatter: simple + +loggers: + sanic: + level: DEBUG + handlers: [internal, error] + network: + level: DEBUG + handlers: [access, error] diff --git a/sanic/defaultFilter.py b/sanic/defaultFilter.py new file mode 100644 index 00000000..9e9508ec --- /dev/null +++ b/sanic/defaultFilter.py @@ -0,0 +1,13 @@ +import logging + + +class DefaultFilter(logging.Filter): + def __init__(self, param=None): + self.param = param + + def filter(self, record): + if self.param is None: + return True + if record.levelno in self.param: + return True + return False diff --git a/sanic/log.py b/sanic/log.py index 1b4d7334..cce4a64f 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,3 +1,4 @@ import logging log = logging.getLogger('sanic') +netlog = logging.getLogger('network') diff --git a/setup.py b/setup.py index deb52c27..e87afdc1 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ setup_kwargs = { 'description': ( 'A microframework based on uvloop, httptools, and learnings of flask'), 'packages': ['sanic'], + 'package_data': {'':['default.yml']}, 'platforms': 'any', 'classifiers': [ 'Development Status :: 2 - Pre-Alpha', @@ -46,6 +47,7 @@ requirements = [ ujson, 'aiofiles>=0.3.0', 'websockets>=3.2', + 'pyyaml>=3.12' ] if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): print("Installing without uJSON")