Skip to content

Commit bca73c0

Browse files
committed
reading journal logs with journalctl only
1 parent c553201 commit bca73c0

File tree

3 files changed

+11
-215
lines changed

3 files changed

+11
-215
lines changed

nodescraper/plugins/inband/journal/journal_collector.py

Lines changed: 8 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
#
2525
###############################################################################
2626
from nodescraper.base import InBandDataCollector
27-
from nodescraper.connection.inband import TextFileArtifact
28-
from nodescraper.enums import EventCategory, EventPriority, OSFamily
27+
from nodescraper.enums import OSFamily
2928
from nodescraper.models import TaskResult
3029

3130
from .journaldata import JournalData
@@ -37,102 +36,20 @@ class JournalCollector(InBandDataCollector[JournalData, None]):
3736
SUPPORTED_OS_FAMILY = {OSFamily.LINUX}
3837
DATA_MODEL = JournalData
3938

40-
CMD = "ls -1 /var/log/journal/*/system* 2>/dev/null || true"
41-
42-
def _shell_quote(self, s: str) -> str:
43-
"""single-quote fix.
44-
45-
Args:
46-
s (str): path
47-
48-
Returns:
49-
str: escaped path
50-
"""
51-
return "'" + s.replace("'", "'\"'\"'") + "'"
52-
53-
def _flat_name(self, path: str) -> str:
54-
"""Flatten path name
55-
56-
Args:
57-
path (str): path
58-
59-
Returns:
60-
str: flattened path name
61-
"""
62-
return "journalctl__" + path.lstrip("/").replace("/", "__") + ".json"
63-
64-
def _read_with_journalctl(self, path: str):
39+
def _read_with_journalctl(self):
6540
"""Read journal logs using journalctl
6641
67-
Args:
68-
path (str): path for log to read
69-
7042
Returns:
71-
str|None: name of local journal log filed, or None if log was not read
43+
str|None: system journal read
7244
"""
73-
qp = self._shell_quote(path)
74-
cmd = f"journalctl --no-pager --system --all --file={qp} --output=json"
45+
cmd = "journalctl --no-pager --system --all -o short-iso --output=json"
7546
res = self._run_sut_cmd(cmd, sudo=True, log_artifact=False, strip=False)
7647

7748
if res.exit_code == 0:
78-
text = (
79-
res.stdout.decode("utf-8", "replace")
80-
if isinstance(res.stdout, (bytes, bytearray))
81-
else res.stdout
82-
)
83-
fname = self._flat_name(path)
84-
self.result.artifacts.append(TextFileArtifact(filename=fname, contents=text))
85-
self.logger.info("Collected journal: %s", path)
86-
return fname
49+
return res.stdout
8750

8851
return None
8952

90-
def _get_journals(self) -> list[str]:
91-
"""Read journal log files on remote system
92-
93-
Returns:
94-
list[str]: List of names of read logs
95-
"""
96-
list_res = self._run_sut_cmd(self.CMD, sudo=True)
97-
paths = [p.strip() for p in (list_res.stdout or "").splitlines() if p.strip()]
98-
99-
if not paths:
100-
self._log_event(
101-
category=EventCategory.OS,
102-
description="No /var/log/journal files found (including rotations).",
103-
data={"list_exit_code": list_res.exit_code},
104-
priority=EventPriority.WARNING,
105-
)
106-
return []
107-
108-
collected, failed = [], []
109-
for p in paths:
110-
self.logger.debug("Reading journal file: %s", p)
111-
fname = self._read_with_journalctl(p)
112-
if fname:
113-
collected.append(fname)
114-
else:
115-
failed.append(fname)
116-
117-
if collected:
118-
self._log_event(
119-
category=EventCategory.OS,
120-
description="Collected journal logs.",
121-
data={"collected": collected},
122-
priority=EventPriority.INFO,
123-
)
124-
self.result.message = self.result.message or "journalctl logs collected"
125-
126-
if failed:
127-
self._log_event(
128-
category=EventCategory.OS,
129-
description="Some journal files could not be read with journalctl.",
130-
data={"failed": failed},
131-
priority=EventPriority.WARNING,
132-
)
133-
134-
return collected
135-
13653
def collect_data(self, args=None) -> tuple[TaskResult, JournalData | None]:
13754
"""Collect journal lofs
13855
@@ -142,9 +59,9 @@ def collect_data(self, args=None) -> tuple[TaskResult, JournalData | None]:
14259
Returns:
14360
tuple[TaskResult, JournalData | None]: Tuple of results and data model or none.
14461
"""
145-
collected = self._get_journals()
146-
if collected:
147-
data = JournalData(journal_logs=collected)
62+
journal_log = self._read_with_journalctl()
63+
if journal_log:
64+
data = JournalData(journal_log=journal_log)
14865
self.result.message = self.result.message or "Journal data collected"
14966
return self.result, data
15067
return self.result, None

nodescraper/plugins/inband/journal/journaldata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@
2929
class JournalData(DataModel):
3030
"""Data model for journal logs"""
3131

32-
journal_logs: list[str] = []
32+
journal_log: str

test/unit/plugin/test_journal_collector.py

Lines changed: 2 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -59,134 +59,13 @@ def _run_sut_cmd(cmd, *args, **kwargs):
5959
return c
6060

6161

62-
def test_get_journals_happy_path(monkeypatch, system_info, conn_mock):
63-
paths = [
64-
"/var/log/journal/m1/system.journal",
65-
"/var/log/journal/m1/[email protected]",
66-
"/var/log/journal/m2/system.journal",
67-
]
68-
ls_out = "\n".join(paths) + "\n"
69-
70-
def run_map(cmd, **kwargs):
71-
if cmd.startswith("ls -1 /var/log/journal"):
72-
return DummyRes(command=cmd, stdout=ls_out, exit_code=0)
73-
74-
if cmd.startswith("journalctl ") and "--file=" in cmd:
75-
if paths[0] in cmd:
76-
return DummyRes(cmd, stdout='{"MESSAGE":"a"}\n', exit_code=0)
77-
if paths[1] in cmd:
78-
return DummyRes(cmd, stdout=b'{"MESSAGE":"b"}\n', exit_code=0)
79-
if paths[2] in cmd:
80-
return DummyRes(cmd, stdout='{"MESSAGE":"c"}\n', exit_code=0)
81-
82-
return DummyRes(command=cmd, stdout="", exit_code=1, stderr="unexpected")
83-
84-
c = get_collector(monkeypatch, run_map, system_info, conn_mock)
85-
86-
collected = c._get_journals()
87-
assert len(collected) == 3
88-
89-
expected_names = {
90-
"journalctl__var__log__journal__m1__system.journal.json",
91-
"journalctl__var__log__journal__m1__system@0000000000000001-0000000000000002.journal.json",
92-
"journalctl__var__log__journal__m2__system.journal.json",
93-
}
94-
names = {a.filename for a in c.result.artifacts}
95-
assert names == expected_names
96-
97-
contents = {a.filename: a.contents for a in c.result.artifacts}
98-
assert (
99-
contents["journalctl__var__log__journal__m1__system.journal.json"].strip()
100-
== '{"MESSAGE":"a"}'
101-
)
102-
assert (
103-
contents[
104-
"journalctl__var__log__journal__m1__system@0000000000000001-0000000000000002.journal.json"
105-
].strip()
106-
== '{"MESSAGE":"b"}'
107-
)
108-
assert (
109-
contents["journalctl__var__log__journal__m2__system.journal.json"].strip()
110-
== '{"MESSAGE":"c"}'
111-
)
112-
113-
assert any(
114-
evt.get("description") == "Collected journal logs."
115-
and getattr(evt.get("priority"), "name", str(evt.get("priority"))) == "INFO"
116-
for evt in c._events
117-
)
118-
assert c.result.message == "journalctl logs collected"
119-
120-
121-
def test_get_journals_no_files(monkeypatch, system_info, conn_mock):
122-
def run_map(cmd, **kwargs):
123-
if cmd.startswith("ls -1 /var/log/journal"):
124-
return DummyRes(command=cmd, stdout="", exit_code=0)
125-
return DummyRes(command=cmd, stdout="", exit_code=1)
126-
127-
c = get_collector(monkeypatch, run_map, system_info, conn_mock)
128-
129-
collected = c._get_journals()
130-
assert collected == []
131-
assert c.result.artifacts == []
132-
133-
assert any(
134-
evt.get("description", "").startswith("No /var/log/journal files found")
135-
and getattr(evt.get("priority"), "name", str(evt.get("priority"))) == "WARNING"
136-
for evt in c._events
137-
)
138-
139-
140-
def test_get_journals_partial_failure(monkeypatch, system_info, conn_mock):
141-
ok_path = "/var/log/journal/m1/system.journal"
142-
bad_path = "/var/log/journal/m1/[email protected]"
143-
ls_out = ok_path + "\n" + bad_path + "\n"
144-
145-
def run_map(cmd, **kwargs):
146-
if cmd.startswith("ls -1 /var/log/journal"):
147-
return DummyRes(command=cmd, stdout=ls_out, exit_code=0)
148-
149-
if cmd.startswith("journalctl ") and "--file=" in cmd:
150-
if ok_path in cmd:
151-
return DummyRes(cmd, stdout='{"MESSAGE":"ok"}\n', exit_code=0)
152-
if bad_path in cmd:
153-
return DummyRes(cmd, stdout="", exit_code=1, stderr="cannot read")
154-
155-
return DummyRes(command=cmd, stdout="", exit_code=1)
156-
157-
c = get_collector(monkeypatch, run_map, system_info, conn_mock)
158-
159-
collected = c._get_journals()
160-
assert collected == ["journalctl__var__log__journal__m1__system.journal.json"]
161-
assert [a.filename for a in c.result.artifacts] == [
162-
"journalctl__var__log__journal__m1__system.journal.json"
163-
]
164-
165-
assert any(
166-
evt.get("description") == "Some journal files could not be read with journalctl."
167-
and getattr(evt.get("priority"), "name", str(evt.get("priority"))) == "WARNING"
168-
for evt in c._events
169-
)
170-
171-
17262
def test_collect_data_integration(monkeypatch, system_info, conn_mock):
173-
dummy_path = "/var/log/journal/m1/system.journal"
174-
ls_out = dummy_path + "\n"
175-
17663
def run_map(cmd, **kwargs):
177-
if cmd.startswith("ls -1 /var/log/journal"):
178-
return DummyRes(command=cmd, stdout=ls_out, exit_code=0)
179-
if cmd.startswith("journalctl ") and "--file=" in cmd and dummy_path in cmd:
180-
return DummyRes(command=cmd, stdout='{"MESSAGE":"hello"}\n', exit_code=0)
181-
return DummyRes(command=cmd, stdout="", exit_code=1)
64+
return DummyRes(command=cmd, stdout='{"MESSAGE":"hello"}\n', exit_code=0)
18265

18366
c = get_collector(monkeypatch, run_map, system_info, conn_mock)
18467

18568
result, data = c.collect_data()
18669
assert isinstance(data, JournalData)
18770

188-
expected_name = "journalctl__var__log__journal__m1__system.journal.json"
189-
assert data.journal_logs == [expected_name]
190-
assert c.result.message == "journalctl logs collected"
191-
192-
assert [a.filename for a in c.result.artifacts] == [expected_name]
71+
assert data.journal_log == '{"MESSAGE":"hello"}\n'

0 commit comments

Comments
 (0)