Skip to content

Commit 1c3eb04

Browse files
committed
Revert "Standardize pgBackRest logging to use stderr consistently"
This reverts commit 13a9ac4. Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
1 parent 13a9ac4 commit 1c3eb04

File tree

3 files changed

+66
-60
lines changed

3 files changed

+66
-60
lines changed

src/backups.py

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
PGBACKREST_CONF_PATH,
4040
PGBACKREST_CONFIGURATION_FILE,
4141
PGBACKREST_EXECUTABLE,
42-
PGBACKREST_LOG_LEVEL_STDERR,
4342
PGBACKREST_LOGROTATE_FILE,
4443
PGBACKREST_LOGS_PATH,
4544
POSTGRESQL_DATA_PATH,
@@ -185,13 +184,7 @@ def can_use_s3_repository(self) -> tuple[bool, str]:
185184

186185
try:
187186
return_code, stdout, stderr = self._execute_command(
188-
[
189-
PGBACKREST_EXECUTABLE,
190-
PGBACKREST_CONFIGURATION_FILE,
191-
PGBACKREST_LOG_LEVEL_STDERR,
192-
"info",
193-
"--output=json",
194-
],
187+
[PGBACKREST_EXECUTABLE, PGBACKREST_CONFIGURATION_FILE, "info", "--output=json"],
195188
timeout=30,
196189
)
197190
except TimeoutExpired as e:
@@ -203,7 +196,7 @@ def can_use_s3_repository(self) -> tuple[bool, str]:
203196

204197
else:
205198
if return_code != 0:
206-
extracted_error = self._extract_error_message(stderr)
199+
extracted_error = self._extract_error_message(stdout, stderr)
207200
logger.error(f"Failed to run pgbackrest: {extracted_error}")
208201
return False, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE
209202

@@ -367,25 +360,23 @@ def _execute_command(
367360
return process.returncode, process.stdout.decode(), process.stderr.decode()
368361

369362
@staticmethod
370-
def _extract_error_message(stderr: str) -> str:
371-
"""Extract key error message from pgBackRest stderr output.
372-
373-
Since we standardize all pgBackRest commands to use --log-level-stderr=warn,
374-
all errors and warnings are consistently written to stderr. This makes error
375-
extraction predictable and avoids potential log duplication issues.
363+
def _extract_error_message(stdout: str, stderr: str) -> str:
364+
"""Extract key error message from pgBackRest output.
376365
377366
Args:
378-
stderr: Standard error from pgBackRest command containing errors/warnings.
367+
stdout: Standard output from pgBackRest command.
368+
stderr: Standard error from pgBackRest command.
379369
380370
Returns:
381-
Extracted error message from stderr, prioritizing ERROR/WARN lines.
371+
Extracted error message, prioritizing ERROR/WARN lines from output.
382372
"""
383-
if not stderr.strip():
373+
combined_output = f"{stdout}\n{stderr}".strip()
374+
if not combined_output:
384375
return f"Unknown error occurred. Please check the logs at {PGBACKREST_LOGS_PATH}"
385376

386-
# Extract lines with ERROR or WARN markers from pgBackRest stderr output
377+
# Extract lines with ERROR or WARN markers from pgBackRest output
387378
error_lines = []
388-
for line in stderr.splitlines():
379+
for line in combined_output.splitlines():
389380
if "ERROR:" in line or "WARN:" in line:
390381
# Clean up the line by removing debug prefixes like "P00 ERROR:"
391382
cleaned = re.sub(r"^.*?(ERROR:|WARN:)", r"\1", line).strip()
@@ -395,8 +386,13 @@ def _extract_error_message(stderr: str) -> str:
395386
if error_lines:
396387
return "; ".join(error_lines)
397388

398-
# Otherwise return the last non-empty line from stderr
399-
return stderr.strip().splitlines()[-1]
389+
# Otherwise return the last non-empty line from stderr or stdout
390+
if stderr.strip():
391+
return stderr.strip().splitlines()[-1]
392+
if stdout.strip():
393+
return stdout.strip().splitlines()[-1]
394+
395+
return f"Unknown error occurred. Please check the logs at {PGBACKREST_LOGS_PATH}"
400396

401397
def _format_backup_list(self, backup_list) -> str:
402398
"""Formats provided list of backups as a table."""
@@ -442,12 +438,11 @@ def _generate_backup_list_output(self) -> str:
442438
return_code, output, stderr = self._execute_command([
443439
PGBACKREST_EXECUTABLE,
444440
PGBACKREST_CONFIGURATION_FILE,
445-
PGBACKREST_LOG_LEVEL_STDERR,
446441
"info",
447442
"--output=json",
448443
])
449444
if return_code != 0:
450-
extracted_error = self._extract_error_message(stderr)
445+
extracted_error = self._extract_error_message(output, stderr)
451446
raise ListBackupsError(f"Failed to list backups with error: {extracted_error}")
452447

453448
backups = json.loads(output)[0]["backup"]
@@ -515,12 +510,11 @@ def _list_backups(self, show_failed: bool, parse=True) -> dict[str, tuple[str, s
515510
return_code, output, stderr = self._execute_command([
516511
PGBACKREST_EXECUTABLE,
517512
PGBACKREST_CONFIGURATION_FILE,
518-
PGBACKREST_LOG_LEVEL_STDERR,
519513
"info",
520514
"--output=json",
521515
])
522516
if return_code != 0:
523-
extracted_error = self._extract_error_message(stderr)
517+
extracted_error = self._extract_error_message(output, stderr)
524518
raise ListBackupsError(f"Failed to list backups with error: {extracted_error}")
525519

526520
repository_info = next(iter(json.loads(output)), None)
@@ -551,13 +545,12 @@ def _list_timelines(self) -> dict[str, tuple[str, str]]:
551545
return_code, output, stderr = self._execute_command([
552546
PGBACKREST_EXECUTABLE,
553547
PGBACKREST_CONFIGURATION_FILE,
554-
PGBACKREST_LOG_LEVEL_STDERR,
555548
"repo-ls",
556549
"--recurse",
557550
"--output=json",
558551
])
559552
if return_code != 0:
560-
extracted_error = self._extract_error_message(stderr)
553+
extracted_error = self._extract_error_message(output, stderr)
561554
raise ListBackupsError(f"Failed to list repository with error: {extracted_error}")
562555

563556
repository = json.loads(output).items()
@@ -670,7 +663,6 @@ def _initialise_stanza(self, event: HookEvent) -> bool:
670663
return_code, _, stderr = self._execute_command([
671664
PGBACKREST_EXECUTABLE,
672665
PGBACKREST_CONFIGURATION_FILE,
673-
PGBACKREST_LOG_LEVEL_STDERR,
674666
f"--stanza={self.stanza_name}",
675667
"stanza-create",
676668
])
@@ -729,15 +721,14 @@ def check_stanza(self) -> bool:
729721
return_code, stdout, stderr = self._execute_command([
730722
PGBACKREST_EXECUTABLE,
731723
PGBACKREST_CONFIGURATION_FILE,
732-
PGBACKREST_LOG_LEVEL_STDERR,
733724
f"--stanza={self.stanza_name}",
734725
"check",
735726
])
736727
if return_code == PGBACKREST_ARCHIVE_TIMEOUT_ERROR_CODE:
737728
# Raise an error if the archive command timeouts, so the user has the possibility
738729
# to fix network issues and call juju resolve to re-trigger the hook that calls
739730
# this method.
740-
extracted_error = self._extract_error_message(stderr)
731+
extracted_error = self._extract_error_message(stdout, stderr)
741732
logger.error(
742733
f"error: {extracted_error} - please fix the error and call juju resolve on this unit"
743734
)
@@ -796,13 +787,12 @@ def _is_primary_pgbackrest_service_running(self) -> bool:
796787
return False
797788
return_code, stdout, stderr = self._execute_command([
798789
PGBACKREST_EXECUTABLE,
799-
PGBACKREST_LOG_LEVEL_STDERR,
800790
"server-ping",
801791
"--io-timeout=10",
802792
self.charm.primary_endpoint,
803793
])
804794
if return_code != 0:
805-
extracted_error = self._extract_error_message(stderr)
795+
extracted_error = self._extract_error_message(stdout, stderr)
806796
logger.warning(
807797
f"Failed to contact pgBackRest TLS server on {self.charm.primary_endpoint} with error {extracted_error}"
808798
)
@@ -994,8 +984,8 @@ def _run_backup(
994984
command = [
995985
PGBACKREST_EXECUTABLE,
996986
PGBACKREST_CONFIGURATION_FILE,
997-
PGBACKREST_LOG_LEVEL_STDERR,
998987
f"--stanza={self.stanza_name}",
988+
"--log-level-console=debug",
999989
f"--type={BACKUP_TYPE_OVERRIDES[backup_type]}",
1000990
"backup",
1001991
]
@@ -1030,7 +1020,7 @@ def _run_backup(
10301020
f"backup/{self.stanza_name}/{backup_id}/backup.log",
10311021
s3_parameters,
10321022
)
1033-
extracted_error = self._extract_error_message(stderr)
1023+
extracted_error = self._extract_error_message(stdout, stderr)
10341024
error_message = f"Failed to backup PostgreSQL with error: {extracted_error}"
10351025
logger.error(f"Backup failed: {error_message}")
10361026
event.fail(error_message)
@@ -1198,7 +1188,7 @@ def _on_restore_action(self, event): # noqa: C901
11981188
timeout=10,
11991189
)
12001190
if return_code != 0:
1201-
extracted_error = self._extract_error_message(stderr)
1191+
extracted_error = self._extract_error_message(stdout, stderr)
12021192
error_message = (
12031193
f"Failed to remove previous cluster information with error: {extracted_error}"
12041194
)

src/constants.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@
2626

2727
# Snap constants.
2828
PGBACKREST_EXECUTABLE = "charmed-postgresql.pgbackrest"
29-
# pgBackRest logging configuration
30-
# We use stderr for all error/warning output to have a consistent, predictable error extraction
31-
# mechanism. By default, pgBackRest uses stdout (console) for warnings, but we standardize on
32-
# stderr to avoid potential log duplication and to make error handling more reliable.
33-
# Reference: https://pgbackrest.org/configuration.html#section-log
34-
PGBACKREST_LOG_LEVEL_STDERR = "--log-level-stderr=warn"
3529
# pgBackRest error codes
3630
PGBACKREST_ARCHIVE_TIMEOUT_ERROR_CODE = (
3731
82 # Archive timeout - unable to archive WAL files within configured timeout period

tests/unit/test_backups.py

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,57 +37,79 @@ def harness():
3737
harness.cleanup()
3838

3939

40-
def test_extract_error_message_with_error_in_stderr():
41-
"""Test extracting error from stderr with ERROR marker."""
42-
stderr = """2025-11-07 07:21:11.120 P00 ERROR: [056]: unable to find primary cluster - cannot proceed
40+
def test_extract_error_message_with_error_in_stdout():
41+
"""Test extracting error from stdout with ERROR marker."""
42+
stdout = """2025-11-07 07:21:11.120 P00 ERROR: [056]: unable to find primary cluster - cannot proceed
4343
HINT: are all available clusters in recovery?"""
44-
result = PostgreSQLBackups._extract_error_message(stderr)
44+
stderr = ""
45+
result = PostgreSQLBackups._extract_error_message(stdout, stderr)
4546
assert result == "ERROR: [056]: unable to find primary cluster - cannot proceed"
4647

4748

48-
def test_extract_error_message_with_plain_stderr():
49+
def test_extract_error_message_with_error_in_stderr():
4950
"""Test extracting error from stderr when no ERROR marker."""
51+
stdout = ""
5052
stderr = "Connection refused: cannot connect to S3"
51-
result = PostgreSQLBackups._extract_error_message(stderr)
53+
result = PostgreSQLBackups._extract_error_message(stdout, stderr)
5254
assert result == "Connection refused: cannot connect to S3"
5355

5456

57+
def test_extract_error_message_with_both_stdout_and_stderr():
58+
"""Test extracting error when ERROR marker is in stdout and stderr has additional info."""
59+
stdout = "P00 ERROR: database error occurred"
60+
stderr = "Additional stderr info"
61+
result = PostgreSQLBackups._extract_error_message(stdout, stderr)
62+
assert result == "ERROR: database error occurred"
63+
64+
5565
def test_extract_error_message_with_warning():
56-
"""Test extracting warning message from stderr."""
57-
stderr = "P00 WARN: configuration issue detected"
58-
result = PostgreSQLBackups._extract_error_message(stderr)
66+
"""Test extracting warning message from output."""
67+
stdout = "P00 WARN: configuration issue detected"
68+
stderr = ""
69+
result = PostgreSQLBackups._extract_error_message(stdout, stderr)
5970
assert result == "WARN: configuration issue detected"
6071

6172

6273
def test_extract_error_message_with_multiple_errors():
63-
"""Test extracting multiple ERROR/WARN lines from stderr."""
64-
stderr = """P00 ERROR: first error
74+
"""Test extracting multiple ERROR/WARN lines."""
75+
stdout = """P00 ERROR: first error
6576
P00 WARN: warning message
6677
P00 ERROR: second error"""
67-
result = PostgreSQLBackups._extract_error_message(stderr)
78+
stderr = ""
79+
result = PostgreSQLBackups._extract_error_message(stdout, stderr)
6880
assert result == "ERROR: first error; WARN: warning message; ERROR: second error"
6981

7082

7183
def test_extract_error_message_with_empty_output():
72-
"""Test with empty stderr returns helpful message."""
73-
result = PostgreSQLBackups._extract_error_message("")
84+
"""Test with empty stdout and stderr returns helpful message."""
85+
result = PostgreSQLBackups._extract_error_message("", "")
7486
assert (
7587
result
7688
== "Unknown error occurred. Please check the logs at /var/snap/charmed-postgresql/common/var/log/pgbackrest"
7789
)
7890

7991

92+
def test_extract_error_message_fallback_to_stdout_last_line():
93+
"""Test fallback to last line of stdout when no ERROR/WARN markers."""
94+
stdout = "Some generic output\nMore details here\nFinal error message"
95+
stderr = ""
96+
result = PostgreSQLBackups._extract_error_message(stdout, stderr)
97+
assert result == "Final error message"
98+
99+
80100
def test_extract_error_message_fallback_to_stderr_last_line():
81-
"""Test fallback to last line of stderr when no ERROR/WARN markers."""
101+
"""Test fallback to last line of stderr when no ERROR/WARN markers and no stdout."""
102+
stdout = ""
82103
stderr = "Line 1\nLine 2\nFinal error message"
83-
result = PostgreSQLBackups._extract_error_message(stderr)
104+
result = PostgreSQLBackups._extract_error_message(stdout, stderr)
84105
assert result == "Final error message"
85106

86107

87108
def test_extract_error_message_cleans_debug_prefix():
88109
"""Test that debug prefixes like 'P00 ERROR:' are cleaned up."""
89-
stderr = "2025-11-07 07:21:11.120 P00 ERROR: test error message"
90-
result = PostgreSQLBackups._extract_error_message(stderr)
110+
stdout = "2025-11-07 07:21:11.120 P00 ERROR: test error message"
111+
stderr = ""
112+
result = PostgreSQLBackups._extract_error_message(stdout, stderr)
91113
assert result == "ERROR: test error message"
92114

93115

0 commit comments

Comments
 (0)