From 3382d23d85c9cdaae049453f3e414ada757b6431 Mon Sep 17 00:00:00 2001 From: s3lph Date: Sat, 14 Jul 2018 00:15:19 +0200 Subject: [PATCH 1/4] Basic logging implementation; still undocumented. --- matemat/webserver/config.py | 34 +++++++- matemat/webserver/httpd.py | 77 +++++++++++++++---- matemat/webserver/test/abstract_httpd_test.py | 7 ++ matemat/webserver/test/test_serve.py | 4 +- 4 files changed, 103 insertions(+), 19 deletions(-) diff --git a/matemat/webserver/config.py b/matemat/webserver/config.py index c2e1c5f..222f431 100644 --- a/matemat/webserver/config.py +++ b/matemat/webserver/config.py @@ -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 diff --git a/matemat/webserver/httpd.py b/matemat/webserver/httpd.py index 2d5194d..34da277 100644 --- a/matemat/webserver/httpd.py +++ b/matemat/webserver/httpd.py @@ -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) diff --git a/matemat/webserver/test/abstract_httpd_test.py b/matemat/webserver/test/abstract_httpd_test.py index 4b61417..d691caa 100644 --- a/matemat/webserver/test/abstract_httpd_test.py +++ b/matemat/webserver/test/abstract_httpd_test.py @@ -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): diff --git a/matemat/webserver/test/test_serve.py b/matemat/webserver/test/test_serve.py index 32853de..5db53b3 100644 --- a/matemat/webserver/test/test_serve.py +++ b/matemat/webserver/test/test_serve.py @@ -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 From a4967b1338e9eac616100d72212cf43d5b039db5 Mon Sep 17 00:00:00 2001 From: s3lph Date: Sat, 14 Jul 2018 12:16:46 +0200 Subject: [PATCH 2/4] Slightly improved logging config. --- matemat/webserver/config.py | 20 ++++++++++---------- matemat/webserver/httpd.py | 31 +++++++++++-------------------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/matemat/webserver/config.py b/matemat/webserver/config.py index 222f431..90593ac 100644 --- a/matemat/webserver/config.py +++ b/matemat/webserver/config.py @@ -8,7 +8,7 @@ import logging from configparser import ConfigParser -def parse_logging(symbolic_level: str, symbolic_target: str) -> Tuple[int, Union[str, io.TextIOWrapper]]: +def parse_logging(symbolic_level: str, symbolic_target: str) -> Tuple[int, logging.Handler]: level: int = logging.NOTSET try: level = int(symbolic_level) @@ -20,13 +20,13 @@ def parse_logging(symbolic_level: str, symbolic_target: str) -> Tuple[int, Union except ValueError: pass if symbolic_target == 'stderr': - target: Union[str, io.TextIOWrapper] = sys.stderr + target: logging.Handler = logging.StreamHandler(sys.stderr) elif symbolic_target == 'stdout': - target = sys.stdout + target = logging.StreamHandler(sys.stdout) elif symbolic_target == 'none': - target = None + target = logging.NullHandler() else: - target = os.path.abspath(os.path.expanduser(symbolic_target)) + target = logging.FileHandler(os.path.abspath(os.path.expanduser(symbolic_target))) return level, target @@ -48,9 +48,9 @@ def parse_config_file(paths: Union[str, Iterable[str]]) -> Dict[str, Any]: # Root directory of Jinja2 templates 'templateroot': '/var/matemat/templates', # Log level - 'loglevel': logging.INFO, + 'log_level': logging.INFO, # Log target: An IO stream (stderr, stdout, ...) or a filename - 'logtarget': sys.stderr, + 'log_handler': 'stderr', # Variables passed to pagelets 'pagelet_variables': dict() } @@ -76,9 +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['log_level'], config['log_handler'] =\ + parse_logging(parser['Matemat'].get('LogLevel', config['log_level']), + parser['Matemat'].get('LogTarget', config['log_handler'])) 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 diff --git a/matemat/webserver/httpd.py b/matemat/webserver/httpd.py index 34da277..6a211e9 100644 --- a/matemat/webserver/httpd.py +++ b/matemat/webserver/httpd.py @@ -1,9 +1,7 @@ from typing import Any, Callable, Dict, Tuple, Type, Union -from io import TextIOWrapper import logging -import sys import os import socket import mimetypes @@ -105,8 +103,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, + log_level: int, + log_handler: logging.Handler, bind_and_activate: bool = True) -> None: super().__init__(server_address, handler, bind_and_activate) # Resolve webroot directory @@ -122,16 +120,9 @@ class MatematHTTPServer(HTTPServer): # 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) + # Set up log handler + log_handler.setFormatter(logging.Formatter('%(asctime)s @ %(name)s [%(levelname)s]: %(message)s')) + self.logger.addHandler(log_handler) class MatematWebserver(object): @@ -155,8 +146,8 @@ class MatematWebserver(object): staticroot: str, templateroot: str, pagelet_variables: Dict[str, str], - loglevel: int, - logtarget: Union[str, TextIOWrapper]) -> None: + log_level: int, + log_handler: logging.Handler) -> None: """ Instantiate a MatematWebserver. @@ -165,8 +156,8 @@ 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. + :param log_level: The log level, as defined in the builtin logging module. + :param log_handler: The logging handler. """ # IPv4 address detection heuristic if ':' not in listen and '.' in listen: @@ -178,8 +169,8 @@ class MatematWebserver(object): staticroot, templateroot, pagelet_variables, - loglevel, - logtarget) + log_level, + log_handler) def start(self) -> None: """ From 8f82420d7f17d79a5d048e2bf0502633366acd27 Mon Sep 17 00:00:00 2001 From: s3lph Date: Sat, 14 Jul 2018 12:55:09 +0200 Subject: [PATCH 3/4] Logging config unit tests. --- matemat/webserver/config.py | 12 ++-- matemat/webserver/test/test_config.py | 92 ++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/matemat/webserver/config.py b/matemat/webserver/config.py index 90593ac..6c2ac4d 100644 --- a/matemat/webserver/config.py +++ b/matemat/webserver/config.py @@ -2,23 +2,19 @@ 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, logging.Handler]: - level: int = logging.NOTSET try: - level = int(symbolic_level) + level: int = int(symbolic_level) except ValueError: try: level = int(logging.getLevelName(symbolic_level)) - except KeyError: - pass except ValueError: - pass + raise ValueError(f'Unknown log level: {symbolic_level}') if symbolic_target == 'stderr': target: logging.Handler = logging.StreamHandler(sys.stderr) elif symbolic_target == 'stdout': @@ -50,7 +46,7 @@ def parse_config_file(paths: Union[str, Iterable[str]]) -> Dict[str, Any]: # Log level 'log_level': logging.INFO, # Log target: An IO stream (stderr, stdout, ...) or a filename - 'log_handler': 'stderr', + 'log_handler': logging.StreamHandler(), # Variables passed to pagelets 'pagelet_variables': dict() } @@ -78,7 +74,7 @@ def parse_config_file(paths: Union[str, Iterable[str]]) -> Dict[str, Any]: config['staticroot'] = parser['Matemat'].get('StaticPath', os.path.expanduser(config['staticroot'])) config['log_level'], config['log_handler'] =\ parse_logging(parser['Matemat'].get('LogLevel', config['log_level']), - parser['Matemat'].get('LogTarget', config['log_handler'])) + parser['Matemat'].get('LogTarget', 'stderr')) 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 diff --git a/matemat/webserver/test/test_config.py b/matemat/webserver/test/test_config.py index 2d37635..517bc29 100644 --- a/matemat/webserver/test/test_config.py +++ b/matemat/webserver/test/test_config.py @@ -5,6 +5,8 @@ from unittest import TestCase from unittest.mock import patch from io import StringIO +import logging +import sys from matemat.webserver import parse_config_file @@ -19,6 +21,9 @@ Port = 8080 StaticPath =/var/test/static TemplatePath= /var/test/templates +LogLevel = CRITICAL +LogTarget = /tmp/log/matemat_test.log + [Pagelets] Name=Matemat (Unit Test) @@ -34,6 +39,29 @@ Port=443 Name=Matemat (Unit Test 2) ''' +_LOG_NONE_CONFIG = ''' +[Matemat] +LogTarget=none +''' + +_LOG_STDOUT_CONFIG = ''' +[Matemat] +LogTarget=stdout +''' + +_LOG_STDERR_CONFIG = ''' +[Matemat] +LogTarget=stderr +''' + +_LOG_GARBAGE_PORT = ''' +[Matemat] +Port=iwanttobeavalidporttoo''' + +_LOG_GARBAGE_LOGLEVEL = ''' +[Matemat] +LogLevel=thisisnotaloglevel''' + class IterOpenMock: """ @@ -59,7 +87,7 @@ class IterOpenMock: class TestConfig(TestCase): - def test_parse_config_empty_defualt_values(self): + def test_parse_config_empty_default_values(self): """ Test that default values are set when reading an empty config file. """ @@ -72,12 +100,17 @@ class TestConfig(TestCase): self.assertIn('port', config) self.assertIn('staticroot', config) self.assertIn('templateroot', config) + self.assertIn('log_level', config) + self.assertIn('log_handler', config) self.assertIn('pagelet_variables', config) # Make sure all mandatory values are set to their default self.assertEqual('::', config['listen']) self.assertEqual(80, config['port']) self.assertEqual('/var/matemat/static', config['staticroot']) self.assertEqual('/var/matemat/templates', config['templateroot']) + self.assertEqual(logging.INFO, config['log_level']) + self.assertIsInstance(config['log_handler'], logging.StreamHandler) + self.assertEqual(sys.stderr, config['log_handler'].stream) self.assertIsInstance(config['pagelet_variables'], dict) self.assertEqual(0, len(config['pagelet_variables'])) @@ -94,6 +127,8 @@ class TestConfig(TestCase): self.assertIn('port', config) self.assertIn('staticroot', config) self.assertIn('templateroot', config) + self.assertIn('log_level', config) + self.assertIn('log_handler', config) self.assertIn('pagelet_variables', config) self.assertIn('Name', config['pagelet_variables']) self.assertIn('UploadDir', config['pagelet_variables']) @@ -103,6 +138,9 @@ class TestConfig(TestCase): self.assertEqual(8080, config['port']) self.assertEqual('/var/test/static', config['staticroot']) self.assertEqual('/var/test/templates', config['templateroot']) + self.assertEqual(logging.CRITICAL, config['log_level']) + self.assertIsInstance(config['log_handler'], logging.FileHandler) + self.assertEqual('/tmp/log/matemat_test.log', config['log_handler'].baseFilename) self.assertEqual('Matemat\n(Unit Test)', config['pagelet_variables']['Name']) self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir']) self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile']) @@ -134,3 +172,55 @@ class TestConfig(TestCase): self.assertEqual('Matemat (Unit Test 2)', config['pagelet_variables']['Name']) self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir']) self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile']) + + def test_parse_config_logging_none(self): + """ + Test that "LogTaget=none" disables logging. + """ + # Mock the open() function to return a config file example with disabled logging + with patch('builtins.open', return_value=StringIO(_LOG_NONE_CONFIG)): + # The filename is only a placeholder, file content is determined by mocking open + config = parse_config_file('test') + # Make sure the returned log handler is a null handler + self.assertIsInstance(config['log_handler'], logging.NullHandler) + + def test_parse_config_logging_stdout(self): + """ + Test that "LogTaget=stdout" logs to sys.stdout. + """ + # Mock the open() function to return a config file example with stdout logging + with patch('builtins.open', return_value=StringIO(_LOG_STDOUT_CONFIG)): + # The filename is only a placeholder, file content is determined by mocking open + config = parse_config_file('test') + # Make sure the returned log handler is a stdout handler + self.assertIsInstance(config['log_handler'], logging.StreamHandler) + self.assertEqual(sys.stdout, config['log_handler'].stream) + + def test_parse_config_logging_stderr(self): + """ + Test that "LogTaget=stderr" logs to sys.stderr. + """ + # Mock the open() function to return a config file example with stdout logging + with patch('builtins.open', return_value=StringIO(_LOG_STDERR_CONFIG)): + # The filename is only a placeholder, file content is determined by mocking open + config = parse_config_file('test') + # Make sure the returned log handler is a stdout handler + self.assertIsInstance(config['log_handler'], logging.StreamHandler) + self.assertEqual(sys.stderr, config['log_handler'].stream) + + def test_parse_config_garbage(self): + """ + Test that garbage config raises ValueErrors. + """ + # Mock the open() function to return a config file example with a non-numeric port + with patch('builtins.open', return_value=StringIO(_LOG_GARBAGE_PORT)): + # Make sure a ValueError is raised + with self.assertRaises(ValueError): + # The filename is only a placeholder, file content is determined by mocking open + parse_config_file('test') + # Mock the open() function to return a config file example with a non-numeric log level with invalid symbol + with patch('builtins.open', return_value=StringIO(_LOG_GARBAGE_LOGLEVEL)): + # Make sure a ValueError is raised + with self.assertRaises(ValueError): + # The filename is only a placeholder, file content is determined by mocking open + parse_config_file('test') From 4626f2233973e0cab9de2488cdff0a658f91eacd Mon Sep 17 00:00:00 2001 From: s3lph Date: Sat, 14 Jul 2018 13:14:53 +0200 Subject: [PATCH 4/4] config documentation --- .gitignore | 1 + doc | 2 +- matemat/webserver/config.py | 19 +++++++++++++++++++ matemat/webserver/httpd.py | 4 ++-- matemat/webserver/test/abstract_httpd_test.py | 3 ++- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index fef1fd9..925c5c5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ *.sqlite3 *.db **/matemat.conf +static/upload/ diff --git a/doc b/doc index d5dc5f7..c68df9d 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit d5dc5f794ad1b959b9d4dce47eeb6068e5a75115 +Subproject commit c68df9d86af1d8d0ebb6b6609efeef14f7103761 diff --git a/matemat/webserver/config.py b/matemat/webserver/config.py index 6c2ac4d..1913c4a 100644 --- a/matemat/webserver/config.py +++ b/matemat/webserver/config.py @@ -8,13 +8,30 @@ from configparser import ConfigParser def parse_logging(symbolic_level: str, symbolic_target: str) -> Tuple[int, logging.Handler]: + """ + Parse the LogLevel and LogTarget from configuration. + The LogLevel value may be either an integer or a log level literal defined in the Python builtin logging module. + The LogTarget value may be one of the following: + - `stderr` to log to error output (default) + - `stdout` to log to standard output + - `none` to disable logging + - Any other value is interpreted as a filename + + :param symbolic_level: The value for the LogLevel key. + :param symbolic_target: The value for the LogTarget key. + :return: A tuple of the log level (as an int) and the logging.Handler derived from the log target string. + """ try: + # Attempt to cast the log level into an int level: int = int(symbolic_level) except ValueError: try: + # If this fails, look up the name in the logging class level = int(logging.getLevelName(symbolic_level)) except ValueError: + # LogLevel value was neither an int nor a known literal raise ValueError(f'Unknown log level: {symbolic_level}') + # Special handling of the strings "stderr", "stdout" and "none" if symbolic_target == 'stderr': target: logging.Handler = logging.StreamHandler(sys.stderr) elif symbolic_target == 'stdout': @@ -22,7 +39,9 @@ def parse_logging(symbolic_level: str, symbolic_target: str) -> Tuple[int, loggi elif symbolic_target == 'none': target = logging.NullHandler() else: + # Fallback: Interpret string as filename target = logging.FileHandler(os.path.abspath(os.path.expanduser(symbolic_target))) + # Return log level and log target return level, target diff --git a/matemat/webserver/httpd.py b/matemat/webserver/httpd.py index 6a211e9..1aae08e 100644 --- a/matemat/webserver/httpd.py +++ b/matemat/webserver/httpd.py @@ -120,8 +120,8 @@ class MatematHTTPServer(HTTPServer): # Set up logger self.logger: logging.Logger = logging.getLogger('matemat.webserver') self.logger.setLevel(log_level) - # Set up log handler - log_handler.setFormatter(logging.Formatter('%(asctime)s @ %(name)s [%(levelname)s]: %(message)s')) + # Set up the log handler's (obtained from config parsing) format string + log_handler.setFormatter(logging.Formatter('%(asctime)s %(name)s [%(levelname)s]: %(message)s')) self.logger.addHandler(log_handler) diff --git a/matemat/webserver/test/abstract_httpd_test.py b/matemat/webserver/test/abstract_httpd_test.py index d691caa..0817d41 100644 --- a/matemat/webserver/test/abstract_httpd_test.py +++ b/matemat/webserver/test/abstract_httpd_test.py @@ -119,8 +119,9 @@ class MockServer: # Set up logger self.logger: logging.Logger = logging.getLogger('matemat unit test') self.logger.setLevel(0) + # Initalize a log handler to stderr and set the log format sh: logging.StreamHandler = logging.StreamHandler() - sh.setFormatter(logging.Formatter('%(asctime)s @ %(name)s [%(levelname)s]: %(message)s')) + sh.setFormatter(logging.Formatter('%(asctime)s %(name)s [%(levelname)s]: %(message)s')) self.logger.addHandler(sh)