Repository navigation
Phase 5b: logging facade and redaction — tests and doubles - #70
Merged
Merged
Conversation
Wahbeh-Mohammad
force-pushed
the
19-phase-5b-logging-and-redaction
branch
from
September 18, 2026 08:56
fc93a29 to
e8e2431
Compare
Wahbeh-Mohammad
force-pushed
the
19-phase-5b-logging-and-redaction-tests
branch
from
September 18, 2026 08:56
36952b4 to
a40dbc5
Compare
Wahbeh-Mohammad
force-pushed
the
19-phase-5b-logging-and-redaction
branch
from
September 18, 2026 09:04
e8e2431 to
5771ec7
Compare
Wahbeh-Mohammad
force-pushed
the
19-phase-5b-logging-and-redaction-tests
branch
from
September 18, 2026 09:04
a40dbc5 to
60d3e68
Compare
Wahbeh-Mohammad
force-pushed
the
19-phase-5b-logging-and-redaction
branch
from
September 18, 2026 09:12
5771ec7 to
1e1c7b9
Compare
Wahbeh-Mohammad
force-pushed
the
19-phase-5b-logging-and-redaction-tests
branch
from
September 18, 2026 09:12
60d3e68 to
f5a9941
Compare
Wahbeh-Mohammad
changed the base branch from
19-phase-5b-logging-and-redaction
to
main
September 18, 2026 09:17
Add fourteen suites under test/dexpace/instrumentation/ -- twelve mirrors of the phase-5b files plus the floor-straddling matrix facts as a standing test and the four downstream wirings -- and rewrite 5c's diagnostics mirror as this phase's, asserting the fold, the snapshot bridge across a real thread boundary, and what the 3.2 floor makes of a restored key, floor-aware through FiberStorageFacts. The two steps are driven through the real Pipeline and AsyncPipeline over phase 2's transport fakes and Completer, with 5c's recording tracer factory and meter and 5a's FakeClock, and every configuration a suite consults is built over FakeConfigSource seams so the process environment is never read. Add two top-level doubles under test/support/: RecordingSink, a real in-memory sink with per-severity enablement, and DiagnosticContext, the save-and-restore block for Fiber storage. Every "never throws" claim is asserted on the substituted value; the inert event is measured at zero allocations per chained call; and the four tests the guard pass found missing are here: a sink that re-enters the logger from inside its own write, the async path's header redaction, a recording cursor whose fork raises, and the headers level with a cap supplied. Forty-eight single-edit mutations of lib/ are caught by these suites on 4.0.6 and 3.2.11.
Review round 0's findings, each with the suite that now runs red on its revert. The redactor suite gains the three inputs that leaked through Redactor#header_value -- a network-path reference on the relative route, an authority the parser rejects on the surgery route, and a userinfo on the exploding-policy fallback -- plus the `%zz` parameter name that used to sentinel the whole URL and the opaque URI's query-shaped tail (P5-100, P5-101). The step suite drives four hostile Location values through a real pipeline at HEADERS and asserts the credential in no payload. The event suite gains a HeaderGateTest class: a credential header written straight into Event#field is REDACTED, dropped in omit mode, and the name gate runs before the URL-value redactor (P5-102). Fact 3 of logging_matrix_facts_test.rb was order-dependent on the 3.2 floor: every teardown's `Fiber[OTHER] = nil` is retained there, so `storage == prior` held for any test but the first and the assertion "exact iff = nil deletes" failed on 12 of 13 orders. The expected map is now stated for the row -- `prior` where `= nil` deletes, `prior.merge(OTHER => nil)` on the floor -- which is the same map whether or not the residue was already present. Green on 3.2.11 with seeds 9818, 1 and 42 and standalone; the old spelling reproduces the failure on seed 9818.
Twelve cases for review round 1 of phase 5b. The proxy resolver's warning and its config diagnostic are driven with nine credential-bearing malformed HTTPS_PROXY values -- the no-port, out-of-range, non-numeric, space-in-host, no-host, double-@, scheme-less, network-path and query-carrying spellings -- asserting the placeholder in both channels and the credential in neither, and the not-a-URI message named once without the parser's text (R1-1, P5-103). The two ambient sources meet the reserved-key table: a url.full, an Authorization and a Location in the logger's global context are redacted once at build and the caller's Hash is untouched; the unfiltered and the listed fold redact the same keys; omit mode drops a non-allow-listed header from either source (R1-2, P5-104). A Location with a leading SP, HTAB, VT, NUL or a trailing high byte loses its userinfo through the redactor and through the step at HEADERS over an inbound Headers built with the space, while the four non-authority spellings are written back as given (R1-3, P5-105). charset=utf-7 and iso-2022-jp-2 decode as UTF-8 at Preview.render and the step at BODY still emits the http.response event (R1-4, P5-106). The two mutations that survived round 1 now fail: a settlement failed from a Thread whose own Fiber storage differs emits the failure event under the caller's captured trace.id and span.id and restores the settler's keys (R1-5); a chunked request and response body of declared length -1 put no size field on either event at HEADERS (R1-6). Three suites are split under Metrics/ClassLength: the redactor's surgery-route cases move to SurgeryRouteTest, the step's header cases to HeaderRedactionTest, and WritingTransport moves into the step suite's shared Fixtures.
The surgery route: every prefix review round 2 named before a real authority -- RFC 3986 Appendix C's delimiters, quotes, parentheses, a word, an encoded space, `+`, `@`, a backslash, an NBSP, a line separator, the BINARY obs-text spellings -- and a doubled proxy URL, through Redactor#header_value, through a real pipeline at HEADERS over inbound Headers, and through Proxy.resolve with a quoted, bracketed or doubled HTTPS_PROXY (P5-107). The P5-105 assertion that a non-whitespace prefix is written back verbatim is gone; the not-an-authority list is now what the parser accepts without one and the backslash spellings, and a path that spells a second authority is asserted as OBS-14's path. Per-value redaction: two Locations through the step and an Array through Event#field, each value redacted before the ", " join (P5-108). The async step under a throwing meter at every level: a later settlement raises into the settler while the caller still receives the response, an inline settlement fails the request, and the counter and the span finish are counted at exactly one each way (P5-109). AllocationDelta#allocations_per_call returns the figure two consecutive measurements agree on: on the 3.2.11 floor a one-time cost of 7 or 28 objects can land inside a measured block after the warm-up, once per process and test-order dependent, so a single measurement came back negative in roughly one whole-file run in fifteen and the exact zero assertion failed it (R2-4). A change to 5c's support file; its five allocation suites re-run green and the M02 guard still runs red.
The redactor suite's not-an-authority list gains a path-absolute reference (`/http://user:secret@h/p`, which the parser accepts as a path and the relative route writes back) and a `/ /` that is not a `//` (P5-100, P5-107): both surfaced by re-running review round 2's battery against the repaired redactor, and both are what RFC 3986 decides rather than what the surgery pattern reaches.
Wahbeh-Mohammad
force-pushed
the
19-phase-5b-logging-and-redaction-tests
branch
from
September 18, 2026 09:17
f5a9941 to
5e420e6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #19. Eighth PR of the nine-PR phase-5 stack — the tests for phase 5b's code PR below it. Targets that PR's branch; the phase record is the next PR up.
What lands
18 files, +3,887 / −17: fifteen suites under
gems/dexpace-core/test/dexpace/instrumentation/mirroringlib/one file per file (render.rbandemitter.rbareprivate_constants asserted throughEventand the two steps and say so;diagnostics_test.rbis the full rewrite of 5c's minimal mirror), plus the two doubles the design's testing strategy names and the matrix-fact suite:RecordingSink— a real in-memory_Sinkwith per-severity enablement, recording every entry it is handed exactly as handed, so an assertion that a credential never reached the sink is an assertion about the sink's own record.DiagnosticContext.preserve— the save-and-restore block overFiberstorage that 5c's floor-awareFiberStorageFactshelpers do not cover. Both top-level, the tree's convention (P5-98).Dexpace::RecordingSpan/RecordingTracer/RecordingTracerFactory/RecordingMeter,AllocationDeltaandFiberStorageFacts, and 5a'sFakeClock/FakeConfigSource, consumed unchanged — exceptallocation_delta.rb, which now reports the figure two consecutive measurements agree on (round 2's floor flake, R2-4; 5c's suites re-run green). Phase 2'sFakeTransport/FakeAsyncTransport(settle_later:)and 4c'sForkingProbedrive the steps through real pipelines; noFakeFutureor fake cursor exists.Every file's header names the requirement IDs it exercises; every class inherits
DexpaceTestCase; every thread is joined; nothing usesassert_nothing_raised;GC.statappears only inside the allocation assertions; no 5b test reads realENV— every slot read goes throughFakeConfigSourceseams installed withDexpace.configureand reset in teardown, and the reviewers ran the whole core suite under hostileHTTPS_PROXY/LOG_LEVEL/LOG_PREVIEW_BYTES/MAX_MATERIALIZED_BYTES/MAX_TRACKED_CONTEXTS(every 5b suite green; the four failures are pre-existing 3b/4b ceiling readers, phase 10 inbound bullet 42).What the suites prove rather than restate:
OBS-1as identity and as a number —assert_same(Event::INERT, …)across severities, and a two-loopGC.statdelta of exactly 0.0 per call over the inert chain with the arguments precomputed under# frozen_string_literal: true(R8).{api-version};Tokenmatched case-insensitively and%73igdecoded before matching;?sigwith no=,&&sig=x&,%zz=1(a bad name is unmatchable, the value***), a trailing?preserved, a fragment?not a query, an opaquemailto:with a query tail;/cb?code=S→/cb?***;'',' ',http://[::1, a NUL, 10 KiB of?→ the sentinel, never a raise; the header-value route with userinfo on the relative, surgery and sentinel-fallback routes, leading OWS,<…>, quotes, parentheses,%20,+,@, NBSP, U+2028, obs-text and a doubled URL all losing the credential, a multi-valuedLocationredacted per value; the four RFC 3986 non-authority spellings kept verbatim and named as such;Authorization,Proxy-Authorization,Cookie,Set-Cookie,X-Api-Keyredacted at#fieldand the diagnostic names kept; a credential in the logger's global context and in aFiber[]key redacted atLogger.buildand at#emit.OBS-6/OBS-7— aBasicObject, a raising#inspect, an anonymousStandardErrorsubclass, a 20 KiB multibyte string straddling the 8 KiB boundary — all render, byte-sliced, with a nil-safe class name.OBS-8— 16 threads emitting once; a sink that re-enters the logger from#infonot deadlocking (the mutex is released before the sink call).OBS-10/OBS-24, floor-aware — the fold carries exactly the two default keys and skips a nil-valued one; a snapshot reinstalled in another thread and restored afterwards including on raise; a snapshot-introduced key absent after restore on 3.3+ and present-and-nil on 3.2.11, asserted throughFiberStorageFacts; the async failure event bridged across a settling thread (round 1's surviving mutation, now a case).OBS-20/OBS-30through a realPipeline— a raising sink returning the transport's response with twohttp.instrumentation.logdiagnostics; a raising diagnostic swallowed; a raising tracer inside the same step propagating; on the async path a throwing meter reaching the settler on a later settlement and failing the request on an inline one,addandfinishexactly once in all six level/settlement combinations (round 2's R2-3).OBS-34–OBS-39— the span and both instruments atNONEwith no event; the HEADERS payload with every credential header redacted andurl.fullscrubbed;BODYwithpreview_bytesfrom the configuration (16 configured, 8192 unset), text previewed, octet-stream as the binary marker, a charset with no converter falling back, a chunked body putting no size field on either event (BODY-35, round 1's other surviving mutation), the wire bytes untouched;http.response.duration_msfrom aFakeClockadvanced by hand; everyKeysandEventsconstant present in the manifest.OBS-35— the key required, the tiers in order throughFakeConfigSourceseams, an unrecognised level falling back,DEFAULTisNONE.close_quietly(bad, logger:)emittinghttp.instrumentation.closeand returning nil,onto:still attaching, neither still silent;Hooks.notifywith three raising hooks raising the first with two on the trail and twohttp.instrumentation.hookevents;Proxy.resolve(config, logger:)on a malformedHTTPS_PROXYyielding nil, oneWarningCaptured warning and onehttp.instrumentation.configevent, with the credential in neither channel for the quoted, bracketed and doubled spellings (R3-1's eight parsed-path spellings are the open case);LOG_PREVIEW_BYTESfrozen withLOG_LEVELuntouched.Fiber[:k] = nilretaining on 3.2,Fiber["k"]raising on 3.2/3.3), fact 3 stating each row's expected map (round 0's R0-1: the first cut was order-dependent on the floor).Verification
bundle exec rakeon Ruby 4.0.6 at this tip: exit 0, all seventeen gates green —test:gems2,050 runs / 58,450 assertions, line coverage 99.98%; honest RuboCop 401 files clean.Known follow-ups from the final review (not blocking)
event_test.rb:100: the OBS-8 race test's four-threadQueuegate catches an unsynchronised latch only probabilistically on 3.2.11; a sixteen-thread barrier released on one signal would make it deterministic.FakeClock-advanced caller-callback case atHEADERSandBODYasserting the histogram and the log duration agree and exclude it.diagnostics_test.rbchanges in both this PR and the code PR: 5c's pin repaired to four constants and three functions on the code branch (the suite must be green there), the full 5b mirror here.