Skip to content

Read the log prefix a managed PostgreSQL target renders, and test both parsers on one (#3030) - #3037

Merged
erikdarlingdata merged 3 commits into
devfrom
fix/3030-managed-log-prefix
Sep 5, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
fix/3030-managed-log-prefix

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

PgDeadlockLogParser.s_deadlockBlock requires two things a managed PostgreSQL target's log never renders, and so it matches no line of one. Extract returns nothing, RdsDeadlockIngestor stores nothing, and the cycle records the source as reached with zero rows — while advancing and persisting its resume marker, so the window is consumed and the text is not offered again. Fixes #3030.

log_line_prefix = '%m [%p] ', PostgreSQL's own default, renders 2026-08-26 22:25:24.100 UTC [1549] . The system default on a managed parameter group is '%t:%r:%u@%d:[%p]:', rendering 2026-08-26 22:25:24 UTC:<host>(<port>):<user>@<db>:[1549]:. Two things differ, and each is independently fatal — restoring only the fraction, or only the space, still yields nothing:

  • %t renders no fractional seconds where \.\d+ was mandatory. The fraction is optional now.
  • The prefix's own : sits where the pattern spelled a space, with %r and %u@%d between the zone and the pid, and %r renders empty on a line with no client connection.

The two prefix families are alternatives, not one relaxed pattern

The obvious single pattern — [ :] for the delimiter and an abbreviation-or-numeric-offset alternation for the zone — is wrong in the worst available direction: a wrong answer rather than a missing one. A numeric offset carries colons of its own, so under a colon-delimited prefix the zone cannot tell its own colon from the prefix's. On +00:2001:db8::1(...) — a zero-offset zone and an IPv6 %r — the offset loop takes :2001 as another of its groups, IPv6's next colon satisfies the delimiter, nothing backtracks, and the zone reads +00:2001. IsZeroOffsetLogZone refuses that, so a target whose log_timezone is zero-offset has every window abandoned with a message naming a setting that is already correct — every cycle, for as long as a client keeps connecting from that address. Bounding the offset's inner groups to two digits only moves the boundary: +00:20:db8::1(...) breaks it again, because 0020 renders as 20.

Splitting by delimiter removes the ambiguity instead of narrowing it. The space family is character-for-character what it was, so every existing zone shape captures identically; the colon family excludes : from the zone outright. Both name the same two groups, which .NET allows across alternatives.

The trade is that a numeric offset under a colon-delimited prefix is read up to the delimiter, so a non-zero one refuses as -03 rather than -03:30. The verdict is unchanged in every case — measured across 22 renderings — and only the token in the message is shorter, on a prefix family no measured target combines with a numeric zone. Reading a UTC server as non-UTC is the failure worth designing against; a terser refusal message is not.

The plan parser

PgPlanLogParser.s_planBlock has the same delimiter requirement after the pid, and takes a space or a colon there now. That route is inert on a managed target with the system default prefix regardless, because auto_explain's query id only reaches the log through %Q and the system default has none — but appending the %Q this parser already requires gives '%t:%r:%u@%d:[%p]:%Q ', and the line then reads ]:<id> LOG:, which is what the change accepts.

The fixtures are the other half of the fix

Swept over 2,688 tracked files with the self-hosted prefix as a firing positive control and the managed prefix cross-controlled against it: 27 log-prefix lines across five files, every one self-hosted, none managed anywhere in the tree. RdsDeadlockIngestorTests is the managed transport's own suite, and its DeadlockText is captured self-hosted output — so the transport was proven over text no managed target emits, and a regex-only fix would leave the same class of defect undetectable.

That suite now carries a managed-prefix report driven through the real ingestor. The parser suites carry the four prefix regimes as separate tests — both requirements present, each one absent alone, and neither present — plus the combination that decides the zone question and previously had no coverage at all: a numeric zero-offset zone under a colon-delimited prefix, with an IPv4 %r, an IPv6 %r whose leading hextet is four digits, and one whose leading hextet is two.

Identifiers in the new fixtures are synthesised. 192.0.2.10 is RFC 5737 documentation space and 2001:db8::1 is RFC 3849.

Verification

Compiled the real Windows-only test files into net10.0 xunit v3 hosts on macOS. Lite 63/63 and Darling 109/109 pass, 0 failed, 0 skipped, 0 not run. The Darling host covers all six Darling.Tests files that reference either parser or either collector, plus DocCommentHygieneTests and FleetIdentifierScrubTests; both guards were shown to reach the changed files by planting a violation in one of them and watching each name the file and line.

Six mutations, each against a committed baseline, rebuilt every cycle, restored by content-verified single-path checkout. Each half reverted separately reddens a different named assertion and leaves the other half's green:

mutation red still green
fractional seconds mandatory again ParsesAStampWithNoFractionalSeconds — Assert.Single empty ParsesAColonBeforeTheBracketedPid
colon family removed ParsesAColonBeforeTheBracketedPid — Assert.Single empty ParsesAStampWithNoFractionalSeconds
families collapsed to one relaxed pattern both IPv6 cases of ANumericZeroOffsetZoneParsesUnderAColonDelimitedPrefix — Assert.Null, i.e. a refusal, not a miss the IPv4 case, and both regime tests
plan delimiter reverted ExtractReadsAManagedPrefixAlongsideASelfHostedOne (2 → 1) every deadlock test
ReportUnder made an identity the re-rendering control at Expected 3, Actual 0, and five regime tests on their timestamp assertions —
ManagedDeadlockText collapsed to DeadlockText the managed fixture's control on Assert.DoesNotContain —

The third row is the one that matters most: with the single relaxed pattern the IPv4 fixture passes and only the IPv6 ones fail, which is exactly why an IPv4-only fixture set could not have caught it.

The self-hosted format still parses, by count and not by assertion: the 45 pre-existing tests across PgDeadlockLogParserTests and PgPlanLogParserTests are unchanged and green, +00:00, -00, -0000, GMT and UCT prefixes still parse, and EST, +07, -03:30 and +0530 still refuse with the observed zone reported verbatim under the space-delimited family. A 22-case adversarial probe against the built assembly — patterns dumped back out of PerformanceMonitor.Collectors.dll by reflection — covers both families, both isolating regimes, IPv6 and empty %r, a bracketed app name before the pid, CRLF bodies, a mixed slab (2 reports, no duplicates), and every zone spelling. All 22 verdicts correct.

Scope boundary

The SQL counterparts of both patterns — PgDeadlocksCollector and PgPlanCaptureCollector, which serve the pg_read_file route — carry the space family only. Both parsers now say so in place of assuming parity. They are left narrow deliberately: that route only runs against self-hosted targets, the verification rig's own prefix is %m [%p] %Q , widening them needs any new groups spelled non-capturing so m[1]…m[4] do not shift, and there is no PostgreSQL here to exercise ARE against.

CHANGELOG entry

Not committed, per the routing rule. For the [Unreleased] → ### Fixed block:

- **Deadlock capture read nothing at all from a managed PostgreSQL target's log, and reported the window as read and empty** ([#3030]) - `PgDeadlockLogParser.s_deadlockBlock` required fractional seconds (`\.\d+`, which only `%m` renders) and a space before the bracketed pid. The system default `log_line_prefix` on a managed parameter group is `'%t:%r:%u@%d:[%p]:'`, which renders neither, so the pattern matched no line of such a log; `Extract` returned nothing, `RdsDeadlockIngestor` stored nothing, and the cycle recorded the source as reached with zero rows while advancing its resume marker, consuming the window permanently. **Both halves are independently fatal** - restoring only the fraction, or only the space, still yields zero. The two prefix families are now ALTERNATIVES rather than one relaxed pattern, and that distinction is load-bearing: a numeric zone offset carries colons of its own, so a single pattern using `[ :]` as the delimiter cannot tell the zone's colon from the prefix's, and on `+00:2001:db8::1(...)` - a zero-offset zone and an IPv6 `%r` - the offset loop eats `:2001`, IPv6's own colon satisfies the delimiter, nothing backtracks, and the zone reads `+00:2001`. `IsZeroOffsetLogZone` refuses that, so a target that IS zero-offset would have every window abandoned with a message naming a setting already correct; bounding the offset's groups to two digits only moves the boundary, since `0020` renders as `20`. The space family is character-for-character what it was, so every existing zone shape captures identically; the colon family excludes `:` from the zone, at the cost of reading a numeric offset up to the delimiter there (`-03` rather than `-03:30`) with the verdict unchanged in all 22 measured renderings. `PgPlanLogParser.s_planBlock` had the same delimiter requirement after the pid and takes either character now too. **Every log fixture in the tree carried the self-hosted prefix and none carried the managed one** (27 lines across five files, swept over 2,688 tracked files with a firing positive control), including `RdsDeadlockIngestorTests`' - the managed transport's own suite, proving its plumbing over text no managed target emits. That suite now carries a managed-prefix report driven through the real ingestor, and the parser suites carry the four prefix regimes plus the numeric-zone-under-a-colon-prefix combination that had no coverage, so each half reverted alone reddens a different assertion.

And the link reference:

[#3030]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/3030

PgDeadlockLogParser's block pattern requires fractional seconds and a
space before the bracketed pid. A managed parameter group's system
default log_line_prefix, '%t:%r:%u@%d:[%p]:', renders neither: %t has no
fraction, and the prefix's own ':' sits where PostgreSQL's default puts a
space. The pattern therefore matches no line of such a log, Extract
returns nothing, and the RDS ingest route reports a window it read and
found empty while consuming it on the way out.

Both halves are separable and each is independently fatal: restoring only
the fraction, or only the space, still yields nothing. The pattern now
takes the fraction as optional and either a space or a colon before the
bracket, with the prefix run between the delimiter and the bracket
absorbed lazily - %r and %u@%d land there, and %r renders empty on a line
with no client connection.

The zone is bounded by the prefix's own delimiter. Allowed to run to the
last delimiter before the bracket it reads UTC:<host>(<port>):<user>@<db>,
which IsZeroOffsetLogZone refuses - which would abandon every window on a
fleet whose log_timezone is UTC, naming a setting that is already correct.
Two alternatives, since only a numeric offset carries a colon of its own,
so -03:30 stays whole and a refusal names the zone it read.

PgPlanLogParser's pattern has the same delimiter requirement after the
pid, and auto_explain's %Q appended to that prefix renders `]:<id> LOG:`.
It takes a space or a colon there too.

The fixtures are the other half. Every log fixture in the tree carried the
self-hosted prefix, including the one in RdsDeadlockIngestorTests, which
is the managed transport's own suite - so the transport was proven over
text no managed target emits. That suite now carries a managed-prefix
report, driven through the real ingestor, and the parser suites carry the
four prefix regimes: both requirements present, each one absent alone, and
neither present.

Identifiers in the new fixtures are synthesised; 192.0.2.10 is RFC 5737
documentation space.

The SQL counterparts of both patterns, in PgDeadlocksCollector and
PgPlanCaptureCollector, serve the pg_read_file route and still require the
fraction and the space. Both parsers now say so.
Comment thread PerformanceMonitor.Collectors/PgDeadlockLogParser.cs Outdated
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed the parser/regex change plus the new fixtures across PgDeadlockLogParser, PgPlanLogParser, Lite.Tests, and Darling.Tests.

Scope/parity: PerformanceMonitor.Collectors is a shared project referenced by both Lite and Darling, so this fix and its tests land on both apps automatically — no Lite/Darling drift here. The SQL counterparts (PgDeadlocksCollector, PgPlanCaptureCollector) are deliberately left untouched and the PR says so; that's consistent with the stated scope boundary (self-hosted-only, pg_read_file route). No T-SQL in this diff, so the T-SQL style rules don't apply.

One correctness finding, left as an inline comment on PgDeadlockLogParser.cs's regex: the new zone alternative [+-]\d+(?::\d+)* (added to keep a colon-containing offset like -03:30 intact under the colon-delimited managed prefix) can absorb part of an IPv6 client address into the zone capture when log_timezone renders as a bare numeric offset and the deadlocked backend's client connected over IPv6 with an all-digit leading hextet (e.g. 2606:4700::/32, or the 2001:db8::/32 doc range) — because IPv6 addresses use : as their own delimiter, the regex's mandatory [ :] boundary check is satisfied one iteration too late, with no backtrack to correct it. That corrupts the captured zone, IsZeroOffsetLogZone then refuses it, and per this file's own documented contract that throw aborts the whole read (siblings included) and — on the consume-once RDS route — leaves the resume marker uncommitted, so the same window keeps failing every cycle. This is essentially the same failure class #3030 fixes, reintroduced for a specific but realistic managed-target shape.

Every new fixture in this PR uses an IPv4 host (192.0.2.10) for the managed-prefix regime, and the existing numeric-offset-zone tests (RefusesANumericNonZeroOffsetPrefix_RatherThanMatchingNothing, AZeroOffsetPrefixStillParses) only exercise the space-delimited self-hosted prefix, where this ambiguity can't arise. So the one combination that stresses the new alternation's reason for existing — numeric zone offset and colon-delimited prefix — isn't covered. Worth adding a fixture with an IPv6 %r (all-digit leading hextet) under the managed prefix to confirm/fix before merge.

Everything else — the (?:\.\d+)? fraction relaxation, the [ :] pid-delimiter widening, the lazy [^\n]*? pid search skipping non-numeric bracketed content (e.g. IPv6 in brackets, empty %r), and the PgPlanLogParser counterpart change — traces correctly against the fixtures given, and the new tests are well-targeted (the mutation table in the PR description checks out against the code).

One pattern spanning both prefix families needs the zone to be an
abbreviation-or-numeric-offset alternation and the pid delimiter to be
[ :]. A numeric offset carries colons of its own, so under a
colon-delimited prefix the zone cannot tell its own colon from the
prefix's. On `+00:2001:db8::1(...)` - a zero-offset zone and an IPv6 %r -
the offset loop takes `:2001` as another of its groups, IPv6's next colon
satisfies the delimiter, nothing backtracks, and the zone reads
`+00:2001`. IsZeroOffsetLogZone refuses that, so a target whose
log_timezone IS zero-offset has every window abandoned with a message
naming a setting that is already correct, every cycle, for as long as a
client keeps connecting from that address. Bounding the offset's groups
to two digits only moves the boundary: `+00:20:db8::1(...)` breaks it
again, because 0020 renders as `20`.

The two families are alternatives now. The space family is what
PostgreSQL's own default renders and is character-for-character what it
was, so every existing zone shape captures identically. The colon family
excludes ':' from the zone outright, which removes the ambiguity rather
than narrowing it. Both name the same two groups, which .NET allows
across alternatives, so the reads below do not care which matched.

The trade is that a numeric offset under a colon-delimited prefix is read
up to the delimiter, so a non-zero one refuses as `-03` rather than
`-03:30`. The verdict is unchanged in every case; only the token in the
message is shorter, and only on a prefix family no measured target
combines with a numeric zone.

Three fixtures for the combination that decides it, which had none: a
zero-offset numeric zone under a colon-delimited prefix with an IPv4 %r,
with an IPv6 %r whose leading hextet is all digits, and with one whose
leading hextet is two digits. Each asserts the absence of a refusal
separately from the count, because a misread zone and an unmatched block
are different failures. A fourth pins that a non-zero offset still
refuses there, and that the space-delimited prefix still names it whole.

2001:db8::1 is RFC 3849 documentation space.
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed. No blocking issues found.

Scope: only PerformanceMonitor.Collectors/PgDeadlockLogParser.cs and PgPlanLogParser.cs (shared by Lite and Darling per CONTRIBUTING.md), plus tests in both Lite.Tests and Darling.Tests. No T-SQL touched, so the T-SQL style rules don't apply here.

Correctness — traced the new alternation by hand against the added test fixtures:

  • Space family ((?<zone>[^ \n]+) \[(?<pid>\d+)\]) still requires a literal space immediately before the bracket, so it correctly fails to match colon-delimited (managed) prefixes and falls through to the second branch — no regression on the self-hosted shape.
  • Colon family ((?<zone>[^ :\n]+):[^\n]*?\[(?<pid>\d+)\]) excludes : from the zone, which is exactly what defuses the IPv6/%r ambiguity described in the comment (+00:2001:db8::1(...) no longer misreads the zone as +00:2001). Verified against all the ZeroOffsetColonPrefix*/NonZeroOffsetColonPrefix cases — the truncated-to-first-colon tradeoff for non-zero numeric offsets is called out and tested (ANumericNonZeroOffsetZoneStillRefusesUnderAColonDelimitedPrefix).
  • .NET allows the duplicate zone/pid group names across the two alternatives, so downstream reads in Extract/FromBlock stay indifferent to which branch matched — confirmed this is legal .NET regex behavior.
  • Plan parser's [ :] after \[\d+\] is a minimal, consistent change with the deadlock parser's reasoning.
  • RdsDeadlockIngestorTests's new ManagedDeadlockText relies on string.Replace rewriting all occurrences of SelfHostedPrefix (there are 3 — ERROR/DETAIL/HINT lines) — intentional and consistent with the docstring, not a bug.

Lite/Darling parity — the parser fix lives in the shared PerformanceMonitor.Collectors project and gets test coverage added to both Lite.Tests and Darling.Tests, so no drift. The PR's own comments note that PgDeadlocksCollector/PgPlanCaptureCollector (the SQL pg_read_file route) intentionally stay narrower (space-family only) because that route only ever runs against self-hosted targets that render PostgreSQL's own default prefix — managed/RDS targets go through the C# log-API route this PR fixes. That's a documented design split, not an inconsistency.

Security/perf — no injection surface (log text parsing only, not used to build queries), no secrets, regexes stay linear (lazy quantifier bounded by [^\n], no nested-quantifier blowup risk).

Note: I wasn't able to actually execute dotnet build/dotnet test in this sandbox (the tool required approval I couldn't obtain), so this review is static/manual tracing of the regex against the new fixtures rather than a confirmed green test run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant