Basic logging implementation; still undocumented.

This commit is contained in:
s3lph 2018-07-14 00:15:19 +02:00
parent b12535bc93
commit 3382d23d85
4 changed files with 103 additions and 19 deletions

View file

@ -1,10 +1,35 @@
from typing import Any, Dict, Iterable, List, Union
from typing import Any, Dict, Iterable, List, Tuple, Union
import os
import io
import sys
import logging
from configparser import ConfigParser
def parse_logging(symbolic_level: str, symbolic_target: str) -> Tuple[int, Union[str, io.TextIOWrapper]]:
level: int = logging.NOTSET
try:
level = int(symbolic_level)
except ValueError:
try:
level = int(logging.getLevelName(symbolic_level))
except KeyError:
pass
except ValueError:
pass
if symbolic_target == 'stderr':
target: Union[str, io.TextIOWrapper] = sys.stderr
elif symbolic_target == 'stdout':
target = sys.stdout
elif symbolic_target == 'none':
target = None
else:
target = os.path.abspath(os.path.expanduser(symbolic_target))
return level, target
def parse_config_file(paths: Union[str, Iterable[str]]) -> Dict[str, Any]:
"""
Parse the configuration file at the given path.
@ -22,6 +47,10 @@ def parse_config_file(paths: Union[str, Iterable[str]]) -> Dict[str, Any]:
'staticroot': '/var/matemat/static',
# Root directory of Jinja2 templates
'templateroot': '/var/matemat/templates',
# Log level
'loglevel': logging.INFO,
# Log target: An IO stream (stderr, stdout, ...) or a filename
'logtarget': sys.stderr,
# Variables passed to pagelets
'pagelet_variables': dict()
}
@ -47,6 +76,9 @@ def parse_config_file(paths: Union[str, Iterable[str]]) -> Dict[str, Any]:
config['listen'] = parser['Matemat'].get('Address', config['listen'])
config['port'] = int(parser['Matemat'].get('Port', config['port']))
config['staticroot'] = parser['Matemat'].get('StaticPath', os.path.expanduser(config['staticroot']))
config['loglevel'], config['logtarget'] =\
parse_logging(parser['Matemat'].get('LogLevel', config['loglevel']),
parser['Matemat'].get('LogTarget', config['logtarget']))
config['templateroot'] = parser['Matemat'].get('TemplatePath', os.path.expanduser(config['templateroot']))
# Read all values from the [Pagelets] section, if present. These values are passed to pagelet functions

View file

@ -1,8 +1,9 @@
from typing import Any, Callable, Dict, Tuple, Type, Union
import traceback
from io import TextIOWrapper
import logging
import sys
import os
import socket
import mimetypes
@ -25,9 +26,6 @@ from matemat.webserver.util import parse_args
# Enable IPv6 support (IPv6/IPv4 dual-stack support should be implicitly enabled)
TCPServer.address_family = socket.AF_INET6
# Redirect internal logging to somewhere else, or, for now, silently discard (TODO: logger will come later)
BaseHTTPRequestHandler.log_request = lambda self, code='-', size='-': None
BaseHTTPRequestHandler.log_error = lambda self, fstring='', *args: None
# Dictionary to hold registered pagelet paths and their handler functions
_PAGELET_PATHS: Dict[str, Callable[[str, # HTTP method (GET, POST, ...)
@ -107,6 +105,8 @@ class MatematHTTPServer(HTTPServer):
staticroot: str,
templateroot: str,
pagelet_variables: Dict[str, str],
log_level: int = logging.INFO,
log_target: Union[str, TextIOWrapper] = sys.stderr,
bind_and_activate: bool = True) -> None:
super().__init__(server_address, handler, bind_and_activate)
# Resolve webroot directory
@ -119,6 +119,19 @@ class MatematHTTPServer(HTTPServer):
self.jinja_env: jinja2.Environment = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.abspath(templateroot))
)
# Set up logger
self.logger: logging.Logger = logging.getLogger('matemat.webserver')
self.logger.setLevel(log_level)
# Set up log handler that logs to
sh: logging.Handler = logging.StreamHandler(sys.stderr)
if log_target is None:
sh: logging.Handler = logging.NullHandler()
elif isinstance(log_target, TextIOWrapper):
sh = logging.StreamHandler(log_target)
elif isinstance(log_target, str):
sh = logging.FileHandler(log_target)
sh.setFormatter(logging.Formatter('%(asctime)s @ %(name)s [%(levelname)s]: %(message)s'))
self.logger.addHandler(sh)
class MatematWebserver(object):
@ -141,7 +154,9 @@ class MatematWebserver(object):
port: int,
staticroot: str,
templateroot: str,
pagelet_variables: Dict[str, str]) -> None:
pagelet_variables: Dict[str, str],
loglevel: int,
logtarget: Union[str, TextIOWrapper]) -> None:
"""
Instantiate a MatematWebserver.
@ -150,13 +165,21 @@ class MatematWebserver(object):
:param staticroot: Path to the static webroot directory.
:param templateroot: Path to the Jinja2 templates root directory.
:param pagelet_variables: Dictionary of variables to pass to pagelet functions.
:param loglevel: The log level, as defined in the builtin logging module.
:param logtarget: The logging target.
"""
# IPv4 address detection heuristic
if ':' not in listen and '.' in listen:
# Rewrite IPv4 address to IPv6-mapped form
listen = f'::ffff:{listen}'
# Create the http server
self._httpd = MatematHTTPServer((listen, port), HttpHandler, staticroot, templateroot, pagelet_variables)
self._httpd = MatematHTTPServer((listen, port),
HttpHandler,
staticroot,
templateroot,
pagelet_variables,
loglevel,
logtarget)
def start(self) -> None:
"""
@ -198,10 +221,12 @@ class HttpHandler(BaseHTTPRequestHandler):
# If there is no active session, create a new session ID
if session_id is None or session_id not in self.server.session_vars:
session_id = str(uuid4())
self.server.logger.debug('Started session %s', session_id)
# Check for session timeout
if session_id in self.server.session_vars and self.server.session_vars[session_id][0] < now:
self._end_session(session_id)
self.server.logger.debug('Session %s timed out', session_id)
raise TimeoutError('Session timed out.')
# Update or initialize the session timeout
if session_id not in self.server.session_vars:
@ -362,15 +387,21 @@ class HttpHandler(BaseHTTPRequestHandler):
# Special handling for some errors
except HttpException as e:
self.send_error(e.status, e.title, e.message)
except PermissionError:
if 500 <= e.status < 600:
self.send_error(500, 'Internal Server Error')
self.server.logger.exception('', exc_info=e)
else:
self.server.logger.debug('', exc_info=e)
except PermissionError as e:
self.send_error(403, 'Forbidden')
except ValueError:
self.server.logger.debug('', exc_info=e)
except ValueError as e:
self.send_error(400, 'Bad Request')
self.server.logger.debug('', exc_info=e)
except BaseException as e:
# Generic error handling
self.send_error(500, 'Internal Server Error')
print(e)
traceback.print_tb(e.__traceback__)
self.server.logger.exception('', e.args, e)
# noinspection PyPep8Naming
def do_POST(self) -> None:
@ -389,16 +420,21 @@ class HttpHandler(BaseHTTPRequestHandler):
self._handle('POST', path, args)
# Special handling for some errors
except HttpException as e:
self.send_error(e.status, e.title, e.message)
except PermissionError:
if 500 <= e.status < 600:
self.send_error(500, 'Internal Server Error')
self.server.logger.exception('', exc_info=e)
else:
self.server.logger.debug('', exc_info=e)
except PermissionError as e:
self.send_error(403, 'Forbidden')
except ValueError:
self.server.logger.debug('', exc_info=e)
except ValueError as e:
self.send_error(400, 'Bad Request')
self.server.logger.debug('', exc_info=e)
except BaseException as e:
# Generic error handling
self.send_error(500, 'Internal Server Error')
print(e)
traceback.print_tb(e.__traceback__)
self.server.logger.exception('', e.args, e)
@property
def session_vars(self) -> Dict[str, Any]:
@ -408,3 +444,12 @@ class HttpHandler(BaseHTTPRequestHandler):
:return: Dictionary of named session variables.
"""
return self.server.session_vars[self.session_id][1]
def log_request(self, code='-', size='-'):
self.server.logger.info('%s %s from %s', self.requestline, code, self.client_address)
def log_error(self, fmt: str, *args):
self.server.logger.warning(f'{self.requestline} from {self.client_address}: {fmt}', *args)
def log_message(self, fmt, *args):
self.server.logger.info(fmt, *args)

View file

@ -9,6 +9,7 @@ from abc import ABC
from datetime import datetime
from http.server import HTTPServer
import logging
import jinja2
from matemat.webserver import pagelet, RequestArguments
@ -115,6 +116,12 @@ class MockServer:
self.jinja_env = jinja2.Environment(
loader=jinja2.DictLoader({'test.txt': 'Hello, {{ what }}!'})
)
# Set up logger
self.logger: logging.Logger = logging.getLogger('matemat unit test')
self.logger.setLevel(0)
sh: logging.StreamHandler = logging.StreamHandler()
sh.setFormatter(logging.Formatter('%(asctime)s @ %(name)s [%(levelname)s]: %(message)s'))
self.logger.addHandler(sh)
class MockSocket(bytes):

View file

@ -61,7 +61,7 @@ def serve_test_pagelet_fail(method: str,
pagelet_variables: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
session_vars['test'] = 'hello, world!'
headers['Content-Type'] = 'text/plain'
raise HttpException()
raise HttpException(599, 'Error expected during unit testing')
class TestServe(AbstractHttpdTest):
@ -111,7 +111,7 @@ class TestServe(AbstractHttpdTest):
packet = self.client_sock.get_response()
# Make sure an error is raised
self.assertEqual(500, packet.statuscode)
self.assertEqual(599, packet.statuscode)
def test_serve_pagelet_redirect(self):
# Call the test pagelet that redirects to another path