Finalized the pgbouncemgr.Logger.

This commit is contained in:
Maurice Makaay 2019-12-02 11:53:54 +01:00
parent 9697589318
commit de5d75516d
6 changed files with 225 additions and 5 deletions

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# pylint: disable=no-self-use,too-few-public-methods
"""This module encapsulates the configuration for pgbouncemgr. """This module encapsulates the configuration for pgbouncemgr.
The configuration is read from a YAML file.""" The configuration is read from a YAML file."""
@ -35,6 +36,8 @@ class InvalidConfigValue(ConfigException):
class Config(): 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): def __init__(self, path):
self._set_defaults() self._set_defaults()
parser = self._read(path) parser = self._read(path)
@ -129,7 +132,7 @@ class Config():
def _build_node(self, node_name, data): def _build_node(self, node_name, data):
node = dict() node = dict()
if not data: if not data:
return return
for key in data: for key in data:
value = data[key] value = data[key]
if key in ["port", "connect_timeout"]: if key in ["port", "connect_timeout"]:

113
pgbouncemgr/logger.py Normal file
View File

@ -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)))

View File

@ -3,6 +3,8 @@
PostgreSQL cluster and that reconfigures pgbouncer when needed.""" PostgreSQL cluster and that reconfigures pgbouncer when needed."""
from argparse import ArgumentParser from argparse import ArgumentParser
from pgbouncemgr.logger import Logger
from pgbouncemgr.config import Config
DEFAULT_CONFIG = "/etc/pgbouncer/pgbouncemgr.yaml" DEFAULT_CONFIG = "/etc/pgbouncer/pgbouncemgr.yaml"
@ -12,7 +14,8 @@ DEFAULT_LOG_FACILITY = "LOG_LOCAL1"
def main(): def main():
"""Starts the pgbouncemgr main application.""" """Starts the pgbouncemgr main application."""
args = _parse_arguments() args = _parse_arguments()
print(repr(args)) config = Config(args.config)
log = Logger("pgbouncemgr", args.log_facility, args.debug, args.verbose)
def _parse_arguments(): def _parse_arguments():

View File

@ -24,7 +24,7 @@ class DuplicateNodeAdded(StateException):
already exists in the contained nodes.""" already exists in the contained nodes."""
def __init__(self, node_id): def __init__(self, node_id):
super().__init__( 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): class UnknownNodeRequested(StateException):
"""Raised when a request is made for a node that is not contained """Raised when a request is made for a node that is not contained
@ -349,7 +349,7 @@ class NodeState():
"config": self.config.export(), "config": self.config.export(),
"system_id": self.system_id, "system_id": self.system_id,
"timeline_id": self.timeline_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, "status": self.status,
"error": self.err, "error": self.err,
} }

View File

@ -5,7 +5,7 @@ import json
from datetime import datetime from datetime import datetime
from os import rename from os import rename
from os.path import isfile from os.path import isfile
from pgbouncemgr.log import format_exception_message from pgbouncemgr.logger import format_ex
STORE_ERROR = "error" STORE_ERROR = "error"
STORE_UPDATED = "updated" STORE_UPDATED = "updated"

101
tests/test_logger.py Normal file
View File

@ -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")