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 -*-
# 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"]:

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."""
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():

View File

@ -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,
}

View File

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

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