mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
Without `--all`, `journalctl --output=json` encodes a field over 4096 bytes as `null`, so a long host error matched nothing and the check passed. A repeated `MESSAGE` field arrives as a JSON array of its values, and `bytes()` on that array raised `TypeError`, which aborted the whole check. Pass `--all`, and tell a byte array (all integers) from an array of field values, decoding each value on its own. Reproduced against the host journal: a 5000-byte error line read back as `"MESSAGE":null` and produced no match; an entry with two `MESSAGE` fields, sent over the native journal socket, read back as `["error first","error second"]` and crashed `host_errors` with `TypeError`. With the fix both are reported. Also clear the two test-lint findings of this PR: `test_comments_valid` rejects the narrative comment above the `journalctl` Popen, so it goes; the stderr-to-file choice stays, because an unread stderr pipe that fills up would block `journalctl` while stdout is still being read. `test_no_uncached_file_reads` flags the read of the stub's argument file, which lives in the test's tempdir and is written per run, so the path-keyed `utils.cache.files.read_text` could serve a stale body; the read opts out with `# nocheck: cache-read tempdir fixture`, as the other stub-driven script tests do. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
148 lines
5.2 KiB
Python
148 lines
5.2 KiB
Python
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from utils import PROJECT_ROOT
|
|
|
|
SCRIPT = PROJECT_ROOT / "roles/sys-ctl-hlth-journalctl/files/python/script.py"
|
|
|
|
JOURNALCTL_STUB = """#!/usr/bin/env bash
|
|
printf '%s\\n' "$*" >"${STUB_DIR}/args"
|
|
cat "${STUB_DIR}/journal"
|
|
if [ "${STUB_RC}" -ne 0 ]; then
|
|
\techo "journalctl: unrecognized option '--output-fields'" >&2
|
|
fi
|
|
exit "${STUB_RC}"
|
|
"""
|
|
|
|
APP_ERROR = {
|
|
"MESSAGE": "ERROR: pixelfed queue failed",
|
|
"CONTAINER_ID": "0123456789ab",
|
|
"SYSLOG_IDENTIFIER": "pixelfed",
|
|
"_SYSTEMD_UNIT": "docker.service",
|
|
}
|
|
DAEMON_ERROR = {
|
|
"MESSAGE": 'level=error msg="failed to start container"',
|
|
"SYSLOG_IDENTIFIER": "dockerd",
|
|
"_SYSTEMD_UNIT": "docker.service",
|
|
}
|
|
KERNEL_ERROR = {"MESSAGE": "EXT4-fs error (device sda1)", "SYSLOG_IDENTIFIER": "kernel"}
|
|
BINARY_ERROR = {
|
|
"MESSAGE": list(b"I/O error \xff on disk"),
|
|
"_SYSTEMD_UNIT": "smartd.service",
|
|
}
|
|
REPEATED_ERROR = {
|
|
"MESSAGE": ["unit started", list(b"disk error \xff on sdb")],
|
|
"_SYSTEMD_UNIT": "zfs-import.service",
|
|
}
|
|
SYSTEM_INFO = {
|
|
"MESSAGE": "Started Daily apt download activities.",
|
|
"_SYSTEMD_UNIT": "init.scope",
|
|
}
|
|
|
|
|
|
class TestHlthJournalctlScript(unittest.TestCase):
|
|
def run_script(self, *entries, rc=0):
|
|
"""Run the health script against a stubbed journalctl.
|
|
|
|
Args:
|
|
entries: journal entries the stub prints, one JSON object per line.
|
|
rc: exit code of the stubbed journalctl.
|
|
|
|
Returns:
|
|
Tuple of the exit code, the script output, and the journalctl arguments.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
bin_dir = Path(tmp)
|
|
stub = bin_dir / "journalctl"
|
|
stub.write_text(JOURNALCTL_STUB)
|
|
stub.chmod(0o755)
|
|
(bin_dir / "journal").write_text(
|
|
"".join(json.dumps(entry) + "\n" for entry in entries)
|
|
)
|
|
env = dict(os.environ)
|
|
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
|
|
env["STUB_DIR"] = tmp
|
|
env["STUB_RC"] = str(rc)
|
|
done = subprocess.run(
|
|
[sys.executable, str(SCRIPT)],
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
args = (bin_dir / "args").read_text() # nocheck: cache-read tempdir fixture
|
|
return done.returncode, done.stdout, args
|
|
|
|
def test_reads_the_journal_as_json_with_the_source_fields(self):
|
|
_, _, args = self.run_script(SYSTEM_INFO)
|
|
self.assertIn("--output=json", args)
|
|
self.assertIn(
|
|
"--output-fields=MESSAGE,CONTAINER_ID,SYSLOG_IDENTIFIER,_SYSTEMD_UNIT",
|
|
args,
|
|
)
|
|
|
|
def test_reads_every_field_in_full(self):
|
|
_, _, args = self.run_script(SYSTEM_INFO)
|
|
self.assertIn("--all", args.split())
|
|
|
|
def test_a_repeated_message_field_is_joined_and_reported(self):
|
|
code, output, _ = self.run_script(REPEATED_ERROR, DAEMON_ERROR)
|
|
self.assertEqual(code, 1)
|
|
self.assertIn("zfs-import.service: unit started disk error", output)
|
|
self.assertIn('docker.service: level=error msg="failed to start', output)
|
|
|
|
def test_an_application_error_from_a_container_is_ignored(self):
|
|
code, _, _ = self.run_script(APP_ERROR, SYSTEM_INFO)
|
|
self.assertEqual(code, 0)
|
|
|
|
def test_a_docker_daemon_error_is_reported_with_its_unit(self):
|
|
code, output, _ = self.run_script(APP_ERROR, DAEMON_ERROR)
|
|
self.assertEqual(code, 1)
|
|
self.assertIn(
|
|
'docker.service: level=error msg="failed to start container"', output
|
|
)
|
|
self.assertNotIn("pixelfed queue failed", output)
|
|
|
|
def test_an_entry_without_unit_is_named_by_its_identifier(self):
|
|
code, output, _ = self.run_script(KERNEL_ERROR)
|
|
self.assertEqual(code, 1)
|
|
self.assertIn("kernel: EXT4-fs error (device sda1)", output)
|
|
|
|
def test_a_non_utf8_message_is_decoded_and_reported(self):
|
|
code, output, _ = self.run_script(BINARY_ERROR)
|
|
self.assertEqual(code, 1)
|
|
self.assertIn("smartd.service: I/O error", output)
|
|
|
|
def test_many_container_errors_are_streamed_past_and_not_reported(self):
|
|
container_lines = [
|
|
{**APP_ERROR, "MESSAGE": f"ERROR: pixelfed request {n} failed"}
|
|
for n in range(20000)
|
|
]
|
|
code, output, _ = self.run_script(*container_lines, DAEMON_ERROR)
|
|
self.assertEqual(code, 1)
|
|
self.assertNotIn("pixelfed request", output)
|
|
self.assertEqual(
|
|
output.splitlines()[-1],
|
|
'docker.service: level=error msg="failed to start container"',
|
|
)
|
|
self.assertEqual(len(output.splitlines()), 3)
|
|
|
|
def test_a_journal_without_errors_passes(self):
|
|
code, _, _ = self.run_script(SYSTEM_INFO)
|
|
self.assertEqual(code, 0)
|
|
|
|
def test_a_failing_journalctl_fails_the_check(self):
|
|
code, output, _ = self.run_script(rc=1)
|
|
self.assertEqual(code, 2)
|
|
self.assertIn("journalctl failed with exit code 1", output)
|
|
self.assertIn("unrecognized option '--output-fields'", output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|