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.

Returns:

  • (String)
"[REDACTED]"
MAX_BODY_BYTES =

Bodies whose serialized size reaches this many bytes are truncated (architecture §9).

Returns:

  • (Integer)
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.

Returns:

  • (Set[String])
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).

Returns:

  • (Set[String])
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 to nil.

Returns:

  • (Object)
Object.new

Class Method Summary collapse

Instance Method Summary collapse

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).

Parameters:

  • body (Object, nil) —

    the raw request/response body

Returns:

  • (String, nil)


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.

Parameters:

  • value (Object) —

    a parsed body (Hash/Array/scalar)

Returns:

  • (Object) —

    the redacted copy



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.

Parameters:

  • headers (#each, nil) —

    request/response headers

Returns:

  • (Hash{String => Object})


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).

Parameters:

  • text (String)

Returns:

  • (String)


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.

Parameters:

  • text (String)
  • max_bytes (Integer)

Returns:

  • (String)


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.

Parameters:

  • text (String)

Returns:

  • (Object)


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.

Parameters:

  • value (Object)

Returns:

  • (Object)


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.

Parameters:

  • hash (Hash[untyped, untyped])

Returns:

  • (Hash[untyped, untyped])


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).

Parameters:

  • value (Object)

Returns:

  • (String)


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.

Parameters:

  • body (Object)

Returns:

  • (String)


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).

Parameters:

  • text (String)

Returns:

  • (String)


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