Skip to content

Commit 8465b83

Browse files
author
vshepard
committed
Add logger
1 parent 7411d91 commit 8465b83

3 files changed

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

0 commit comments

Comments
 (0)