Class: LogStruct::Formatter

Inherits:
Logger::Formatter
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/log_struct/formatter.rb

Instance Method Summary collapse

Instance Method Details

#call(severity, time, progname, log_value) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/log_struct/formatter.rb', line 179

def call(severity, time, progname, log_value)
  level_enum = Level.from_severity(severity)

  data = log_value_to_hash(log_value, time: time)

  # Filter params, scrub sensitive values, format ActiveJob GlobalID arguments
  data = process_values(data)

  # Add standard fields if not already present
  data[:src] ||= Source::App
  data[:evt] ||= Event::Log
  data[:ts] ||= time.iso8601(3)
  data[:lvl] = level_enum # Set level from severity parameter
  data[:prog] = progname if progname.present?
  request_id = Thread.current[:logstruct_request_id]
  if request_id.is_a?(String) && !request_id.empty? && !data.key?(:req_id)
    data[:req_id] = request_id
  end

  generate_json(data)
end

#clear_tags!Object



37
38
39
# File 'lib/log_struct/formatter.rb', line 37

def clear_tags!
  Thread.current[:activesupport_tagged_logging_tags] = []
end

#current_tagsObject



21
22
23
# File 'lib/log_struct/formatter.rb', line 21

def current_tags
  Thread.current[:activesupport_tagged_logging_tags] ||= []
end

#generate_json(data) ⇒ Object



204
205
206
# File 'lib/log_struct/formatter.rb', line 204

def generate_json(data)
  "#{data.to_json}\n"
end

#log_value_to_hash(log_value, time:) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/log_struct/formatter.rb', line 117

def log_value_to_hash(log_value, time:)
  case log_value
  when Log::Interfaces::CommonFields
    # Our log classes all implement a custom #serialize method that use symbol keys
    log_value.serialize

  when T::Struct
    # Default T::Struct.serialize methods returns a hash with string keys, so convert them to symbols
    log_value.serialize.deep_symbolize_keys

  when Hash
    # Use hash as is and convert string keys to symbols
    log_value.dup.deep_symbolize_keys

  else
    # Create a Plain log with the message as a string and serialize it with symbol keys
    # log_value can be literally anything: Integer, Float, Boolean, NilClass, etc.
    log_message = case log_value
    # Handle all the basic types without any further processing
    when String, Symbol, TrueClass, FalseClass, NilClass, Array, Hash, Time, Numeric
      log_value
    else
      # Handle the serialization of complex objects in a useful way:
      #
      # 1. For ActiveRecord models: Use as_json which includes attributes
      # 2. For objects with custom as_json implementations: Use their implementation
      # 3. For basic objects that only have ActiveSupport's as_json: Use to_s
      begin
        method_owner = log_value.method(:as_json).owner

        # If it's ActiveRecord, ActiveModel, or a custom implementation, use as_json
        if method_owner.to_s.include?("ActiveRecord") ||
            method_owner.to_s.include?("ActiveModel") ||
            method_owner.to_s.exclude?("ActiveSupport::CoreExtensions") &&
                method_owner.to_s.exclude?("Object")
          log_value.as_json
        else
          # For plain objects with only the default ActiveSupport as_json
          log_value.to_s
        end
      rescue => e
        # Handle serialization errors
        context = {
          object_class: log_value.class.name,
          object_inspect: log_value.inspect.truncate(100)
        }
        LogStruct.handle_exception(e, source: Source::Internal, context: context)

        # Fall back to the string representation to ensure we continue processing
        log_value.to_s
      end
    end

    Log::Plain.new(
      message: log_message,
      timestamp: time
    ).serialize
  end
end

#looks_like_backtrace_array?(array) ⇒ Boolean

Returns:

  • (Boolean)


232
233
234
235
236
237
238
# File 'lib/log_struct/formatter.rb', line 232

def looks_like_backtrace_array?(array)
  backtrace_like_count = array.first(5).count do |element|
    element.is_a?(String) && element.match?(/\A[^:\s]+:\d+/)
  end

  backtrace_like_count >= 3
end

#process_array(array, recursion_depth:) ⇒ Object



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/log_struct/formatter.rb', line 209

def process_array(array, recursion_depth:)
  return [] if array.empty?

  if looks_like_backtrace_array?(array)
    array.map { |value| process_values(value, recursion_depth: recursion_depth + 1) }
  else
    processed = []
    array.each_with_index do |value, index|
      break if index >= 10

      processed << process_values(value, recursion_depth: recursion_depth + 1)
    end

    if array.size > 10
      processed << "... and #{array.size - 10} more items"
    end

    processed
  end
end

#process_values(arg, recursion_depth: 0) ⇒ Object



53
54
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/log_struct/formatter.rb', line 53

def process_values(arg, recursion_depth: 0)
  # Prevent infinite recursion in case any args have circular references
  # or are too deeply nested. Just return args.
  return arg if recursion_depth > 20

  case arg
  when Hash
    result = {}

    # Process each key-value pair
    arg.each do |key, value|
      # Check if this key should be filtered at any depth
      result[key] = if ParamFilters.should_filter_key?(key, value)
        # Filter the value
        {_filtered: ParamFilters.summarize_json_attribute(key, value)}
      else
        # Process the value normally
        process_values(value, recursion_depth: recursion_depth + 1)
      end
    end

    result
  when Array
    process_array(arg, recursion_depth: recursion_depth)
  when GlobalID::Identification
    begin
      arg.to_global_id
    rescue
      begin
        case arg
        when ActiveRecord::Base
          "#{arg.class}(##{arg.id})"
        else
          # For non-ActiveRecord objects that failed to_global_id, try to get a string representation
          # If this also fails, we want to catch it and return the error placeholder
          String(T.cast(arg, Object))
        end
      rescue => e
        LogStruct.handle_exception(e, source: Source::Internal)
        "[GLOBALID_ERROR]"
      end
    end
  when Source, Event
    arg.serialize
  when String
    scrub_string(arg)
  when Time
    arg.iso8601(3)
  else
    # Any other type (e.g. Symbol, Integer, Float, Boolean etc.)
    arg
  end
rescue => e
  # Report error through LogStruct's framework
  context = {
    processor_method: "process_values",
    value_type: arg.class.name,
    recursion_depth: recursion_depth
  }
  LogStruct.handle_exception(e, source: Source::Internal, context: context)
  arg
end

#push_tags(*tags) ⇒ Object



42
43
44
# File 'lib/log_struct/formatter.rb', line 42

def push_tags(*tags)
  current_tags.concat(tags)
end

#scrub_string(string) ⇒ Object



47
48
49
50
# File 'lib/log_struct/formatter.rb', line 47

def scrub_string(string)
  # Use StringScrubber module to scrub sensitive information from strings
  StringScrubber.scrub(string)
end

#tagged(*tags, &blk) ⇒ Object



27
28
29
30
31
32
33
# File 'lib/log_struct/formatter.rb', line 27

def tagged(*tags, &blk)
  new_tags = tags.flatten
  current_tags.concat(new_tags) if new_tags.any?
  yield self
ensure
  current_tags.pop(new_tags.size) if new_tags&.any?
end