Both journald receivers import one PRIORITY mapping, and each tier's suite was asserting the whole table against its own receiver. That checks one file twice: invert the table and two cases fail saying the same thing, which tells you nothing about which of the two possible defects you have. Split by subject instead. The table's contents — the inverted direction, overwrite_text, parse_from/on_error — belong to the file that holds them, so they get a suite of their own reading that file directly, with no fixture at all. Each tier keeps a case, reduced to the question only it can answer: does MY receiver carry the shared mapping. Both tier cases stay. They cover different receivers over different journals — the agent container's own and the swarm collector's host journal — and one tier quietly losing its parser while the other keeps one is exactly the half-fixed state worth catching. Membership rather than equality of the whole operator list, so a tier that later grows an unrelated operator of its own still passes. Checked against seven defect scenarios: each fails exactly one case, and names the right one.
113 lines
4.9 KiB
Nix
113 lines
4.9 KiB
Nix
# `checks.module-eval-journald-severity` — see ./lib.nix for the shared
|
|
# rationale (why this suite exists, naming convention, "evaluates
|
|
# not executes").
|
|
#
|
|
# The odd one out in this directory: every sibling suite evaluates a host or an
|
|
# agent and reads the config that falls out, where this one reads a single file
|
|
# that is already a value. It is a suite of its own for the reason the operator
|
|
# gave when she saw the mapping asserted in two tiers at once — **test the
|
|
# thing only once**. The table lives in one file, so its contents are one
|
|
# subject and belong to one case. Each tier's suite then asks a different
|
|
# question, about wiring rather than contents: does MY receiver carry this.
|
|
#
|
|
# What that buys is a readable failure. Invert the table and exactly this suite
|
|
# goes red, naming the table. Drop the parser from one tier's receiver and
|
|
# exactly that tier goes red, naming the tier. Neither failure makes you read
|
|
# the other one to work out which of the two happened.
|
|
#
|
|
# Needs no `nixosSystem` fixture at all — the thing under test is a file — so
|
|
# this suite costs nothing to evaluate next to its siblings.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
}:
|
|
let
|
|
inherit
|
|
(import ./lib.nix {
|
|
inherit
|
|
pkgs
|
|
lib
|
|
self
|
|
nixosSystem
|
|
;
|
|
})
|
|
journaldSeverityParser
|
|
runGroup
|
|
;
|
|
|
|
# The parser's own mapping read back as a plain `PRIORITY -> severity-name`
|
|
# function, so the case below can ask what a priority MAPS TO instead of
|
|
# matching on the shape of the attrset that produces it. `null` for a
|
|
# priority the table does not name, which the case uses as its control.
|
|
severityOf =
|
|
priority:
|
|
let
|
|
hits = lib.attrNames (
|
|
lib.filterAttrs (_: v: lib.elem priority (lib.toList v)) journaldSeverityParser.mapping
|
|
);
|
|
in
|
|
if journaldSeverityParser == null || hits == [ ] then null else lib.head hits;
|
|
|
|
cases = [
|
|
{
|
|
# ⚠️ The direction, and the only part of this mapping a reviewer cannot
|
|
# check by looking. syslog's PRIORITY counts DOWN in urgency (0 = emerg,
|
|
# 7 = debug) where the OpenTelemetry severity counts UP (1 = trace,
|
|
# 21 = fatal). Wired across, debug renders as critical and emerg as
|
|
# unspecified — and nothing reports an error, because every line still
|
|
# arrives and every field is still populated.
|
|
#
|
|
# Every priority, not a sample. An inverted copy still maps each value
|
|
# to SOMETHING, so a case that checks one end, or checks that a mapping
|
|
# merely exists, passes on the exact defect it was written for. The
|
|
# `"8"` arm is the control: it says this reader looks values up rather
|
|
# than answering the same way whatever it is handed, so the eight
|
|
# arms above it mean what they appear to mean.
|
|
name = "journald PRIORITY maps to severity the right way up";
|
|
ok =
|
|
severityOf "0" == "fatal"
|
|
&& severityOf "1" == "fatal"
|
|
&& severityOf "2" == "fatal"
|
|
&& severityOf "3" == "error"
|
|
&& severityOf "4" == "warn"
|
|
&& severityOf "5" == "info"
|
|
&& severityOf "6" == "info"
|
|
&& severityOf "7" == "debug"
|
|
&& severityOf "8" == null;
|
|
}
|
|
{
|
|
# The second way this mapping can be present and useless, and the one
|
|
# that looks already handled. Without `overwrite_text` the parser sets
|
|
# the severity NUMBER from the table above and leaves the severity TEXT
|
|
# as the raw value it matched — so `severity_text` reaches the log store
|
|
# as the literal "6". That is a populated field: it passes any check
|
|
# asking merely whether severity is set, and it is not a level anything
|
|
# renders. VictoriaLogs has no ingest parameter naming a level field, so
|
|
# the text is the whole interface.
|
|
name = "the mapping writes a level name, not the raw priority digit";
|
|
ok = journaldSeverityParser.overwrite_text or false;
|
|
}
|
|
{
|
|
# Two settings that are load-bearing in opposite directions.
|
|
#
|
|
# `parse_from` names where the value is. The journald input unmarshals
|
|
# each `journalctl -o json` line into the entry BODY as a map, so the
|
|
# field lives under `body.`; `attributes.PRIORITY` is the spelling that
|
|
# looks equally plausible and finds nothing, leaving a parser that
|
|
# reports a missing field per entry rather than failing to start.
|
|
#
|
|
# `on_error` decides what happens to a record it cannot read. It must
|
|
# still REACH the store: `drop` would make the logs dashboard's
|
|
# "lines with no severity" panel read zero by deleting its own evidence,
|
|
# which is the empty-result-as-success failure this whole mapping exists
|
|
# to end.
|
|
name = "the mapping reads the field the receiver fills, and keeps what it cannot read";
|
|
ok =
|
|
journaldSeverityParser.parse_from or null == "body.PRIORITY"
|
|
&& journaldSeverityParser.on_error or null == "send";
|
|
}
|
|
];
|
|
in
|
|
runGroup "journald-severity" cases
|