Module: Dinie::Internal::LogRedaction
- Defined in:
- lib/dinie/runtime/logger.rb
Overview
Pure, stateless redaction + truncation helpers shared by RuntimeLogger. A financial backend carries PII and credentials in headers and bodies, so every value the logger emits passes through here first (architecture §9, RB12). No I/O, no state — trivially testable.
The body-field redaction list is **derived from** Model::REDACTED_ATTRIBUTES (story 002) so the logger and the model ‘#inspect` can never drift apart: one source of truth for “what counts as PII” (the story’s stated risk — divergence here is a leak).
Constant Summary collapse
- REDACTED =
Mask substituted for any redacted header value or body field.
"[REDACTED]"- MAX_BODY_BYTES =
Bodies whose serialized size reaches this many bytes are truncated (architecture §9).
2048- REDACTED_HEADERS =
Header names (lowercased) whose values carry credentials/signatures → masked. Covers the ‘Authorization: Basic` of the token POST (client secret), the webhook HMAC, the per-call client-secret header, and any forward-proxy auth.
Set.new( %w[authorization webhook-signature x-dinie-client-secret proxy-authorization] ).freeze
- REDACTED_BODY_FIELDS =
Body field names (lowercased) carrying PII/secrets → masked recursively. Mirrors the model’s ‘#inspect` redaction set verbatim (the canonical list, story 002).
Set.new(Model::REDACTED_ATTRIBUTES.map(&:to_s)).freeze
Class Method Summary collapse
-
.clip_to_bytes(text, max_bytes) ⇒ Object
private
Take whole characters until the next one would exceed ‘max_bytes`.
-
.format_body(body) ⇒ String?
Render a body for logging: redact PII, serialize to JSON, then truncate.
- .parse_json(text) ⇒ Object private
-
.redact_body(value) ⇒ Object
Deep-copy ‘value`, masking any Hash key (case-insensitive) that names a PII/secret field.
- .redact_hash(hash) ⇒ Object private
-
.redact_headers(headers) ⇒ Hash{String => Object}
Replace the value of any sensitive header with the mask (case-insensitive).
-
.safe_generate(value) ⇒ Object
private
‘JSON.generate` that never raises (a log line must not blow up the request).
- .serialize_for_log(body) ⇒ Object private
-
.truncate_body(text) ⇒ String
Truncate to MAX_BODY_BYTES on a codepoint boundary, appending ‘…[truncated, full_size=NNN]` (NNN = full UTF-8 byte size).
Class Method Details
.clip_to_bytes(text, max_bytes) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Take whole characters until the next one would exceed ‘max_bytes`.
109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/dinie/runtime/logger.rb', line 109 def clip_to_bytes(text, max_bytes) bytes = 0 clipped = +"" text.each_char do |char| char_bytes = char.bytesize break if bytes + char_bytes > max_bytes bytes += char_bytes clipped << char end clipped end |
.format_body(body) ⇒ String?
Render a body for logging: redact PII, serialize to JSON, then truncate. A String body is JSON-parsed first (so its fields are redacted) and logged verbatim when it is not JSON. ‘nil`/empty bodies return `nil` (the caller drops the key).
80 81 82 83 84 |
# File 'lib/dinie/runtime/logger.rb', line 80 def format_body(body) return nil if body.nil? || (body.is_a?(String) && body.empty?) truncate_body(serialize_for_log(body)) end |
.parse_json(text) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
123 124 125 126 127 |
# File 'lib/dinie/runtime/logger.rb', line 123 def parse_json(text) JSON.parse(text) rescue JSON::ParserError NOT_JSON end |
.redact_body(value) ⇒ Object
Deep-copy ‘value`, masking any Hash key (case-insensitive) that names a PII/secret field. Arrays and nested objects are walked; scalars pass through untouched.
59 60 61 62 63 64 65 |
# File 'lib/dinie/runtime/logger.rb', line 59 def redact_body(value) case value when Array then value.map { |item| redact_body(item) } when Hash then redact_hash(value) else value end end |
.redact_hash(hash) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
68 69 70 71 72 |
# File 'lib/dinie/runtime/logger.rb', line 68 def redact_hash(hash) hash.each_with_object({}) do |(key, value), out| out[key] = REDACTED_BODY_FIELDS.include?(key.to_s.downcase) ? REDACTED : redact_body(value) end end |
.redact_headers(headers) ⇒ Hash{String => Object}
Replace the value of any sensitive header with the mask (case-insensitive). Accepts a Hash or ‘Faraday::Utils::Headers`; returns a plain Hash with the original key casing preserved.
46 47 48 49 50 51 52 |
# File 'lib/dinie/runtime/logger.rb', line 46 def redact_headers(headers) return {} if headers.nil? headers.each_with_object({}) do |(key, value), out| out[key.to_s] = REDACTED_HEADERS.include?(key.to_s.downcase) ? REDACTED : value end end |
.safe_generate(value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
‘JSON.generate` that never raises (a log line must not blow up the request).
131 132 133 134 135 |
# File 'lib/dinie/runtime/logger.rb', line 131 def safe_generate(value) JSON.generate(value) rescue StandardError "[unserializable]" end |
.serialize_for_log(body) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
87 88 89 90 91 92 |
# File 'lib/dinie/runtime/logger.rb', line 87 def serialize_for_log(body) return safe_generate(redact_body(body)) unless body.is_a?(String) parsed = parse_json(body) parsed.equal?(NOT_JSON) ? body : safe_generate(redact_body(parsed)) end |
.truncate_body(text) ⇒ String
Truncate to MAX_BODY_BYTES on a codepoint boundary, appending ‘…[truncated, full_size=NNN]` (NNN = full UTF-8 byte size). Returns the input unchanged when under the threshold (so a binary/multipart blob never floods the log — story risk note).
100 101 102 103 104 105 |
# File 'lib/dinie/runtime/logger.rb', line 100 def truncate_body(text) full_size = text.bytesize return text if full_size < MAX_BODY_BYTES "#{clip_to_bytes(text, MAX_BODY_BYTES)}…[truncated, full_size=#{full_size}]" end |