From de5d75516da6967fdbbc9b649411a1b1fb7538d5 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Mon, 2 Dec 2019 11:53:54 +0100 Subject: [PATCH] Finalized the pgbouncemgr.Logger. --- pgbouncemgr/config.py | 5 +- pgbouncemgr/logger.py | 113 +++++++++++++++++++++++++++++++++++++ pgbouncemgr/manager.py | 5 +- pgbouncemgr/state.py | 4 +- pgbouncemgr/state_store.py | 2 +- tests/test_logger.py | 101 +++++++++++++++++++++++++++++++++ 6 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 pgbouncemgr/logger.py create mode 100644 tests/test_logger.py diff --git a/pgbouncemgr/config.py b/pgbouncemgr/config.py index 02ce970..abaa014 100644 --- a/pgbouncemgr/config.py +++ b/pgbouncemgr/config.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=no-self-use,too-few-public-methods """This module encapsulates the configuration for pgbouncemgr. The configuration is read from a YAML file.""" @@ -35,6 +36,8 @@ class InvalidConfigValue(ConfigException): class Config(): + """This class is used to load the pgbouncemgr configuration from + a yaml file and to enrich it with default values where needed.""" def __init__(self, path): self._set_defaults() parser = self._read(path) @@ -129,7 +132,7 @@ class Config(): def _build_node(self, node_name, data): node = dict() if not data: - return + return for key in data: value = data[key] if key in ["port", "connect_timeout"]: diff --git a/pgbouncemgr/logger.py b/pgbouncemgr/logger.py new file mode 100644 index 0000000..dbf5b8f --- /dev/null +++ b/pgbouncemgr/logger.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# pylint: disable=missing-docstring,global-statement,no-self-use + +import syslog +import re + + +class LoggerException(Exception): + """Used for all exceptions that are raised from pgbouncemgr.logger.""" + +class SyslogLogTargetException(Exception): + """Used for all exceptions that are raised from the SyslogLogTarget.""" + + +class Logger(list): + def debug(self, msg): + for log_target in self: + log_target.debug(msg) + + def info(self, msg): + for log_target in self: + log_target.info(msg) + + def warning(self, msg): + for log_target in self: + log_target.warning(msg) + + def error(self, msg): + for log_target in self: + log_target.error(msg) + + +def format_ex(exception): + """Takes an exception as its input and returns a message that can be used + for logging purposes: single line, multiple spaces collapsed.""" + error = str(exception).strip() + error = re.sub(r' +', ' ', error) + error = re.sub(r'\r?\n+', ' ', error) + error = re.sub(r'\s{2,}', ' / ', error) + name = type(exception).__name__ + return "%s: %s" % (name, error) + + +class MemoryLogTarget(list): + """MemoryTarget is used to collect log messages in memory.""" + def debug(self, msg): + self.append(["DEBUG", msg]) + + def info(self, msg): + self.append(["INFO", msg]) + + def warning(self, msg): + self.append(["WARNING", msg]) + + def error(self, msg): + self.append(["ERROR", msg]) + + +class ConsoleLogTarget(): + """ConsoleTarget is used to send log messages to the console.""" + def __init__(self, verbose, debug): + self.verbose_enabled = verbose or debug + self.debug_enabled = debug + + def debug(self, msg): + if self.debug_enabled: + print("[DEBUG] %s" % msg) + + def info(self, msg): + if self.verbose_enabled: + print("[INFO] %s" % msg) + + def warning(self, msg): + if self.verbose_enabled: + print("[WARNING] %s" % msg) + + def error(self, msg): + if self.verbose_enabled: + print("[ERROR] %s" % msg) + + +class SyslogLogTarget(): + """Syslogtarget is used to send log messages to syslog.""" + def __init__(self, ident, facility): + facility = self._resolve_facility(facility) + syslog.openlog(ident=ident, facility=facility) + + def debug(self, msg): + syslog.syslog(syslog.LOG_DEBUG, msg) + + def info(self, msg): + syslog.syslog(syslog.LOG_INFO, msg) + + def warning(self, msg): + syslog.syslog(syslog.LOG_WARNING, msg) + + def error(self, msg): + syslog.syslog(syslog.LOG_ERR, msg) + + def _resolve_facility(self, facility): + """Resolve the provided facility. When it is numeric, then use the + number. Otherwise try to look it up in the syslog lib by name.""" + try: + return int(facility) + except ValueError: + pass + + try: + return int(getattr(syslog, str.upper(facility))) + except (AttributeError, ValueError): + raise SyslogLogTargetException( + "Invalid syslog facility provided (facility=%s)" % + (facility if facility is None else repr(facility))) diff --git a/pgbouncemgr/manager.py b/pgbouncemgr/manager.py index 10c1fc6..2ed299b 100644 --- a/pgbouncemgr/manager.py +++ b/pgbouncemgr/manager.py @@ -3,6 +3,8 @@ PostgreSQL cluster and that reconfigures pgbouncer when needed.""" from argparse import ArgumentParser +from pgbouncemgr.logger import Logger +from pgbouncemgr.config import Config DEFAULT_CONFIG = "/etc/pgbouncer/pgbouncemgr.yaml" @@ -12,7 +14,8 @@ DEFAULT_LOG_FACILITY = "LOG_LOCAL1" def main(): """Starts the pgbouncemgr main application.""" args = _parse_arguments() - print(repr(args)) + config = Config(args.config) + log = Logger("pgbouncemgr", args.log_facility, args.debug, args.verbose) def _parse_arguments(): diff --git a/pgbouncemgr/state.py b/pgbouncemgr/state.py index 4361202..d09a3bd 100644 --- a/pgbouncemgr/state.py +++ b/pgbouncemgr/state.py @@ -24,7 +24,7 @@ class DuplicateNodeAdded(StateException): already exists in the contained nodes.""" def __init__(self, node_id): super().__init__( - "Node already exists in state (duplicate node_id=%s)" % node_id) + "Node already exists in state (duplicate node_id=%s)" % node_id) class UnknownNodeRequested(StateException): """Raised when a request is made for a node that is not contained @@ -349,7 +349,7 @@ class NodeState(): "config": self.config.export(), "system_id": self.system_id, "timeline_id": self.timeline_id, - "is_leader": self.parent_state._leader_node_id == self.node_id, + "is_leader": self.parent_state.leader_node_id == self.node_id, "status": self.status, "error": self.err, } diff --git a/pgbouncemgr/state_store.py b/pgbouncemgr/state_store.py index f2b1fbc..9b13e18 100644 --- a/pgbouncemgr/state_store.py +++ b/pgbouncemgr/state_store.py @@ -5,7 +5,7 @@ import json from datetime import datetime from os import rename from os.path import isfile -from pgbouncemgr.log import format_exception_message +from pgbouncemgr.logger import format_ex STORE_ERROR = "error" STORE_UPDATED = "updated" diff --git a/tests/test_logger.py b/tests/test_logger.py new file mode 100644 index 0000000..9219128 --- /dev/null +++ b/tests/test_logger.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +import unittest +from pgbouncemgr.logger import * + + +class LoggerTests(unittest.TestCase): + def test_Logger_CanBeEmpty(self): + """A NULL-logger is a logger that does sink all the messages that + come in. No messages are written or collected whatsoever.""" + logger = Logger() + send_logs(logger) + + self.assertTrue(len(logger) == 0) + + def test_Logger_WritesToAllTargets(self): + logger = Logger() + logger.append(MemoryLogTarget()) + logger.append(MemoryLogTarget()) + send_logs(logger) + + self.assertEqual([ + [ + ['DEBUG', 'Debug me'], + ['INFO', 'Inform me'], + ['WARNING', 'Warn me'], + ['ERROR', 'Break me'] + ], [ + ['DEBUG', 'Debug me'], + ['INFO', 'Inform me'], + ['WARNING', 'Warn me'], + ['ERROR', 'Break me'] + ] + ], logger) + + +class SyslogLargetTests(unittest.TestCase): + def test_GivenInvalidFacility_ExceptionIsRaised(self): + with self.assertRaises(SyslogLogTargetException) as context: + SyslogLogTarget("my app", "LOG_WRONG") + self.assertIn("Invalid syslog facility provided", str(context.exception)) + self.assertIn("'LOG_WRONG'", str(context.exception)) + + def test_GivenValidFacility_LogTargetIsCreated(self): + SyslogLogTarget("my app", "LOG_LOCAL0") + + +class ConsoleLogTargetTests(unittest.TestCase): + def test_CanCreateSilentConsoleLogger(self): + console = ConsoleLogTarget(False, False) + + self.assertFalse(console.verbose_enabled) + self.assertFalse(console.debug_enabled) + + def test_CanCreateVerboseConsoleLogger(self): + console = ConsoleLogTarget(True, False) + + self.assertTrue(console.verbose_enabled) + self.assertFalse(console.debug_enabled) + + def test_CanCreateDebuggingConsoleLogger(self): + console = ConsoleLogTarget(False, True) + + self.assertTrue(console.verbose_enabled) + self.assertTrue(console.debug_enabled) + + def test_CanCreateVerboseDebuggingConsoleLogger(self): + """Basically the same as a debugging console logger, + since the debug flag enables the verbose flag as well.""" + console = ConsoleLogTarget(True, True) + + self.assertTrue(console.verbose_enabled) + self.assertTrue(console.debug_enabled) + + +class FormatExTests(unittest.TestCase): + def test_SimpleException(self): + exception = Exception("It crashed!") + msg = format_ex(exception) + + self.assertEqual("Exception: It crashed!", msg) + + def test_ComplexException(self): + exception = Exception( + "It crashed!\n" + + "The error was around line 10\n" + + " \t A. memory broken? \r\n" + + " \t B. cpu broken?\r\n" + + "\n") + msg = format_ex(exception) + + self.assertEqual( + "Exception: It crashed! The error was around line 10 / " + + "A. memory broken? / B. cpu broken?", msg) + + +def send_logs(logger): + logger.debug("Debug me") + logger.info("Inform me") + logger.warning("Warn me") + logger.error("Break me")