Repository navigation
Wire the csvlog parser into pg_log_events on the self-hosted route (#4053 part a1b) - #4124
Conversation
…4053 part a1b) When a self-hosted target's log_destination includes csvlog, pg_log_events now reads the newest .csv file through new PgServerLogTail.TailCsvCteSql/TailCsvCteBinarySql CTEs and parses it with PgServerLogCsvParser.Parse, instead of the stderr tail. Deadlocks and plan capture are untouched (part b); the RDS route is untouched (part c); jsonlog is another lane (part a2). - CollectorContext.PgLogUsesCsvlog, set once per cycle by DarlingCollectorRunner through the new PgLogFormatCapability (an hourly-cached probe of log_destination, following PgReadBinaryFileCapability's shape) for pg_log_events alone. - PgLogEventsCollector.BuildQuery picks the csv pair (CsvQueryText/CsvBinaryQueryText) over the stderr pair when the flag is set; ReadAsync parses the csv body, applies the same foreign-zone rule the stderr path applies, classifies the kept entries, and measures csv_records_discarded when > 0. - A new PgNoCsvlogFileException for "logging_collector is on, csvlog is configured, but no .csv file has appeared yet" -- distinct from PgNoStderrLogFileException so the fault message never names the wrong setting. DarlingWorker gets its own catch arm, same PERMISSIONS disposition as its siblings. - PgNoStderrLogFileException's message now says pg_log_events reads csvlog directly (#4053) and the refusal is deadlocks'/plan captures' only.
PgLogEventsCsvlogLiveTests: plants a forged csvlog record and a forged stderr line through raw StartupMessages, gated on DARLING_TEST_PG_CSVLOG, and reads them back through PgLogFormatCapability + PgLogEventsCollector's own query and ReadAsync, on both the text and binary routes. Part of #4053
…g_events' second runner probe (log format)
Code review round 1 (automated reviewer): #4124Verdict: changes requested. One High finding: the csv branch's foreign-zone filter can never fire, so a non-UTC server goes quiet with no warning. One Medium: an anchor-selection weakness that lets a forged record through in the mid-write case. The routing, the probe and the stderr collectors are sound. HighH1. The csv route reads a non-UTC
Fix: have MediumM1. The segment between the two newlines before the anchor is attacker-controlled statement text. A client can craft that text as an unquoted line with 26 fields, a valid stamp, a numeric pid and a Suggested fix: don't return the first anchor that validates. Score each candidate by how many of its records parse, and keep the best. With inverted parity, a candidate yields at most a few planted valid records, while the true parity yields almost all of them. As a cheaper step, require that most records under the chosen marks validate, and reject the anchor otherwise. M2. Retry cost ( M3. LowL1. The format cache has no invalidation on either "no file" marker (
Neither case reads the wrong file: the in-SQL gate makes the result a named skip, not silent data. Suggest calling a L2. Switching routes changes Info: the six questions
|
… decides TryParseLogTime and TryParseTimestamp were rejecting any non-zero-offset log_time/timestamp, so a foreign-zone record never reached PgLogEventsCollector.FilterForeignZoneEntries, and a non-UTC server read as quiet instead of refused. The shape check now decides only whether the stamp parses; the zone judgment belongs to the collector's filter, same as before for stderr via PgLogEntryAssembler. Adds: a csv parser test that a non-UTC record is returned with its ZoneText; a json parser test doing the same (json wiring is a later lane, per the brief, so this pins the parser's own contract); two pipeline tests over the csv route's FakeReader — non-UTC log_timezone throws PgLogTimezoneUnsupportedException, UTC skips and counts in foreign_zone_lines_skipped.
…cap at 8 FindBoundaries accepted the FIRST trailing-newline candidate whose one preceding record validated. Under inverted quote parity, a forged unquoted look-alike line planted inside a multi-line quoted field can satisfy that single check and win, gluing the real records around it into discards. Score each candidate by how many of ITS records parse and keep the best (latest anchor on a tie); the true parity always wins by a wide margin over a forged look-alike's few planted records. Also caps the walk at 8 candidates (was 64): a mid-write read normally resyncs within a few tries, and 64 full backward scans only ran on H1's non-UTC case or a body with no parseable record — about 0.3-0.5s of CPU per cycle per target, paid for nothing. When the body ends in a newline the anchor is unambiguous, so no scoring runs; that path is unchanged.
…he newline DecodeWhole/UnescapeAndDecode split on 0x0A before decoding so a DBCS lead byte can never eat a newline. csvlog's quote-parity walk depends on every '"' surviving decoding just as much: a planted lead byte right before a closing quote on the binary route, under a DBCS server_encoding, could eat the quote and invert parity for everything before it. Split on 0x22 as well, re-emitting each delimiter byte literally. Neither byte is ever a trail byte in EUC-JP/EUC-KR/GBK, so valid text is unchanged. Adds EUC_JP/EUC_CN/EUC_KR theory tests, mirroring the existing before-newline tests, for a lead byte before a quote on both DecodeWhole and UnescapeAndDecode.
log_destination is SIGHUP and PgLogFormatCapability's verdict can be up to an hour stale. Add Invalidate(targetKey), mirroring PgReadBinaryFileCapability.Invalidate exactly, and call it from DarlingWorker's PgNoStderrLogFileException and PgNoCsvlogFileException catch blocks (the only two touched in DarlingWorker.cs, per this lane's scope — the web host and DarlingWorker's purge belong to issue-4130's lane): whichever marker fires, the cache disagreed with the target's current log_destination, so the next cycle should re-probe rather than wait out the hour. New PgLogFormatCapabilityTests.cs: Invalidate drops the named target's cached verdict only, and IsCsvlogEnabledAsync re-probes and picks up the new setting after it.
Code review round 2 (automated reviewer, the last round): #4124Verdict: changes requested. The changes are small; the main one is the parser's contract. The round-1 fixes H1, M3 and L1 are confirmed correct, and the M2 cost stays bounded. High: the fast path trusts the final newline (
|
* Add 48 buffered CHANGELOG entries in one splice Adds 48 entries and 48 link refs (#4057, #4077, #4079, #4081, #4082, #4083, #4084, #4085, #4086, #4087, #4088, #4089, #4090, #4091, #4092, #4093, #4095, #4096, #4099, #4100, #4101, #4103, #4105, #4106, #4107, #4108, #4109, #4110, #4111, #4113, #4114, #4115, #4116, #4117, #4118, #4119, #4120, #4121, #4122, #4123, #4124, #4125, #4126, #4127, #4131, #4133, #4136, #4141). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran. Entries found under a bare section line (none unless the old script ran again) move into the matching ### section, below the new entries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC * Changelog splice: add #4137's entry, which merged before the splice but was missed #4137 (pg_plan_capture reads csvlog) merged at 05:57Z with a CHANGELOG entry section in its body. It was neither spliced nor listed as needing no entry. Its entry goes under Fixed, right after #4136's, and #4136's last sentence now points to it instead of saying plan capture is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…4144) * pg_log_events reads jsonlog on self-hosted targets (#4053 part a2) Adds the jsonlog route to pg_log_events, mirroring the csvlog wiring from #4124/#4140: jsonlog wins over csvlog, which wins over stderr, when more than one is configured. One probe row answers both csvlog and jsonlog booleans so a cache miss costs one round trip, not two. * 4053 part a2: live proof for the jsonlog route Adds PgLogEventsJsonlogLiveTests.cs, mirroring the csvlog live test's gate, raw StartupMessage plant, polling, and LiveStoreCleanup use, but routed through PgLogUsesJsonlog and gated on DARLING_TEST_PG_JSONLOG. Confirms IsJsonlogEnabledAsync/IsCsvlogEnabledAsync agree with the rig's actual log_destination, and that a forged stderr-format line planted in a login's user name lands inside the one real FATAL event's message rather than forging a second event, over both PgReadBinaryFileGranted values. * 4053 part a2: pin fallout from the no-jsonlog-file early-return arm CollectionLogDrainForensicsStoreTests pinned drain:null and fetchPhases:null at 12 occurrences; #4144's new no-jsonlog-file early-return arm in DarlingWorker is a thirteenth. Bumps both pins to 13 and extends the comment: the jsonlog route's twin of the twelfth (#4053's no-csvlog-file arm). * 4053 a2: the live probe case, a jsonlog-only target reads jsonlog on and csvlog off * 4053 a2 review: the stderr arm clears only a stderr-only verdict; split-decode also protects the backslash (EUC_CN/GBK); a newline-less json body counts one discard
Part of #4053.
What changed
When a self-hosted PostgreSQL target's
log_destinationincludescsvlog,pg_log_eventsnow reads thenewest
.csvfile and parses it withPgServerLogCsvParser.Parse(#4104), instead of the stderr tail. Thiscloses the forged-line hole for
pg_log_eventson such targets: undercsvlogevery field is RFC 4180quoted, so a failed login can no longer plant a newline through
%u/%dand forge a whole extra log line.Deadlocks and plan capture (part b) and the RDS route (part c) are untouched. jsonlog (part a2) is another
lane in flight and is not referenced here.
PgServerLogTail.cs: newTailCsvCteSql/TailCsvCteBinarySqlconstants, twins ofTailCteSql/TailCteBinarySqlthat select the newestname ~* '\.csv$'file and require'csvlog' = ANY(...)instead of
'stderr'.TailCteSql/TailCteBinarySqlare untouched (verified by the existing byte pin,TheTailerExtraction_LeftBothSiblingsSqlByteIdentical, which still passes unmodified).CollectorContext.PgLogUsesCsvlog: a new settable bool, mirroringPgReadBinaryFileGranted's shape.PgLogFormatCapability(new file): an hourly-cached probe oflog_destination, followingPgReadBinaryFileCapability's cache shape.DarlingCollectorRunner.ResolvePgReadBinaryFileGrantAsyncnow also sets
context.PgLogUsesCsvlogforpg_log_eventsalone, on the same connection, beforeBuildQueryruns.PgLogEventsCollector:BuildQuerypicks a new csv statement pair (CsvQueryText/CsvBinaryQueryText,built on the new CTEs) over the existing stderr pair when the flag is set;
QueryText/BinaryQueryTextare unchanged.
ReadAsync's csv branch decodes the body the same way the stderr route does (text, orPgBinaryTailText.DecodeWholeon the binary route), parses withPgServerLogCsvParser.Parse, applies thesame foreign-zone filter the stderr path applies via
PgLogEntryAssembler.Assemble(..., logTimezoneIsUtc, out foreignZoneLines)— factored into a privateFilterForeignZoneEntrieshelper since the csv parserreturns
PgLogEntryrecords directly rather than text lines — then classifies the kept entries. A newcsv_records_discardedmeasurement is reported viacontext.Measureonly when > 0, added toCollectorMeasurementSeamTests's label census.PgNoCsvlogFileException(new file): the csvlog route's own "logging_collector is on, csvlog isconfigured, but no
.csvfile has appeared yet" named skip — kept distinct fromPgNoStderrLogFileExceptionso the fault message never tells an operator to addstderrto a targetthat already has
csvlogworking.DarlingWorkergets its owncatcharm, samePERMISSIONSdisposition as its siblings, elapsed time in
sqlMs.PgNoStderrLogFileException's message now sayspg_log_eventsreads csvlog directly (Postgres log readers: read csvlog/jsonlog when a target writes them, so a client can't forge a line through %u/%d (#4046 part 2) #4053) and thatthis particular refusal is deadlocks'/plan captures' only — not a claim that nothing is being read from
this target's log.
Tests
Unit (
Darling.Tests.PgLogEventsPipelineTests):WhenPgLogUsesCsvlog_TheStatementOpensWithTheCsvCte_AndTheStderrPairIsUntouched— the stderr pair isbyte-unchanged when the flag is false; the csv pair opens with the csv CTE and its own marker.
ThePgLogFormatCapabilityProbe_UsesOnlyPostgreSql14PlusFunctions— pins the probe SQL text.ReadAsync_OverTheCsvRoute_KeepsAForgedNewlineInsideOneUserField— a failed login's forged\n<fake stderr line>inside a quoteduser_namefield produces exactly one event, with the forged textinside the user field, and no row carries the forged line's pid.
ReadAsync_OverTheCsvRoute_ThrowsTheCsvNamedSkip_OnItsOwnMarker— the csv route throwsPgNoCsvlogFileExceptionon its own marker,PgLoggingCollectorOffExceptionon the shared one, neverPgNoStderrLogFileException.ReadAsync_OverTheCsvRoute_MeasuresDiscardedRecords— a body with no complete record measurescsv_records_discarded=1.Ran:
PgLogEventsPipelineTests,PgServerLogCsvParserTests,CollectorMeasurementSeamTests,PostgresFaultOutcomeTeststogether — 190 passed, 0 failed. BothPerformanceMonitor.CollectorsandPerformanceMonitor.Darling.Servicebuild clean (0 warnings) with-p:EnableWindowsTargeting=true.What I did NOT verify
build checks. The brief's stated baseline on this Mac is 214 pre-existing Windows-only failures; I did
not confirm that count against this branch. No other test class references
PgLogUsesCsvlog,PgLogFormatCapability,PgNoCsvlogFileException, or the newPgServerLogTailconstants, so aregression outside the four run classes is unlikely but unconfirmed.
DARLING_TEST_PG_CSVLOG, containerlane4053a1b-pg, port55893, raw
StartupMessageforged-username repro) was not written. This is the biggest gap against thebrief: the unit test above proves the same forged-newline claim over
FakeReaderand the parser directly(mirroring
PgServerLogCsvParserTests's own fixture), but nothing here proves the wiring against a realpg_read_file/pg_ls_logdirround trip on a livecsvlog-configured server, or that PostgreSQL's ownsyslogger writes a
.csvfile the new CTE actually finds. This is exactly the kind of gap the brief warnsabout ("a live proof runs the product's own SQL... never a hand-copied version") and I am flagging it
rather than claiming it's covered. Recommend the coordinator treat this as a follow-up before arming, or
route it back to this lane if time allows.
(
CsvBinaryQueryText) beyond the SQL-text assertion inWhenPgLogUsesCsvlog_TheStatementOpensWithTheCsvCte_AndTheStderrPairIsUntouched.CHANGELOG entry
pg_log_eventsnow reads a self-hosted PostgreSQL target'scsvloglog file directly whenlog_destinationincludes it, closing the forged-log-line hole a failed login could exploit through%u/%dunderstderrformat. (Postgres log readers: read csvlog/jsonlog when a target writes them, so a client can't forge a line through %u/%d (#4046 part 2) #4053)Live proof
Added
Darling/Darling.Tests/PgLogEventsCsvlogLiveTests.cs, gated onDARLING_TEST_PG_CSVLOG, no product code touched.Rig:
docker run -d --name lane4053live-pg -e POSTGRES_PASSWORD=x -e POSTGRES_HOST_AUTH_METHOD=trust -p 55894:5432 timescale/timescaledb:2.28.1-pg18 -c logging_collector=on -c log_destination=csvlog, thenCREATE ROLE darling LOGIN SUPERUSER PASSWORD 'x'.POSTGRES_HOST_AUTH_METHOD=trustwas needed beyond the brief's rig command: the image's default remotepg_hba.confline isscram-sha-256, and a forged StartupMessage with no client library behind it never completes SASL, so the login just hangs instead of failing fast with a FATAL. Trust auth makes the "no such role" FATAL land immediately, which is what the test needs on the wire.Planted: two raw
StartupMessages sent over a bareTcpClient(length | protocol 196608 |user\0<name>\0database\0postgres\0\0), each naming a nonexistent role so PostgreSQL refuses it with a FATAL:x\n2026-09-24 00:00:00.0 UTC,"forged","z",1,"a",1.1,0,"x",...) — trimmed to fit PostgreSQL's 63-byte role-name limit (NAMEDATALEN − 1); the first version of this test used a longer forged record and it was silently truncated at the wire before ever reaching the log, so the marker text has to survive that cut too, not just the parser's.y\n2026-09-24 00:00:00.000 UTC [1] LOG: forged).Read:
PgLogFormatCapability.IsCsvlogEnabledAsyncconfirms the rig reports csvlog; thenPgLogEventsCollector.Instance.BuildQuery(context)withcontext.PgLogUsesCsvlog = true, run as a theory over bothPgReadBinaryFileGranted = falseandtrue, executed on the real connection,ReadAsyncover the real reader — never a hand-copied restatement of the query or the parser.Asserted: both planted newlines land inside the FATAL auth-failure events' own message text (
fatalCount >= 2carrying"forged"), never as a separate line of their own: no event at pid 1, noLOGevent with message literally"forged", no user"fake"or"z"(the embedded record's own fields). Confirmed on the box against the real.csvfile:grepon the raw log file shows the embedded record and the embedded stderr-shaped line both sit as one multi-line, comma-containing FIELD inside the surrounding real FATAL record — exactly the RFC 4180 quoting the #4053 part a1 doc comment describes, and exactly why this couldn't have been forged into a second row.Ran:
PgLogEventsCsvlogLiveTestsalone:Total: 1, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0PgLogEventsPipelineTests,PgLogEventsLivePostgresTests,PgPlanCaptureGuardedCastLiveTests,LiveCleanupConversionRatchetTeststogether:Total: 75, Errors: 0, Failed: 0, Skipped: 2, Not Run: 0DARLING_TEST_PGandDARLING_TEST_PG_CSVLOGset:Total: 13397, Errors: 0, Failed: 220, Skipped: 120, Not Run: 1Not verified: the brief names "214 Windows-only failures" as this Mac's known baseline for dev; this run shows 220 failing, and none of them are in
PgLogEventsCsvlogLiveTestsor any other csvlog/#4053 test (checked by grepping the full run log forcsvlogand[FAIL]together — no hits). I did not diff the other ~6 failing test names against a pinned baseline list, because none is named in the brief and I don't have one to diff against; I did confirm this new class is not among them and that adding it changed nothing else. If the coordinator has the exact 214-name baseline, a diff against it would close this out; otherwise treat the ~6-failure delta as pre-existing drift unrelated to this change, not evidence against it.Container
lane4053live-pgremoved after the run.Round-1 review fixes
PgServerLogCsvParser.TryParseLogTime/PgServerLogJsonParser.TryParseTimestampno longer reject a non-zero-offset zone; the shape check decides only "is this a record." The zone judgment moves entirely toPgLogEventsCollector.FilterForeignZoneEntries, which already had the stderr-parity throw/skip logic — it was just never reached. New tests: a csv parser test that a non-UTC record is returned with itsZoneText; a matching json parser test (json wiring is a later lane); two pipeline tests over the csv route'sFakeReader— non-UTClog_timezonethrowsPgLogTimezoneUnsupportedException, UTC skips and counts inforeign_zone_lines_skipped.556c8c683PgServerLogCsvParser.FindBoundaries: when the body ends on a newline, the anchor is unambiguous and unchanged. When it doesn't, up to 8 trailing candidate anchors are each walked backward and scored by how many of their bounded records parse; the highest-scoring candidate wins, latest anchor breaking a tie. A forged, unquoted look-alike line under inverted parity yields at most a couple of planted records, while true parity yields nearly all of them, so the score gap is wide. Cap lowered from 64 tries to 8, since a real mid-write resync is normally found within a handful of embedded newlines; the full walk only ever ran on H1's non-UTC case or a body with no parseable record. New test: a body cut mid-write inside a multi-line quoted field, ten real records ahead of two planted look-alikes — the real records dominate and nofakemessage text is ever emitted.81a8fabc0PgBinaryTailText.SplitAndDecodealready split on0x0Abefore decoding so a lead byte in EUC-JP/EUC-KR/GBK couldn't consume a newline; extended to split on0x22('"') too, re-emitting each delimiter byte literally between decoded segments. Neither byte is ever a trail byte in those encodings, so valid text decodes identically either way; this only matters on the binary route under a DBCSserver_encoding. New[Theory]tests (EUC_JP/EUC_CN/EUC_KR) mirroring the existing before-newline tests, for a lead byte before a quote, on bothDecodeWholeandUnescapeAndDecode.4b72f678cPgLogFormatCapability.Invalidate(targetKey), mirroringPgReadBinaryFileCapability.Invalidateexactly. Called fromDarlingWorker'sPgNoStderrLogFileExceptionandPgNoCsvlogFileExceptioncatch blocks — the two pg_log_events exception handlers this lane's brief scoped me to; I did not touch anything else inDarlingWorker.cs(the web host and the rest ofDarlingWorker's purge belong to issue-4130's lane). NewPgLogFormatCapabilityTests.cs:Invalidatedrops only the named target's cached verdict (a second target's cache survives), and the nextIsCsvlogEnabledAsynccall re-probes and picks up the current setting.e3025ae70RawText, soraw_line_hashchanges with it. Events inside the overlap window when a target switches from stderr to csvlog are stored twice, once under each hash. This is a one-time duplicate on the switch, not a fault — no dedup logic is warranted for it.What I ran
dotnet buildforPerformanceMonitor.Collectors,PerformanceMonitor.Darling.Service, andDarling.Tests(-p:EnableWindowsTargeting=true): all green, 0 warnings, 0 errors.PgServerLogCsvParserTests,PgServerLogJsonParserTests,PgBinaryTailTextTests,PgLogEventsPipelineTests,PgReadBinaryFileCapabilityTests,CollectorMeasurementSeamTests,CollectionLogDrainForensicsStoreTests,PgLogFormatCapabilityTests): 184/184 passed.Darling.Testssuite once,DARLING_TEST_PGunset: 13418 total, 214 failed, 716 skipped, 1 not run. The 214 failures are all in Windows-only/WPF classes that fail on this Mac rig independent of this PR (AgTopologyCardsTests,DarlingInstallLocationTests,DarlingStoreUpgradeTests,ViewerDrillDownTests,TargetProviderTests, live-Postgres classes needingDARLING_TEST_PG, etc.) — none of the touched-area classes (csv/json parser, binary tail text, log events pipeline, either capability cache) appear in the failure list.scrubcheckon every diff before each push: 0 hits each time.Not verified
FakeReader/FakeScalarConnection, consistent with the rest of these test classes. The existing live-fixture tests (Pg14/Pg18 captures) were left untouched.Part of #4053
PR tender verification
Starting head
e3025ae70a18d0208bf4729898a53573ec933db5. The branch merged cleanly withdevat verification time. Nodevmerge was needed.PgServerLogCsvParser.FindBoundaries/ComputeMarks(PerformanceMonitor.Collectors/PgServerLogCsvParser.cs:195-210) walk backward from a newline anchor and track quote parity. A newline inside a quoted field (c == '\n' && !inQuotes, line 206) is never a record boundary. That gives one CSV record per event, never one line. A truncated final record, where the body does not end in\n, is scored across up to 8 trailing candidate anchors (MaxAnchorCandidates, line 130). The text after the winning anchor is never sliced intoboundaries, so it is held back rather than emitted half-parsed.PgServerLogCsvParserTests.TrailingPartialRecordIsNotEmittedpins this. The red-watch below breaks the quote guard and 6 tests fail, so the guard is proven load-bearing, not just documented.greatest(n.size - TailBytesLiteral, 0)as OFFSET andTailBytesLiteral, a compile-time 4 MiB constant, as LENGTH (PerformanceMonitor.Collectors/PgServerLogTail.cs:125-209). There is no row cap anywhere in the read path. The byte bound is what limits memory.TryParseLogTime(PgServerLogCsvParser.cs:335-354) parses only the stamp and no longer consults the zone for UTC-ness, per the H1 review fix already on this branch.PgLogEventsCollector.FilterForeignZoneEntriesdecides that afterward, throughPgDeadlockLogParser.IsZeroOffsetLogZone. That check is an explicit allowlist (UTC, GMT, UCT, plus a numeric-zero-offset shape), never a live timezone-database lookup. An ambiguous or unknown abbreviation such as EDT is not zero-offset. Such a record is either dropped and counted, when the target's ownlog_timezonerenders UTC, or it refuses the whole batch withPgLogTimezoneUnsupportedException. It is never silently reinterpreted as UTC.PgLogEntry.OccurredAtUtcis aDateTime, which holds the millisecond precision csvlog's ownlog_timerenders. csvlog itself carries no finer precision to lose. The store-write path is unchanged shared code, outside this diff.entry.Severity(PgLogFamilyParsers.cs:44,RankOf(entry.Severity) < RankOf("WARNING")). Correction from the PR tender: that holds for jsonlog, not csvlog. csvlog writeserror_severitytranslated (_(error_severity(edata->elevel)), line 162 of PostgreSQL'scsvlog.c). Under a German catalog the gate seesFEHLER, notERROR. Themessage_localefacet from Four parsers read a customer target's log in English, and for pg_deadlocks a locale-blinded read is indistinguishable from a quiet server #3061 already names a translated catalog as an unmet precondition. But the LOG-tier sub-families (connection/disconnection, lock-wait, temp-file, autovacuum, and checkpoint/restartpoint, atPgLogFamilyParsers.cs:93,144,195,393) all gate ons_shape.IsMatch(entry.Message), an English-text regex.PgLogEventClassifier.Classifydrops any entry no parser claims. On a non-Englishlc_messagesserver, those five families' events are absent frompg_log_eventsoutput entirely. They are not misclassified. This is pre-existing inPgLogFamilyParsers.cs, a file this PR does not touch. The csvlog wiring reuses the same classifier the stderr route already used, so it inherits the limitation rather than introducing it. Reporting this per the brief, not fixing it: it is a cross-cutting, design-level concern in shared code, not a 30-minute in-lane fix.CommandTextassignment (PgLogFormatCapability.cs) is a fixed constant,ProbeSql, with no interpolation. Every path, offset and length in the tail CTEs is either a compile-time literal (TailBytesLiteral) or built entirely inside SQL from catalog functions (current_setting('log_directory'),pg_ls_logdir()). None is C#-interpolated from caller- or attacker-influenced data.PgLogUsesCsvlogis false, whether csvlog is off or the cache is stale, the existing stderrQueryText/BinaryQueryTextpath is unchanged. A missing.csvfile throws the newPgNoCsvlogFileException, which carries its own marker and message naming csvlog, never stderr. Both no-file catch arms invalidatePgLogFormatCapability's cache (DarlingWorker.cs:9850and:9876, the L1 review fix), so the next cycle re-probes rather than waiting out the hour. Both fallbacks record aPERMISSIONSstatus withex.Messageexplaining what happened.Build succeeded, 0 Warning(s), 0 Error(s). I ran all 9 classes this PR adds or touches:PgServerLogCsvParserTests,PgServerLogJsonParserTests,PgBinaryTailTextTests,PgLogFormatCapabilityTests,PgReadBinaryFileCapabilityTests,PgLogEventsPipelineTests(only that one class from its 4-class file, confirmed by the diff's single hunk location),CollectorMeasurementSeamTests,CollectionLogDrainForensicsStoreTests, plus the gated live classPgLogEventsCsvlogLiveTests. Result: 184 passed, 1 skipped (no local rig, as instructed), 0 failed. Red-watch: I revertedComputeMarks's quote guard at line 206 fromc == '\n' && !inQuotestoc == '\n', simulating naive line-splitting. 6 tests failed immediately, includingForgedNewlineInQuotedUserNameStaysInOneEntry,MultiLineQuotedMessageIsOneRecordandReadAsync_OverTheCsvRoute_KeepsAForgedNewlineInsideOneUserField. This confirms the suite exercises the record-vs-line invariant. I restored the guard, rebuilt, and the suite went green again: 69/69 across the two classes exercised. No new test was needed.gh pr checks 4124:buildpass,Darling PostgreSQL testspass,Lite tests (result)pass, all threeDarling PG testsshards pass,Darling Linux buildpass,Darling whole-tree guardspass, all fourLite testsshards pass,check-branchespass,description-driftpass. Onlyreview(Claude Auto Review) fails. That is expected and ignored per the brief. No Lite budget test sometimes sees get_alert_history's dataService parameter in a full run #4075 pin flake appeared.4124-reported.txtdid not exist. I wrote it (SECTION: Fixed) and ran the plain-English checker: 1 hit remains, the standard**bold** ([#N]) - textdash every existing entry in the buffer also carries. Left as-is.Fixes made: none. No defect turned up that needed a code change. This PR already carries one full review round's fixes: H1, M1+M2, M3 and L1, commits
556c8c68,81a8fabc,4b72f678ande3025ae7. Every one of them checks out correct under this independent re-verification.Head sha left:
e3025ae70a18d0208bf4729898a53573ec933db5, unchanged, since no commits were made.