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) ⇒ String

Serializes Log (or string) into JSON

Parameters:

  • severity (String, Symbol, Integer)
  • time (Time)
  • progname (String, nil)
  • log_value (T.untyped)

Returns:

  • (String)


179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# 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?

  generate_json(data)
end

#clear_tags!void

This method returns an undefined value.

Add clear_tags! method to support ActiveSupport::TaggedLogging



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

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

#current_tagsArray<String>

Add current_tags method to support ActiveSupport::TaggedLogging

Returns:

  • (Array<String>)


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

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

#generate_json(data) ⇒ String

Output as JSON with a newline. We mock this method in tests so we can inspect the data right before it gets turned into a JSON string.

Parameters:

  • data (Hash{T.untyped => T.untyped})

Returns:

  • (String)


200
201
202
# File 'lib/log_struct/formatter.rb', line 200

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

#log_value_to_hash(log_value, time:) ⇒ Hash{Symbol => T.untyped}

Parameters:

  • log_value (T.untyped)
  • time (Time)

Returns:

  • (Hash{Symbol => T.untyped})


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

Check if an array looks like a backtrace (array of strings with file:line pattern)

Parameters:

  • array (Array<T.untyped>)

Returns:

  • (Boolean)


228
229
230
231
232
233
234
# File 'lib/log_struct/formatter.rb', line 228

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:) ⇒ Array<T.untyped>

Parameters:

  • array (Array<T.untyped>)
  • recursion_depth (Integer)

Returns:

  • (Array<T.untyped>)


205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/log_struct/formatter.rb', line 205

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) ⇒ T.untyped

Parameters:

  • arg (T.untyped)
  • recursion_depth (Integer) (defaults to: 0)

Returns:

  • (T.untyped)


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) ⇒ T.untyped

Parameters:

  • tags (Array<String>)

Returns:

  • (T.untyped)


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

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

#scrub_string(string) ⇒ String

Parameters:

  • string (String)

Returns:

  • (String)


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) ⇒ T.untyped

Add tagged method to support ActiveSupport::TaggedLogging

Parameters:

  • tags (Array<String>)
  • blk (T.proc.params(formatter: Formatter).void)

Returns:

  • (T.untyped)


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