[ AGNTLOG-703 ] Fix syslog TCP frame misclassification and recognize appliance timestamp layouts - #54492
[ AGNTLOG-703 ] Fix syslog TCP frame misclassification and recognize appliance timestamp layouts#54492ddrthall wants to merge 11 commits into
Conversation
|
🎯 Code Coverage (details) 🔗 Commit SHA: 518c44c | Docs | Datadog PR Page | Give us feedback! |
Files inventory check summaryFile checks results against ancestor 36808442: Results for datadog-agent_7.84.0~devel.git.24.385dbe3.pipeline.129724895-1_amd64.deb:No change detected Results for datadog-iot-agent_7.84.0~devel.git.24.385dbe3.pipeline.129724895-1_amd64.deb:No change detected |
Static quality checks✅ Please find below the results from static quality gates Successful checksInfo
|
Regression DetectorRegression Detector ResultsMetrics dashboard Baseline: b39d95d Optimization Goals: ✅ No significant changes detected
|
| perf | experiment | goal | Δ mean % | Δ mean % CI | trials | links |
|---|---|---|---|---|---|---|
| ➖ | quality_gate_private_action_runner | memory utilization | +0.31 | [+0.19, +0.43] | 1 | Logs bounds checks dashboard |
| ➖ | quality_gate_security_mean_fs_load | memory utilization | +0.13 | [+0.10, +0.17] | 1 | Logs bounds checks dashboard |
| ➖ | quality_gate_idle | memory utilization | +0.01 | [-0.05, +0.06] | 1 | Logs bounds checks dashboard |
| ➖ | quality_gate_security_idle | memory utilization | +0.00 | [-0.05, +0.06] | 1 | Logs bounds checks dashboard |
| ➖ | quality_gate_idle_all_features | memory utilization | -0.03 | [-0.06, +0.01] | 1 | Logs bounds checks dashboard |
| ➖ | quality_gate_security_no_fs_load | memory utilization | -0.13 | [-0.22, -0.03] | 1 | Logs bounds checks dashboard |
| ➖ | quality_gate_logs | % cpu utilization | -0.42 | [-1.27, +0.43] | 1 | Logs bounds checks dashboard |
| ➖ | quality_gate_metrics_logs | memory utilization | -1.02 | [-1.26, -0.77] | 1 | Logs bounds checks dashboard |
Bounds Checks: ✅ Passed
| perf | experiment | bounds_check_name | replicates_passed | observed_value | links |
|---|---|---|---|---|---|
| ✅ | quality_gate_idle | intake_connections | 10/10 | 3 ≤ 4 | bounds checks dashboard |
| ✅ | quality_gate_idle | memory_usage | 10/10 | 149.90MiB ≤ 154MiB | bounds checks dashboard |
| ✅ | quality_gate_idle | total_bytes_received | 10/10 | 742.92KiB ≤ 819.20KiB | bounds checks dashboard |
| ✅ | quality_gate_idle_all_features | intake_connections | 10/10 | 3 ≤ 4 | bounds checks dashboard |
| ✅ | quality_gate_idle_all_features | memory_usage | 10/10 | 493.80MiB ≤ 512MiB | bounds checks dashboard |
| ✅ | quality_gate_idle_all_features | total_bytes_received | 10/10 | 1.12MiB ≤ 1.25MiB | bounds checks dashboard |
| ✅ | quality_gate_logs | intake_connections | 10/10 | 17 ≤ 40 | bounds checks dashboard |
| ✅ | quality_gate_logs | memory_usage | 10/10 | 184.60MiB ≤ 195MiB | bounds checks dashboard |
| ✅ | quality_gate_logs | missed_bytes | 10/10 | 0B = 0B | bounds checks dashboard |
| ✅ | quality_gate_logs | total_bytes_received | 10/10 | 263.82MiB ≤ 292MiB | bounds checks dashboard |
| ✅ | quality_gate_metrics_logs | cpu_usage | 10/10 | 361.48 ≤ 2000 | bounds checks dashboard |
| ✅ | quality_gate_metrics_logs | intake_connections | 10/10 | 18 ≤ 40 | bounds checks dashboard |
| ✅ | quality_gate_metrics_logs | memory_usage | 10/10 | 374.15MiB ≤ 430MiB | bounds checks dashboard |
| ✅ | quality_gate_metrics_logs | missed_bytes | 10/10 | 0B = 0B | bounds checks dashboard |
| ✅ | quality_gate_metrics_logs | total_bytes_received | 10/10 | 0.94GiB ≤ 1.04GiB | bounds checks dashboard |
| ✅ | quality_gate_private_action_runner | memory_usage | 10/10 | 72.07MiB ≤ 75MiB | bounds checks dashboard |
| ✅ | quality_gate_security_idle | cpu_usage | 10/10 | 27.91 ≤ 100 | bounds checks dashboard |
| ✅ | quality_gate_security_idle | memory_usage | 10/10 | 300.61MiB ≤ 330MiB | bounds checks dashboard |
| ✅ | quality_gate_security_mean_fs_load | cpu_usage | 10/10 | 59.10 ≤ 200 | bounds checks dashboard |
| ✅ | quality_gate_security_mean_fs_load | memory_usage | 10/10 | 278.86MiB ≤ 310MiB | bounds checks dashboard |
| ✅ | quality_gate_security_no_fs_load | cpu_usage | 10/10 | 21.42 ≤ 100 | bounds checks dashboard |
| ✅ | quality_gate_security_no_fs_load | memory_usage | 10/10 | 284.84MiB ≤ 320MiB | bounds checks dashboard |
Explanation
Confidence level: 90.00%
Effect size tolerance: |Δ mean %| ≥ 5.00%
Performance changes are noted in the perf column of each table:
- ✅ = significantly better comparison variant performance
- ❌ = significantly worse comparison variant performance
- ➖ = no significant change in performance
A regression test is an A/B test of target performance in a repeatable rig, where "performance" is measured as "comparison variant minus baseline variant" for an optimization goal (e.g., ingress throughput). Due to intrinsic variability in measuring that goal, we can only estimate its mean value for each experiment; we report uncertainty in that value as a 90.00% confidence interval denoted "Δ mean % CI".
For each experiment, we decide whether a change in performance is a "regression" -- a change worth investigating further -- if all of the following criteria are true:
-
Its estimated |Δ mean %| ≥ 5.00%, indicating the change is big enough to merit a closer look.
-
Its 90.00% confidence interval "Δ mean % CI" does not contain zero, indicating that if our statistical model is accurate, there is at least a 90.00% chance there is a difference in performance between baseline and comparison variants.
-
Its configuration does not mark it "erratic".
Replicate Execution Details
We run multiple replicates for each experiment/variant. However, we allow replicates to be automatically retried if there are any failures, up to 8 times, at which point the replicate is marked dead and we are unable to run analysis for the entire experiment. We call each of these attempts at running replicates a replicate execution. This section lists all replicate executions that failed due to the target crashing or being oom killed.
Note: In the below tables we bucket failures by experiment, variant, and failure type. For each of these buckets we list out the replicate indexes that failed with an annotation signifying how many times said replicate failed with the given failure mode. In the below example the baseline variant of the experiment named experiment_with_failures had two replicates that failed by oom kills. Replicate 0, which failed 8 executions, and replicate 1 which failed 6 executions, all with the same failure mode.
| Experiment | Variant | Replicates | Failure | Logs | Debug Dashboard |
|---|---|---|---|---|---|
| experiment_with_failures | baseline | 0 (x8) 1 (x6) | Oom killed | Debug Dashboard |
The debug dashboard links will take you to a debugging dashboard specifically designed to investigate replicate execution failures.
❌ Retried Profiling Replicate Execution Failures (ddprof)
Note: Profiling replicas may still be executing. See the debug dashboard for up to date status.
| Experiment | Variant | Replicates | Failure | Debug Dashboard |
|---|---|---|---|---|
| quality_gate_idle | baseline | 10 | Oom killed | Debug Dashboard |
| quality_gate_idle_all_features | baseline | 10 | Oom killed | Debug Dashboard |
| quality_gate_idle_all_features | comparison | 10 | Oom killed | Debug Dashboard |
| quality_gate_logs | baseline | 10 | Oom killed | Debug Dashboard |
| quality_gate_logs | comparison | 10 | Oom killed | Debug Dashboard |
| quality_gate_metrics_logs | baseline | 10 | Oom killed | Debug Dashboard |
| quality_gate_metrics_logs | comparison | 10 | Oom killed | Debug Dashboard |
| quality_gate_security_idle | baseline | 10 | Oom killed | Debug Dashboard |
| quality_gate_security_no_fs_load | baseline | 10 | Crashed (exit code: 134) | Debug Dashboard |
| quality_gate_security_no_fs_load | comparison | 10 | Oom killed | Debug Dashboard |
CI Pass/Fail Decision
✅ Passed. All Quality Gates passed.
- quality_gate_security_idle, bounds check memory_usage: 10/10 replicas passed. Gate passed.
- quality_gate_security_idle, bounds check cpu_usage: 10/10 replicas passed. Gate passed.
- quality_gate_logs, bounds check memory_usage: 10/10 replicas passed. Gate passed.
- quality_gate_logs, bounds check total_bytes_received: 10/10 replicas passed. Gate passed.
- quality_gate_logs, bounds check missed_bytes: 10/10 replicas passed. Gate passed.
- quality_gate_logs, bounds check intake_connections: 10/10 replicas passed. Gate passed.
- quality_gate_private_action_runner, bounds check memory_usage: 10/10 replicas passed. Gate passed.
- quality_gate_metrics_logs, bounds check cpu_usage: 10/10 replicas passed. Gate passed.
- quality_gate_metrics_logs, bounds check missed_bytes: 10/10 replicas passed. Gate passed.
- quality_gate_metrics_logs, bounds check memory_usage: 10/10 replicas passed. Gate passed.
- quality_gate_metrics_logs, bounds check intake_connections: 10/10 replicas passed. Gate passed.
- quality_gate_metrics_logs, bounds check total_bytes_received: 10/10 replicas passed. Gate passed.
- quality_gate_idle, bounds check total_bytes_received: 10/10 replicas passed. Gate passed.
- quality_gate_idle, bounds check memory_usage: 10/10 replicas passed. Gate passed.
- quality_gate_idle, bounds check intake_connections: 10/10 replicas passed. Gate passed.
- quality_gate_idle_all_features, bounds check intake_connections: 10/10 replicas passed. Gate passed.
- quality_gate_idle_all_features, bounds check total_bytes_received: 10/10 replicas passed. Gate passed.
- quality_gate_idle_all_features, bounds check memory_usage: 10/10 replicas passed. Gate passed.
- quality_gate_security_mean_fs_load, bounds check memory_usage: 10/10 replicas passed. Gate passed.
- quality_gate_security_mean_fs_load, bounds check cpu_usage: 10/10 replicas passed. Gate passed.
- quality_gate_security_no_fs_load, bounds check memory_usage: 10/10 replicas passed. Gate passed.
- quality_gate_security_no_fs_load, bounds check cpu_usage: 10/10 replicas passed. Gate passed.
f0743f8 to
f79074f
Compare
f79074f to
5c6612a
Compare
385dbe3 to
54e5119
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 54e51196d4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // | ||
| // To adopt new output after an intentional parser change: | ||
| // | ||
| // go test ./pkg/logs/internal/parsers/syslog -run TestCorpusGolden -update-golden |
There was a problem hiding this comment.
Document the golden update with dda inv
This new regeneration hint tells contributors to run raw go test, but this repository’s Go packages rely on custom build tags and the repo-wide workflow requires dda inv test instead. In the documented update path for the golden corpus, following this command can produce misleading local results or bypass the intended test wrapper; please replace it with the dda inv test --targets=... equivalent.
AGENTS.md reference: AGENTS.md:L35-L46
Useful? React with 👍 / 👎.
The syslog frame matcher chose its framing method from the first byte alone, so
any line starting with a nonzero digit was committed to RFC 6587 octet counting
before anything verified that the digits were a length. A Cisco NX-OS year-first
header ("2024 Apr 04 08:05:06 host %MODULE-5-...") was therefore read as
MSG-LEN=2024, and because MSG-LEN is the authoritative boundary the declared body
was never re-scanned for frame starts, silently swallowing every following
message -- including well-formed ones -- into one truncated log.
The malformed-resync path already required the complete "digits SP < digit"
signature via isSyslogFrameStart; that strictness simply was never applied to the
initial dispatch. Apply it there too, using a three-way verdict so a length
prefix split across reads waits for more bytes instead of being misclassified as
malformed.
Scanning the integration log pipeline fixtures, 34 samples across 10 integrations
begin with digits and a space and were affected, including Cisco MDS, Barracuda
SecureEdge, MySQL, and Squid.
Sending each syslog-fed integration's own pipeline fixtures through the parser
showed 28 of 119 messages losing every header field to the nilvalue, which also
makes remap_source routing on syslog.appname or syslog.hostname impossible. Four
distinct layouts were unrecognized:
- RFC 3164 with a non-padded single-digit day ("Jan 9 03:47:40"). The 15-byte
layout check is offset-based, so the 14-byte form put a digit where a space
was required and the whole header was abandoned. Emitted by BeyondTrust
Privileged Remote Access, Cisco ISE and Infoblox DDI.
- Cisco NX-OS year-first headers ("2024 Apr 04 08:05:06 host %MODULE-5-..."),
whose leading year also matches the RFC 5424 VERSION SP shape and so was
dispatched to the RFC 5424 parser and rejected for an over-long version.
- ISO 8601 in place of the BSD timestamp, both the Cisco ASA/FTD style where a
colon follows the timestamp and no hostname or tag exists, and the
hostname+tag style used by Delinea Secret Server and Picus Security.
- More than one space between the RFC 5424 VERSION and TIMESTAMP (Claroty CTD),
which yielded an empty timestamp and shifted every later header field.
Also extend the existing double-header detection: appliances such as Cisco ISE
repeat a BSD timestamp in the tag position, and its month abbreviation was being
extracted as the APP-NAME. Only ISO 8601 second timestamps were detected before.
Verified against the vendor fixtures the layouts came from, and fuzzed for 13.5M
executions across FuzzParse and FuzzParseBSDLine with no crashes.
… PRI Auditing the samples behind the timestamp work against vendor documentation and third-party parsers turned up three problems. The tolerance for multiple spaces between the RFC 5424 VERSION and TIMESTAMP rested on a single vendor fixture and could not be reproduced anywhere else, so it is removed rather than shipped. The year-first NX-OS layout was justified with a sample carrying an invented PRI. Cisco documents the actual remote-logging default as "<189>:2025 Mar 27 16:22:24 switch %SYSLOG-...", terminating the PRI with a colon, which did not parse. The colon is now skipped when a timestamp follows it, which also covers Cisco EMBLEM output; content that merely begins with a colon is unaffected. The remaining sample attributions are corrected: the ISO-offset case belongs to Cisco ASA rather than Ivanti, whose messages are conformant RFC 5424, and the repeated BSD header is a relay artifact rather than a Cisco ISE behavior.
A sample without a PRI says nothing about what its sender emits: a PRI is mandatory on the wire, but the default file templates of both rsyslog and syslog-ng drop it and render the timestamp as RFC 3339. The samples that lack one are therefore tailed-file forms, not evidence of device behavior, and the Delinea and NX-OS cases now say so. For the same reason the framer no longer cites an NX-OS header as its example of a digit-leading frame — real NX-OS traffic starts with a PRI. Any sender whose output arrives without one produces the ambiguity instead. Comments and test names only; no behavior change.
Unlike the other layouts here, a doubled BSD header is not what any integration documents its devices as emitting — it is produced by a relay that fails to recognize an already-formatted line and prepends its own header. That makes it a different problem with a different scope, so it is split out rather than carried here. The ISO variant of the check is untouched.
Auditing every message in the syslog integration corpus turned up ten that the parser handled wrongly. Three failed outright; the other seven parsed without error while putting values in the wrong fields, which is the worse outcome: syslog.hostname and syslog.appname drive source and service routing, so a bogus value misroutes the log with nothing to show for it. Five distinct causes: - Cisco omits the Device-ID when "logging device-id" is disabled, leaving only a separator before the "%FTD-1-430003:" mnemonic. FTD pads with spaces, ASA writes a colon straight after the timestamp. Cisco's syslog guide tells collectors to expect an optional colon "followed by zero or more spaces" here. Neither shape was handled, so the messages failed with "BSD: missing hostname" and lost their body entirely. - BIND and yum put the TAG where the HOSTNAME belongs, yielding hostnames of "network:" and "Erased:". A single trailing colon distinguishes the two, since RFC 3164 defines HOSTNAME as a hostname or IP address. Tokens with more than one colon stay HOSTNAME so IPv6 addresses ending in "::" are unaffected. - A relay that prepends its own header leaves the device's header in place, so a second timestamp lands in the TAG position and the scan reported an APP-NAME of "May" or "Oct". The parser already guarded the ISO form of this; extend it to the BSD form. - RFC 5424 mandates one SP between HEADER fields, but Claroty CTD pads with two. Counting single spaces shifted every field along by one and left TIMESTAMP as the empty string. No HEADER field may contain a space, so collapsing runs cannot merge fields. - A TAG with no terminator was claimed as APP-NAME, leaving MSG empty and discarding the body. RFC 3164 only recognizes a TAG by its terminator; without one the remainder is CONTENT. Verified against the 102-message corpus: exactly the ten intended messages change, all of them corrections, and the sweep for misattributed fields now comes back clean. Fuzzing both entry points for 27M executions found no crashes.
The hand-written tests in this package pin one sample per parsing decision, which is the right shape for asserting a behaviour but blind to the failure mode that matters most here: a message that parses without error into the wrong fields. Nobody writes a test asserting that HOSTNAME is "network:", so nothing catches it when it is. Add testdata/corpus.json, holding one message per syslog-capable integration copied verbatim from that integration's log-pipeline fixtures, along with every field the parser produces for it. 102 messages across 31 integrations, spanning all seven framing classes and both the PRI-bearing wire form and the PRI-less rendering a tailed file carries. Each record keeps the fixture path and line it came from, so a reviewer can trace any sample back. Comparing every field of every message means a header field quietly losing its value shows up as a diff rather than passing unnoticed. Verified by reverting one of the parser fixes: the golden fails and names the integration, the fixture line, and the field that moved. Regenerate with: go test ./pkg/logs/internal/parsers/syslog -run TestCorpusGolden -update-golden Octet-counted messages are stored without their RFC 6587 MSG-LEN prefix, since the framer strips it before the parser runs.
Three places decided independently whether a header began with a timestamp:
the colon skip after the PRI, the digit branch of the Parse dispatch, and
parseBSD itself. Each consulted bsdTimestampLen and isoTimestampLen
separately, so the same bytes were classified up to three times and the
dispatch could in principle disagree with the parse about where a header
starts.
Route all three through timestampLen, which returns the length and whether
the match was the ISO form. A digit-leading line can only match the
year-first BSD layout or ISO, so the two dispatch arms that both called
parseBSD collapse into one.
Two smaller consolidations:
- parseRFC5424 extracted its five HEADER fields through five near-identical
bail-out blocks. Those became identical once the fields shared a single
nextStart offset, so they reduce to five assignments and one exit.
- bsdTimestampLen splits on the first byte. Year-first is the only layout
opening with a digit and the rest open with a month abbreviation, so the
abbreviation is validated once rather than once per layout.
Add the missing test for an ISO timestamp followed by a colon and a body
carrying no Cisco mnemonic. Every ASA and FTD sample happens to carry one, so
that handling could be deleted with the whole suite still passing, while the
bodies it protects were dropped along with their message.
No behaviour change. benchstat over ten runs reports a geomean of -0.91%,
with bytes and allocations per op identical.
Frame detection accepted an RFC 6587 octet-counted frame after seeing only
"<" and a single digit following the MSG-LEN and its space. Ordinary prose
supplies that shape readily, so a newline-framed line such as
54 <1 minute elapsed
was classified as MSG-LEN=54. MSG-LEN is the authoritative frame boundary and
the body it declares is never re-scanned for frame starts, so the declared
body ran past the newline and tore the following frame in half:
[0] "<1 minute elapsed\n<134>Feb 10 12:00:00 flushhost FLUSH"
[1] "TAG[1]: well_formed_message"
That is the same corruption the MSG-LEN signature check exists to prevent,
one byte further along in the signature.
Require a whole PRI -- "<", a 1-3 digit PRIVAL, ">" -- through priLen, and
route both classifyOctetPrefix and isSyslogFrameStart through it so that
resynchronizing accepts exactly what the frame reader accepts. Otherwise a
malformed run is cut short at a candidate the reader would then reject,
splitting one coherent malformed frame into fragments. A PRI straddling a TCP
read still reports needMore rather than being rejected outright.
maxFrameStartLookbehind becomes a computed bound on the longest signature it
has to cover. The value is unchanged at 16 bytes.
Each new test was checked against the unfixed framer to confirm it reproduces
the corruption rather than passing vacuously.
Reported by Codex review on the pull request.
Cisco documents the IOS header as
seq no:timestamp: %facility-severity-MNEMONIC:description
where everything before the '%' varies with "service timestamps log
[datetime|uptime] [msec] [localtime] [show-timezone]". Three of those options
produced a header the parser could not read.
show-timezone appends the zone name directly after the seconds, which lands in
the TAG position:
<187>Mar 1 18:46:11 UTC: %LINK-3-UPDOWN: Interface Serial0 up
That parsed without error and set APP-NAME to "UTC", so every device sharing a
timezone would route under a service named after it -- a misattribution with
nothing to signal it, which is the failure this package is most concerned with.
Absorb the zone into the TIMESTAMP instead. The test is narrow on purpose: two
to five uppercase letters, terminated by a colon, and only when a Cisco
mnemonic follows. Without that last condition a short uppercase TAG such as
"GW: session opened" would be swallowed.
msec appends fractional seconds, which is the shape Cisco's own Embedded
Syslog Manager guide prints ("Mar 18 14:52:10.039:%LINK-5-CHANGED: ..."). The
timestamp matched only its 15-byte prefix, leaving the parse to fail on the
'.'; the message survived, since the wrapper always transmits the raw line, but
it lost its sub-second precision, was flagged as a parse error, and had any
CEF or LEEF payload inside it discarded. Accept the fraction on all four BSD
layouts, as isoTimestampLen already did on the ISO ones.
Cisco defines a leading '*' as meaning the clock never synchronized to a
reliable source. Step over it so the header behind it parses. The marker is not
kept in the TIMESTAMP, where it would defeat any downstream time parse; it
stays visible in the content, which is transmitted verbatim.
Uptime timestamps ("00:00:46:", "1w2d:") are deliberately left alone. They are
time since reboot rather than wall clock, so there is nothing to extract and
passing the line to MSG is already correct.
benchstat over eight runs: geomean +0.01%, allocations unchanged. Parse_BSD
alone shows +4.7% that I could not attribute -- ParseBSDLine exercises nearly
the same path at +0.8% -- and it is likely code layout.
54e5119 to
518c44c
Compare
What does this PR do?
Fixes two classes of defect in syslog collection over sockets, and adds a regression harness for the second.
The TCP framer no longer commits to RFC 6587 octet counting on the strength of a leading digit. It requires the whole
MSG-LEN SP PRIsignature — with a complete PRI, meaning<, a one-to-three digit PRIVAL, then>— before any digits are read as a length, and falls back to newline recovery otherwise. Because a TCP read can split anywhere, the test is three-way: an incomplete signature waits for more bytes rather than guessing. Frame detection and malformed-run resynchronization now share that one test, so the two cannot disagree about where a frame starts.The parser recognizes the header layouts appliances actually emit, and stops putting values in the wrong fields. Newly handled: non-padded single-digit days, Cisco NX-OS year-first timestamps, a colon terminating the PRI as in Cisco's EMBLEM dialect, ISO 8601 in place of the BSD timestamp, and the Cisco IOS
service timestampsoptions for fractional seconds, timezone names, and the unsynchronized-clock marker. Newly corrected: a TAG sitting in the HOSTNAME position, a relay's second timestamp mined for an APP-NAME, padded RFC 5424 field separators, an unterminated TAG swallowing the body, and the separator Cisco leaves behind when the Device-ID is disabled.Alongside those,
testdata/corpus.jsonpins the parser's complete output for 75 messages across 22 integrations, spanning all seven framing classes and both the wire form and the PRI-less rendering a tailed file carries.Motivation
The framing bug lost data silently and without bound, but only behind a payload that was already malformed. Framing method is chosen at a frame boundary, and conformant traffic opens every frame with a PRI, so reaching the digit-leading path at all requires the payload sitting at that boundary to be non-conformant. From there
MSG-LENis the authoritative frame boundary and the body it declares is never re-scanned for frame starts, so a digit run misread as a length swallowed everything behind it into one truncated log. The messages it swallowed could themselves be perfectly well-formed, which is what turned a single malformed payload into unbounded loss.The parser defects degraded attribution rather than content. An unrecognized layout returns every header field as the nilvalue, which alone makes source remapping on
syslog.appnameorsyslog.hostnameimpossible. A misattributed field is worse, because those same attributes drive source and service routing, so the log is misrouted with nothing to signal that anything went wrong — a timezone name read as an APP-NAME would route every device in a zone under a service named after it. The original line is preserved throughout; only extracted attributes change.The corpus exists because targeted tests cannot catch that second class. One sample per parsing decision is the right shape for asserting a behaviour, but nobody writes an assertion that HOSTNAME is not
network:, so nothing fails when it is. Comparing every field of every message turns a quiet misattribution into a diff.Every accepted layout is corroborated by vendor documentation or an independent parser implementation rather than by a single fixture; one tolerance that could not be reproduced beyond its originating fixture was dropped rather than shipped. Cisco's documented collector expectations drove the most recent round: the guides define the IOS header as
seq no:timestamp: %facility-severity-MNEMONIC:descriptionand enumerate theservice timestampsoptions that vary everything ahead of the%.Describe how you validated your changes
dda inv test --targets=./pkg/logs/internal/framer,./pkg/logs/internal/parsers/syslog, also run under-race.The golden corpus is the primary check on the parser work: reverting any one of the fixes makes it fail and name the integration, the fixture line, and the field that moved. Each framer regression test was run against the unfixed framer to confirm it reproduces the corruption rather than passing vacuously. Both parser entry points were fuzzed with no crashes, and
benchstatreports the combined parser changes as throughput-neutral with allocations per operation unchanged.New coverage sits in
syslog_octet_prefix_test.go, which asserts that a genuine octet-counted frame is still framed by length, that digit-leading non-frames fall through to newline recovery instead of swallowing their successors, and that a length prefix straddling a read boundary is not misclassified; and intimestamp_variants_test.go, which covers each accepted layout and cites the fixture or vendor guide it came from, noting whether the sample is a wire capture or a file rendering since only the former carries a PRI.Additional Notes
Two layouts Cisco documents are deliberately left unhandled. Uptime timestamps (
00:00:46:,1w2d:) are time since reboot rather than wall clock, so there is nothing to extract and passing the line through as content is already correct. The IOS sequence-number prefix is not yet recognized and remains a follow-up.Repeated BSD headers are also out of scope here. Unlike the other layouts, a doubled BSD header is not something any integration documents its devices as emitting — it comes from a relay that fails to recognize an already-formatted line — so it is a separate problem with a separate scope. The ISO form of that check is untouched.
Every corpus message names its origin, and each origin records the public repository and pinned commit, the vendor document, or that the sample was synthesized, so any entry can be traced without guesswork. Regenerate after an intentional parser change with: