systemd: serve kernel-log, the kernel's messages of one boot (issue 1975)

journalctl --dmesg --boot=N under the same escalation, window, priority, fixed-string match, 2000-line cap
and redaction as the journal verb (shapeLines is the one filter both use); a boot is 0 or a negative whole
number, refused otherwise before anything runs. No scope: the kernel belongs to no manager.

Builds: issue 1975, the operator's request of 2026-11-12 (the lead's handover; no design line names the verb)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 02:17:28 +01:00
parent d9f2b7bcfa
commit 3ebcb15a4b
6 changed files with 190 additions and 27 deletions
+69 -22
View File
@@ -332,28 +332,7 @@ func (m *Manager) Journal(scope Scope, unit string, q JournalQuery) (map[string]
return nil, err
}
known, envErr := m.unitSecrets(scope, unit)
all := []string{}
for _, l := range strings.Split(out, "\n") {
if l != "" {
all = append(all, l)
}
}
scanned := len(all)
kept, redacted := []string{}, 0
for _, l := range all {
l, n := redact(l, known)
redacted += n
if q.Match != "" && !strings.Contains(l, q.Match) {
continue
}
if len(l) > LongestLine {
l = l[:LongestLine] + "…"
}
kept = append(kept, l)
}
if len(kept) > q.Lines {
kept = kept[len(kept)-q.Lines:]
}
kept, scanned, redacted := shapeLines(out, known, q)
answer := map[string]any{"unit": unit, "scope": string(scope), "lines": kept, "count": len(kept)}
for k, v := range map[string]string{"since": q.Since, "until": q.Until, "match": q.Match, "priority": q.Priority} {
if v != "" {
@@ -376,6 +355,74 @@ func (m *Manager) Journal(scope Scope, unit string, q JournalQuery) (map[string]
return answer, nil
}
// shapeLines is the lines of journalctl's output that a query keeps, each redacted and cut, the last q.Lines of
// those holding the match; and how many lines were read and how many secrets replaced.
func shapeLines(out string, known []knownSecret, q JournalQuery) (kept []string, scanned, redacted int) {
all := []string{}
for _, l := range strings.Split(out, "\n") {
if l != "" {
all = append(all, l)
}
}
scanned = len(all)
kept = []string{}
for _, l := range all {
l, n := redact(l, known)
redacted += n
if q.Match != "" && !strings.Contains(l, q.Match) {
continue
}
if len(l) > LongestLine {
l = l[:LongestLine] + "…"
}
kept = append(kept, l)
}
if len(kept) > q.Lines {
kept = kept[len(kept)-q.Lines:]
}
return kept, scanned, redacted
}
// KernelLog is the kernel's messages of one boot (journalctl -k, dmesg's, of this boot or an earlier one) that a
// query keeps: the same window, priority, match, cap and redaction as Journal. The kernel's messages belong to
// no unit and no manager, so it has no scope; the read is the system journal's, escalated as Journal's is.
func (m *Manager) KernelLog(q JournalQuery) (map[string]any, error) {
q, err := q.valid()
if err != nil {
return nil, err
}
read := q.Lines
if q.Match != "" {
read = MatchScan
}
out, err := m.call(System, "journalctl", q.kernelArgv(read)...)
if err != nil {
return nil, err
}
kept, scanned, redacted := shapeLines(out, nil, q)
boot := q.Boot
if boot == "" {
boot = "0"
}
answer := map[string]any{"boot": boot, "lines": kept, "count": len(kept)}
for k, v := range map[string]string{"since": q.Since, "until": q.Until, "match": q.Match, "priority": q.Priority} {
if v != "" {
answer[k] = v
}
}
if q.Match != "" {
answer["scanned"] = scanned
if scanned >= MatchScan {
answer["note"] = fmt.Sprintf("the match was looked for in the window's last %d lines only: narrow the window to reach earlier ones", MatchScan)
}
}
if redacted > 0 {
answer["redacted"] = redacted
answer["leak"] = "the kernel's messages hold secrets, shown as [redacted: <what it was>]"
}
return answer, nil
}
// unitSecrets are the values of the unit's own Environment= that must not be answered. Read with
// systemctl show, which needs no escalation; a unit that does not exist has none.
func (m *Manager) unitSecrets(scope Scope, unit string) ([]knownSecret, error) {