Skip to content

Commit 9bfc3dd

Browse files
Phase 5b: logging facade and redaction — tests and doubles (#70)
* test: logging facade and redaction suites (phase 5b) 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. * test: cover the header userinfo routes, the event name gate, fact 3 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. * test: cover round 1's four repairs and the two surviving mutations 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. * test: cover round 2's three repairs and steady the allocation helper 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. * test: name two more parsed-path spellings the surgery never sees 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.
1 parent 5f3c75f commit 9bfc3dd

18 files changed

Lines changed: 3887 additions & 17 deletions

‎gems/dexpace-core/test/dexpace/instrumentation/async_step_test.rb‎

Lines changed: 490 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# frozen_string_literal: true
2+
# SPDX-License-Identifier: MIT
3+
4+
require_relative "../../test_helper"
5+
require_relative "../../support/recording_sink"
6+
require_relative "../../../lib/dexpace/instrumentation/contain"
7+
8+
# Exercises: OBS-20, XCUT-20
9+
#
10+
# The one failure-containment primitive: a module function (P5-37), so a containment cannot
11+
# migrate to the sink the way a `logger.contain { }` would invite. Nothing here is asserted with
12+
# assert_nothing_raised (testing/26b866e1): every totality claim is asserted on the diagnostic
13+
# that was emitted, or on the nil that came back.
14+
class DexpaceInstrumentationContainTest < DexpaceTestCase
15+
Instrumentation = Dexpace::Instrumentation
16+
Logger = Dexpace::Instrumentation::Logger
17+
Events = Dexpace::Instrumentation::Events
18+
Keys = Dexpace::Instrumentation::Keys
19+
20+
test "OBS-20: the block runs, its value is swallowed, and nil comes back" do
21+
sink = RecordingSink.new
22+
ran = false
23+
24+
logger = Logger.build(sink: sink)
25+
result = Instrumentation.contain(logger, event: Events::INSTRUMENTATION_LOG) do
26+
ran = true
27+
:value_a_caller_must_not_branch_on
28+
end
29+
30+
assert(ran)
31+
assert_nil(result)
32+
assert_empty(sink.entries)
33+
end
34+
35+
test "OBS-20: a StandardError from the block becomes a WARNING diagnostic carrying the cause" do
36+
sink = RecordingSink.new
37+
38+
logger = Logger.build(sink: sink)
39+
result = Instrumentation.contain(logger, event: Events::INSTRUMENTATION_LOG) do
40+
raise ::IOError, "network stream severed"
41+
end
42+
43+
assert_nil(result)
44+
assert_equal(1, sink.entries.size)
45+
assert_equal(:warn, sink.entries.first.severity)
46+
payload = sink.payloads.first
47+
48+
assert_equal(Events::INSTRUMENTATION_LOG, payload[Keys::EVENT])
49+
assert_equal("IOError: network stream severed", payload[Keys::CAUSE])
50+
end
51+
52+
test "OBS-20: the diagnostic carries the event name it was given, whichever of the five" do
53+
sink = RecordingSink.new
54+
logger = Logger.build(sink: sink)
55+
[Events::INSTRUMENTATION_CLOSE, Events::INSTRUMENTATION_HOOK, Events::INSTRUMENTATION_CONFIG,
56+
Events::INSTRUMENTATION_SHUTDOWN,].each do |name|
57+
Instrumentation.contain(logger, event: name) { raise "x" }
58+
end
59+
60+
assert_equal([Events::INSTRUMENTATION_CLOSE, Events::INSTRUMENTATION_HOOK,
61+
Events::INSTRUMENTATION_CONFIG, Events::INSTRUMENTATION_SHUTDOWN,],
62+
sink.payloads.map { |payload| payload[Keys::EVENT] },)
63+
sink.payloads.each do |payload|
64+
assert_operator(payload[Keys::EVENT], :start_with?, Events::INSTRUMENTATION_PREFIX)
65+
end
66+
end
67+
68+
# "a secondary failure while emitting that diagnostic MUST be swallowed": a sink that raises on
69+
# every call, the diagnostic included. The assertion is the nil and the absence of a raise
70+
# reaching here -- and it is the case that fails if the inner rescue is written as a bare
71+
# `rescue nil` in a place Ruby parses differently than intended.
72+
test "OBS-20: a secondary failure while emitting the diagnostic is swallowed" do
73+
exploding = ::Object.new
74+
%i[debug? info? warn? error?].each { |name| exploding.define_singleton_method(name) { true } }
75+
%i[debug info warn error].each do |name|
76+
exploding.define_singleton_method(name) { |*| raise ::StandardError, "sink failure" }
77+
end
78+
logger = Logger.build(sink: exploding)
79+
80+
result = Instrumentation.contain(logger, event: Events::INSTRUMENTATION_LOG) do
81+
raise ::ArgumentError, "primary error"
82+
end
83+
84+
assert_nil(result)
85+
end
86+
87+
test "OBS-20: a sink whose predicate itself raises is contained too" do
88+
hostile = ::Object.new
89+
%i[debug info warn error debug? info? warn? error?].each do |name|
90+
hostile.define_singleton_method(name) { |*| raise "hostile #{name}" }
91+
end
92+
logger = Logger.build(sink: hostile)
93+
94+
assert_nil(Instrumentation.contain(logger, event: Events::INSTRUMENTATION_LOG) { raise "x" })
95+
end
96+
97+
test "OBS-20: with Logger::NULL the failure is contained and nothing is produced" do
98+
result = Instrumentation.contain(Logger::NULL, event: Events::INSTRUMENTATION_LOG) { raise "x" }
99+
100+
assert_nil(result)
101+
end
102+
103+
# The rescue is StandardError and never Exception: a SignalException, a NoMemoryError or a
104+
# ScriptError inside a log line is not a logging failure to swallow.
105+
test "OBS-20, XCUT-20: only StandardError is contained; a non-StandardError propagates" do
106+
sink = RecordingSink.new
107+
logger = Logger.build(sink: sink)
108+
109+
assert_raises(::SignalException) do
110+
Instrumentation.contain(logger, event: Events::INSTRUMENTATION_LOG) { raise ::SignalException, "TERM" }
111+
end
112+
assert_raises(::NotImplementedError) do
113+
Instrumentation.contain(logger, event: Events::INSTRUMENTATION_LOG) { raise ::NotImplementedError }
114+
end
115+
assert_empty(sink.entries)
116+
end
117+
118+
test "P5-37: contain is a module function on Instrumentation, not a method on Logger or Event" do
119+
assert_respond_to(Instrumentation, :contain)
120+
refute_respond_to(Logger::NULL, :contain)
121+
refute_respond_to(Dexpace::Instrumentation::Event::INERT, :contain)
122+
assert_raises(Dexpace::InvalidArgumentError) { Instrumentation.contain(Logger::NULL, event: "x") }
123+
end
124+
end

‎gems/dexpace-core/test/dexpace/instrumentation/diagnostics_test.rb‎

Lines changed: 231 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,26 @@
22
# SPDX-License-Identifier: MIT
33

44
require_relative "../../test_helper"
5-
require "dexpace"
5+
require_relative "../../support/fiber_storage_facts"
6+
require_relative "../../support/warning_capture"
7+
require_relative "../../support/diagnostic_context"
8+
require_relative "../../../lib/dexpace/instrumentation/diagnostics"
69

7-
# OBS-10 and OBS-23's two key names and OBS-10's default allow-list, as phase 5c shipped them early
8-
# for phase 5b to adopt (P5-71), and the shape phase 5b left the file in when it extended it: the
9-
# fold, the snapshot bridge and the reserved prefix are added here, and the file still requires
10-
# nothing, so tracing.rb and scope.rb can require it without dragging the logging half in (R11).
11-
# The fold and the bridge are asserted in the phase-5b suite that replaces this file.
10+
# Exercises: OBS-10, OBS-23, OBS-24
11+
#
12+
# Phase 5c shipped this module's three constants early for phase 5b to adopt (P5-71) and pinned
13+
# them here; phase 5b extends the file with .capture, .with and .folded and rewrites this mirror
14+
# as its own -- 5c's "no method" pin is gone, its two constant cases and its Symbol-key case
15+
# stay, and the "requires nothing" pin stays because 5c's independence_test.rb loads this file by
16+
# name and must not acquire the logging half behind Tracing (R11). Every case that touches fiber
17+
# storage runs inside DiagnosticContext.preserve (testing/4ef070df), and the two floor-sensitive
18+
# assertions go through FiberStorageFacts.
19+
#
20+
# Split into nested classes under Metrics/ClassLength: the constants, the bridge, the fold.
1221
class DexpaceInstrumentationDiagnosticsTest < DexpaceTestCase
13-
Diagnostics = Dexpace::Instrumentation::Diagnostics
22+
include FiberStorageFacts
1423

24+
Diagnostics = Dexpace::Instrumentation::Diagnostics
1525
SOURCE = File.expand_path("../../../lib/dexpace/instrumentation/diagnostics.rb", __dir__)
1626

1727
test "OBS-23: the two keys are the wire names as Symbols" do
@@ -26,20 +36,24 @@ class DexpaceInstrumentationDiagnosticsTest < DexpaceTestCase
2636
assert_same(Diagnostics::SPAN_ID, Diagnostics::DEFAULT_KEYS[1])
2737
end
2838

29-
# P5-71, from the adopting side: phase 5b extended the file in place -- a fourth constant and
30-
# the three functions -- and added no require, which is what keeps 5c's load-time assertion
31-
# true: a require of anything else here would pull the logging half in behind Tracing.
32-
test "P5-71: the module defines four constants, three functions, and the file requires nothing" do
39+
# P5-39: the reserved prefix under which core's own fiber slots live (5c's current-span slot
40+
# among them), never folded in either mode; neither key 5b declares falls under it.
41+
test "P5-39: RESERVED_PREFIX is dexpace. and covers neither diagnostic key" do
42+
assert_equal("dexpace.", Diagnostics::RESERVED_PREFIX)
43+
refute(Diagnostics::TRACE_ID.name.start_with?(Diagnostics::RESERVED_PREFIX))
44+
refute(Diagnostics::SPAN_ID.name.start_with?(Diagnostics::RESERVED_PREFIX))
45+
end
46+
47+
# R11: the file 5c's independence subprocess loads by name defines nothing of the logging half
48+
# and requires nothing, so Tracing can require it without acquiring Event, Keys or Logger.
49+
test "R11: the module holds four constants and three functions, and the file requires nothing" do
3350
assert_equal(%i[DEFAULT_KEYS RESERVED_PREFIX SPAN_ID TRACE_ID],
3451
Diagnostics.constants(false).sort,)
3552
assert_equal(%i[capture folded with], Diagnostics.singleton_methods.sort)
3653
assert_empty(Diagnostics.instance_methods(false))
3754
refute_match(/^\s*require/, File.read(SOURCE), "diagnostics.rb must require nothing")
3855
end
3956

40-
# The Symbol decision (R11) rests on a carrier fact the corpus records for 3.4.10 only:
41-
# Fiber[]= interns a String key there and on 4.0.6, but raises TypeError on 3.2.11 and 3.3.12
42-
# (measured 2026-09-17). A Symbol works on every row, and this is the row-by-row proof.
4357
test "R11: a Symbol key is accepted by the per-key carrier API on this interpreter" do
4458
::Fiber[Diagnostics::TRACE_ID] = "t"
4559

@@ -48,4 +62,207 @@ class DexpaceInstrumentationDiagnosticsTest < DexpaceTestCase
4862
ensure
4963
::Fiber[Diagnostics::TRACE_ID] = nil
5064
end
65+
66+
# OBS-24's snapshot and bridge.
67+
class BridgeTest < DexpaceTestCase
68+
include FiberStorageFacts
69+
70+
Diagnostics = Dexpace::Instrumentation::Diagnostics
71+
72+
# Asserted per key, not as whole-map equality: `rake test` shares one process and one main
73+
# fiber, so a whole-map assertion is one about every other suite's storage hygiene.
74+
test "OBS-24: capture is a frozen Hash of the present keys, a fresh object per call" do
75+
DiagnosticContext.preserve do
76+
::Fiber[:"trace.id"] = "t1"
77+
snapshot = Diagnostics.capture
78+
79+
assert_equal("t1", snapshot[:"trace.id"])
80+
assert_predicate(snapshot, :frozen?)
81+
refute_same(snapshot, Diagnostics.capture)
82+
refute_predicate(::Fiber.current.storage, :frozen?, "the live storage is untouched")
83+
assert(snapshot.keys.all?(::Symbol))
84+
end
85+
end
86+
87+
# The floor decision: a nil-valued key is not a diagnostic-context value (OBS-10 skips it),
88+
# so it is absent from a snapshot on every row -- which makes the snapshot the same shape on
89+
# 3.2, where `= nil` leaves nil-valued keys behind, as on 3.3+, where only the warned setter
90+
# can create one. The input is built the only way it can be on 3.3+.
91+
test "OBS-24, OBS-10: capture omits a nil-valued key on every row" do
92+
DiagnosticContext.preserve do
93+
::Fiber[:"dexpace.test.kept"] = "v"
94+
before = ::Fiber.current.storage
95+
WarningCapture.record { ::Fiber.current.storage = before.merge("dexpace.test.nil": nil) }
96+
97+
assert(::Fiber.current.storage.key?(:"dexpace.test.nil"))
98+
snapshot = Diagnostics.capture
99+
100+
refute(snapshot.key?(:"dexpace.test.nil"))
101+
assert_equal("v", snapshot[:"dexpace.test.kept"])
102+
ensure
103+
WarningCapture.record { ::Fiber.current.storage = before }
104+
end
105+
end
106+
107+
test "OBS-24: capture reads {} inside a fiber that opted out of storage" do
108+
captured = ::Fiber.new(storage: nil) { Diagnostics.capture }.resume
109+
110+
assert_equal({}, captured)
111+
assert_predicate(captured, :frozen?)
112+
end
113+
114+
test "OBS-24: with installs the snapshot for the block and restores the prior context after" do
115+
DiagnosticContext.preserve do
116+
::Fiber[:prior_key] = "prior"
117+
::Fiber[:"trace.id"] = "before"
118+
snapshot = { "trace.id": "t2", introduced: "new" }.freeze
119+
120+
value = Diagnostics.with(snapshot) do
121+
assert_equal("t2", ::Fiber[:"trace.id"])
122+
assert_equal("new", ::Fiber[:introduced])
123+
assert_equal("prior", ::Fiber[:prior_key], "keys the snapshot does not name are kept")
124+
:block_value
125+
end
126+
127+
assert_equal(:block_value, value)
128+
assert_equal("before", ::Fiber[:"trace.id"], "an overwritten key is put back")
129+
assert_equal("prior", ::Fiber[:prior_key])
130+
assert_nil(::Fiber[:introduced])
131+
assert_diagnostic_key_removed(:introduced)
132+
end
133+
end
134+
135+
test "OBS-24: with restores the prior context when the block raises" do
136+
DiagnosticContext.preserve do
137+
::Fiber[:"trace.id"] = "before"
138+
snapshot = { "trace.id": "t2", introduced: "new" }
139+
140+
error = assert_raises(::RuntimeError) do
141+
Diagnostics.with(snapshot) do
142+
assert_equal("t2", ::Fiber[:"trace.id"])
143+
raise "inside the bridge"
144+
end
145+
end
146+
147+
assert_equal("inside the bridge", error.message)
148+
assert_equal("before", ::Fiber[:"trace.id"])
149+
assert_nil(::Fiber[:introduced])
150+
assert_diagnostic_key_removed(:introduced)
151+
end
152+
end
153+
154+
# A new Thread INHERITS a copy of the parent's storage, so B's "original context" is not
155+
# empty; the assertion is that B's own prior comes back, never that it reads nil.
156+
test "OBS-24: with bridges a real thread boundary and restores B's own prior context" do
157+
DiagnosticContext.preserve do
158+
::Fiber[:"trace.id"] = "parent_trace"
159+
snapshot = Diagnostics.capture.merge("span.id": "s1").freeze
160+
161+
result = ::Thread.new do
162+
::Fiber[:thread_local] = "orig"
163+
::Fiber[:"trace.id"] = "b_own_trace"
164+
inside = nil
165+
Diagnostics.with(snapshot) { inside = [::Fiber[:"trace.id"], ::Fiber[:"span.id"]] }
166+
[inside, ::Fiber[:"trace.id"], ::Fiber[:thread_local], ::Fiber[:"span.id"]]
167+
end.value
168+
169+
assert_equal(%w[parent_trace s1], result[0], "the captured keys are visible inside")
170+
assert_equal("b_own_trace", result[1], "B's own value is back")
171+
assert_equal("orig", result[2])
172+
assert_nil(result[3])
173+
assert_equal("parent_trace", ::Fiber[:"trace.id"], "mutations stay on the calling thread")
174+
assert_nil(::Fiber[:"span.id"])
175+
end
176+
end
177+
178+
test "OBS-24: the snapshot is any object answering #each and #keys with Symbol keys" do
179+
DiagnosticContext.preserve do
180+
duck = ::Struct.new(:pairs) do
181+
def each(&) = pairs.each(&)
182+
def keys = pairs.keys
183+
def [](key) = pairs[key]
184+
end.new({ "trace.id": "duck" })
185+
186+
Diagnostics.with(duck) { assert_equal("duck", ::Fiber[:"trace.id"]) }
187+
188+
assert_nil(::Fiber[:"trace.id"])
189+
end
190+
end
191+
end
192+
193+
# OBS-10's fold, in both modes.
194+
class FoldTest < DexpaceTestCase
195+
Diagnostics = Dexpace::Instrumentation::Diagnostics
196+
197+
test "OBS-10: the allow-listed mode folds only present listed keys, keyed by Symbol#name" do
198+
DiagnosticContext.preserve do
199+
::Fiber[:"trace.id"] = "1111"
200+
::Fiber[:other_key] = "other"
201+
202+
folded = Diagnostics.folded(Diagnostics::DEFAULT_KEYS)
203+
204+
assert_equal({ "trace.id" => "1111" }, folded)
205+
assert_same(Diagnostics::TRACE_ID.name, folded.keys.first)
206+
refute_predicate(folded, :frozen?, "the fold is the event's to merge into")
207+
end
208+
end
209+
210+
test "OBS-10: the allow-listed mode skips a nil-valued listed key" do
211+
DiagnosticContext.preserve do
212+
::Fiber[:"span.id"] = "s"
213+
::Fiber[:"span.id"] = nil
214+
215+
assert_equal({}, Diagnostics.folded(Diagnostics::DEFAULT_KEYS))
216+
end
217+
end
218+
219+
test "OBS-10: an empty allow-list folds nothing" do
220+
DiagnosticContext.preserve do
221+
::Fiber[:"trace.id"] = "1111"
222+
223+
assert_equal({}, Diagnostics.folded([]))
224+
end
225+
end
226+
227+
# R12: the nil-valued key is the input OBS-10's "keys with null values MUST be skipped" is
228+
# about, and it is constructed the only way it can be on 3.3+ -- the warned whole-map
229+
# setter, inside WarningCapture so FatalWarnings does not fail the test. The ban is on lib/,
230+
# not on a test deliberately producing a host-made state.
231+
test "OBS-10: the unfiltered mode folds every present key and skips nil values" do
232+
DiagnosticContext.preserve do
233+
before = ::Fiber.current.storage
234+
warnings = WarningCapture.record do
235+
::Fiber.current.storage = before.merge(a: nil, b: "beta")
236+
end
237+
238+
assert_equal(1, warnings.size)
239+
folded = Diagnostics.folded(nil)
240+
241+
assert_equal("beta", folded["b"])
242+
refute(folded.key?("a"))
243+
assert(folded.keys.all?(::String))
244+
ensure
245+
WarningCapture.record { ::Fiber.current.storage = before }
246+
end
247+
end
248+
249+
# P5-39: the crossing neither design named. 5c stores the current span under
250+
# :"dexpace.current_span" and says it "must never be folded"; the unfiltered mode folds the
251+
# WHOLE map, so without the skip a live Span reaches OBS-6's rendering on every event. The
252+
# key is written literally: 5c's constant is a private_constant, which a qualified reference
253+
# cannot reach, and the literal is what the skip actually matches.
254+
test "OBS-10, P5-39: the unfiltered mode skips every dexpace.-prefixed slot" do
255+
DiagnosticContext.preserve do
256+
::Fiber[:"dexpace.current_span"] = ::Object.new
257+
::Fiber[Diagnostics::TRACE_ID] = "t1"
258+
::Fiber[:tenant] = "acme"
259+
260+
folded = Diagnostics.folded(nil)
261+
262+
assert_equal("t1", folded["trace.id"])
263+
assert_equal("acme", folded["tenant"])
264+
refute(folded.keys.any? { |key| key.start_with?("dexpace.") })
265+
end
266+
end
267+
end
51268
end

0 commit comments

Comments
 (0)