Module: JsonLogging::Sanitizer

Defined in:
lib/json_logging/sanitizer.rb

Constant Summary collapse

CONTROL_CHARS =

Control characters that should be escaped or removed from log messages

/[\x00-\x1F\x7F]/
MAX_STRING_LENGTH =

Maximum string length before truncation

10_000
MAX_CONTEXT_SIZE =

Maximum context hash size (number of keys)

50
MAX_DEPTH =

Maximum depth for nested structures

10
MAX_BACKTRACE_LINES =

Maximum backtrace lines to include

20
SENSITIVE_KEY_PATTERNS =

Common sensitive key patterns (case insensitive) - fallback when Rails ParameterFilter not available

/\b(password|passwd|pwd|secret|token|api_key|apikey|access_token|auth_token|private_key|credential)\b/i

Class Method Summary collapse

Class Method Details

.rails_parameter_filterObject

Get Rails ParameterFilter if available, nil otherwise



24
25
26
27
28
29
30
31
32
33
34
# File 'lib/json_logging/sanitizer.rb', line 24

def rails_parameter_filter
  return nil unless defined?(Rails) && Rails.respond_to?(:application)
  return nil unless Rails.application.respond_to?(:config)

  filter_params = Rails.application.config.filter_parameters
  return nil if filter_params.empty?

  ActiveSupport::ParameterFilter.new(filter_params)
rescue
  nil
end

.sanitize_backtrace(backtrace) ⇒ Object

Sanitize backtrace - truncate and remove sensitive paths



142
143
144
145
146
147
148
149
150
151
# File 'lib/json_logging/sanitizer.rb', line 142

def sanitize_backtrace(backtrace)
  return [] unless backtrace.is_a?(Array)

  # Take first MAX_BACKTRACE_LINES, sanitize each
  backtrace.first(MAX_BACKTRACE_LINES).map do |line|
    sanitize_string(line.to_s)
  end
rescue
  []
end

.sanitize_exception(ex) ⇒ Object

Sanitize exception, including backtrace



129
130
131
132
133
134
135
136
137
138
139
# File 'lib/json_logging/sanitizer.rb', line 129

def sanitize_exception(ex)
  {
    "error" => {
      "class" => ex.class.name,
      "message" => sanitize_string(ex.message.to_s),
      "backtrace" => sanitize_backtrace(ex.backtrace)
    }
  }
rescue
  {"error" => {"class" => "Exception", "message" => "<sanitization_failed>"}}
end

.sanitize_hash(hash, depth: 0) ⇒ Object

Sanitize a hash, removing sensitive keys and limiting size/depth Uses Rails ParameterFilter when available, falls back to pattern matching



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/json_logging/sanitizer.rb', line 55

def sanitize_hash(hash, depth: 0)
  return hash unless hash.is_a?(Hash)

  # Prevent excessive nesting
  return {"error" => "max_depth_exceeded"} if depth > MAX_DEPTH

  # Limit hash size first
  limited_hash = if hash.size > MAX_CONTEXT_SIZE
    truncated = hash.first(MAX_CONTEXT_SIZE).to_h
    truncated["_truncated"] = true
    truncated
  else
    hash
  end

  # Use Rails ParameterFilter if available (handles encrypted attributes automatically)
  filter = rails_parameter_filter
  if filter
    # ParameterFilter will filter based on Rails.config.filter_parameters
    # This includes encrypted attributes automatically
    # Create a deep copy since filter modifies in place (Rails 6+)
    filtered = limited_hash.respond_to?(:deep_dup) ? limited_hash.deep_dup : limited_hash.dup
    filtered = filter.filter(filtered)

    # Then sanitize values (strings, control chars, etc.) preserving filtered structure
    filtered.each_with_object({}) do |(key, value), result|
      result[key] = sanitize_value(value, depth: depth + 1)
    end

  else
    # Fallback: use pattern matching for sensitive keys
    limited_hash.each_with_object({}) do |(key, value), result|
      key_str = key.to_s

      # Skip sensitive keys
      if SENSITIVE_KEY_PATTERNS.match?(key_str)
        result[key_str.gsub(/(?<!^)(?=[A-Z])/, "_").downcase + "_filtered"] = "[FILTERED]"
        next
      end

      result[key] = sanitize_value(value, depth: depth + 1)
    end
  end
rescue
  {"sanitization_error" => true}
end

.sanitize_string(str) ⇒ Object

Sanitize a string by removing/escaping control characters and truncating



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/json_logging/sanitizer.rb', line 37

def sanitize_string(str)
  return str unless str.is_a?(String)

  # Remove or replace control characters
  sanitized = str.gsub(CONTROL_CHARS, "")

  # Truncate if too long
  if sanitized.length > MAX_STRING_LENGTH
    sanitized = sanitized[0, MAX_STRING_LENGTH] + "...[truncated]"
  end

  sanitized
rescue
  "<sanitization_error>"
end

.sanitize_value(value, depth: 0) ⇒ Object

Sanitize a value (handles strings, hashes, arrays, etc.) Preserves numeric, boolean, and nil types



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/json_logging/sanitizer.rb', line 104

def sanitize_value(value, depth: 0)
  case value
  when String
    sanitize_string(value)
  when Hash
    sanitize_hash(value, depth: depth)
  when Array
    # Limit array size
    sanitized = value.first(MAX_CONTEXT_SIZE).map { |v| sanitize_value(v, depth: depth + 1) }
    sanitized << "[truncated]" if value.size > MAX_CONTEXT_SIZE
    sanitized
  when Exception
    sanitize_exception(value)
  when Numeric, TrueClass, FalseClass, NilClass
    # Preserve numeric, boolean, and nil types
    value
  else
    # For other types, convert to string and sanitize
    sanitize_string(value.to_s)
  end
rescue
  "<unprintable>"
end

.sensitive_key?(key) ⇒ Boolean

Check if a key looks sensitive

Returns:

  • (Boolean)


154
155
156
# File 'lib/json_logging/sanitizer.rb', line 154

def sensitive_key?(key)
  SENSITIVE_KEY_PATTERNS.match?(key.to_s)
end