Skip to content

Commit 563abed

Browse files
author
vshepard
committed
Add logger
1 parent 7411d91 commit 563abed

3 files changed

Lines changed: 127 additions & 6 deletions

File tree

pg_probackup2/app.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
from .storage.fs_backup import TestBackupDir, FSTestBackupDir
1515
from .gdb import GDBobj
1616
from .init_helpers import init_params
17+
from .logger import log_command, patch_testgres_command_logging
18+
19+
patch_testgres_command_logging()
1720

1821
warning = """
1922
Wrong splint in show_pb
@@ -123,8 +126,7 @@ def run(self, command, gdb=False, old_binary=False, return_id=True, env=None,
123126
command, strcommand = self._add_options(command, skip_log_directory)
124127

125128
self.test_class.cmd = f"{binary_path} {strcommand}"
126-
if self.verbose:
127-
print(self.test_class.cmd)
129+
log_command(self.test_class.cmd)
128130

129131
cmdline = self._form_cmdline(binary_path, command)
130132

@@ -773,8 +775,7 @@ def run_binary(self, command, asynchronous=False, env=None):
773775
if not env:
774776
env = self.test_env
775777

776-
if self.verbose:
777-
print([' '.join(map(str, command))])
778+
log_command(command)
778779
try:
779780
if asynchronous:
780781
return subprocess.Popen(

pg_probackup2/gdb.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import unittest
66
from time import sleep
77

8+
from .logger import log_command
9+
810

911
class GdbException(Exception):
1012
def __init__(self, message="False"):
@@ -56,8 +58,7 @@ def __init__(self, cmd, env, attach=False):
5658
else:
5759
self.cmd = self.base_cmd + ['--args'] + cmd
5860

59-
if self.verbose:
60-
print([' '.join(map(str, self.cmd))])
61+
log_command(self.cmd)
6162

6263
self.proc = subprocess.Popen(
6364
self.cmd,

pg_probackup2/logger.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Usage:
3+
[pytest]
4+
log_cli = true
5+
log_cli_level = 25
6+
7+
or, without touching pytest config at all, by pointing the
8+
``PG_PROBACKUP_CMD_LOG`` environment variable at a file -- every executed
9+
command is then appended there as a bare, copy-paste-into-a-shell line.
10+
"""
11+
12+
import functools
13+
import logging
14+
import os
15+
16+
# The level between INFO (20) and WARNING (30)
17+
COMMAND = 25
18+
logging.addLevelName(COMMAND, 'COMMAND')
19+
20+
cmd_logger = logging.getLogger('pg_probackup2.commands')
21+
22+
_FILE_HANDLER_INSTALLED = False
23+
_TESTGRES_PATCHED = False
24+
25+
26+
def _maybe_add_file_handler():
27+
"""Attach a file handler to the command logger if PG_PROBACKUP_CMD_LOG is set."""
28+
global _FILE_HANDLER_INSTALLED
29+
if _FILE_HANDLER_INSTALLED:
30+
return
31+
_FILE_HANDLER_INSTALLED = True
32+
33+
path = os.environ.get('PG_PROBACKUP_CMD_LOG')
34+
if not path:
35+
return
36+
handler = logging.FileHandler(path)
37+
handler.setLevel(COMMAND)
38+
handler.setFormatter(logging.Formatter('%(message)s'))
39+
cmd_logger.addHandler(handler)
40+
if cmd_logger.level == logging.NOTSET or cmd_logger.level > COMMAND:
41+
cmd_logger.setLevel(COMMAND)
42+
43+
44+
def log_command(cmd):
45+
"""Echo a command that is executed.
46+
47+
Args:
48+
cmd: the command, either a list/tuple of arguments or an already
49+
assembled string.
50+
"""
51+
_maybe_add_file_handler()
52+
53+
if isinstance(cmd, (list, tuple)):
54+
line = ' '.join(map(str, cmd))
55+
else:
56+
line = str(cmd)
57+
58+
print([line])
59+
60+
# bare line for handlers/files (grep- and copy-paste-friendly)
61+
cmd_logger.log(COMMAND, '%s', line)
62+
63+
64+
def patch_testgres_command_logging():
65+
"""Echo testgres utility invocations (pg_ctl, initdb, pg_basebackup, ...) too.
66+
67+
testgres routes every external utility through ``utils.execute_utility2``.
68+
Several submodules (``node``, ``cache``, ``backup``, ...) grab their own
69+
reference at import time via ``from .utils import execute_utility2``, so it
70+
is not enough to replace the function in ``utils``. We therefore:
71+
72+
1. replace it in ``testgres.utils`` (so any *later* ``from .utils import``
73+
picks up the wrapper), and
74+
2. sweep every already-loaded ``testgres.*`` module and replace its
75+
``execute_utility2`` attribute when it still points at the original.
76+
77+
The wrapper only adds an echo and otherwise delegates unchanged, so it is
78+
safe across testgres versions; any failure to patch is non-fatal.
79+
"""
80+
global _TESTGRES_PATCHED
81+
if _TESTGRES_PATCHED:
82+
return
83+
_TESTGRES_PATCHED = True
84+
85+
try:
86+
import sys
87+
import testgres.utils as tg_utils
88+
except Exception as e: # testgres missing/broken -- nothing to patch
89+
logging.getLogger(__name__).debug(
90+
"Could not patch testgres for command logging: %s", e)
91+
return
92+
93+
original = getattr(tg_utils, 'execute_utility2', None)
94+
if original is None or getattr(original, '_pb2_logging_wrapper', False):
95+
return
96+
97+
@functools.wraps(original)
98+
def wrapper(os_ops, args, *a, **kw):
99+
try:
100+
log_command(args)
101+
except Exception:
102+
# echoing must never break the actual command execution
103+
pass
104+
return original(os_ops, args, *a, **kw)
105+
106+
wrapper._pb2_logging_wrapper = True
107+
108+
# 1) the source module (covers future ``from .utils import execute_utility2``)
109+
tg_utils.execute_utility2 = wrapper
110+
111+
# 2) every already-imported testgres submodule that copied the reference
112+
for name, module in list(sys.modules.items()):
113+
if module is None or not name.startswith('testgres'):
114+
continue
115+
try:
116+
if getattr(module, 'execute_utility2', None) is original:
117+
module.execute_utility2 = wrapper
118+
except Exception:
119+
pass

0 commit comments

Comments
 (0)