Module: Dinie::Internal::LogRedaction
- Defined in:
- lib/dinie/runtime/logger.rb,
sig/dinie/runtime/logger.rbs
Overview
Pure, stateless redaction + truncation helpers. module_function: instance methods (so
self-typing + internal calls resolve) plus singleton aliases for the two RuntimeLogger
calls externally (LogRedaction.redact_headers / .format_body).
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: Basicof 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
#inspectredaction set verbatim (the canonical list, story 002). Set.new(Model::REDACTED_ATTRIBUTES.map(&:to_s)).freeze
- NOT_JSON =
Sentinel telling format_body that a String body was not JSON (so it is logged as-is, not re-serialized). Distinct from a real JSON
null, which parses tonil. Object.new
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.generatethat 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).
Instance Method Summary collapse
-
#clip_to_bytes(text, max_bytes) ⇒ String
private
Take whole characters until the next one would exceed
max_bytes. - #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) ⇒ Hash[untyped, untyped] private
-
#safe_generate(value) ⇒ String
private
JSON.generatethat never raises (a log line must not blow up the request). - #serialize_for_log(body) ⇒ String 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 |
Instance Method Details
#clip_to_bytes(text, max_bytes) ⇒ String
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 |
#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.
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) ⇒ Hash[untyped, untyped]
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 |
#safe_generate(value) ⇒ String
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) ⇒ String
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 |