Class: ErrorRadar::ErrorLog

Inherits:
ApplicationRecord show all
Defined in:
app/models/error_radar/error_log.rb

Overview

One row per distinct failure (collapsed by fingerprint). Doubles as a task on the triage board. Table: error_radar_error_logs.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.build_fingerprint(category:, error_class:, source:, message:) ⇒ Object



66
67
68
69
70
71
72
73
# File 'app/models/error_radar/error_log.rb', line 66

def self.build_fingerprint(category:, error_class:, source:, message:)
  normalized = message.to_s
                      .gsub(/\d+/, '#')                        # ids, counts, timestamps
                      .gsub(/0x[0-9a-f]+/i, '0x#')             # object addresses
                      .gsub(/[0-9a-f]{8}-[0-9a-f-]{27}/i, '#') # uuids
                      .strip
  Digest::SHA1.hexdigest([category, error_class, source, normalized].join('|'))
end

.presence(value) ⇒ Object



79
80
81
# File 'app/models/error_radar/error_log.rb', line 79

def self.presence(value)
  value.respond_to?(:empty?) ? (value.empty? ? nil : value) : value
end

.record(category:, message:, severity: :error, error_class: nil, source: nil, backtrace: nil, context: {}, http_status: nil, request_url: nil, api_code: nil, api_subcode: nil, fingerprint: nil) ⇒ Object

Record (or roll-up) an error. Idempotent per fingerprint: identical errors increment ‘occurrences` and bump `last_seen_at` instead of creating a new row. NEVER raises — logging must not break the calling code path.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'app/models/error_radar/error_log.rb', line 35

def self.record(category:, message:, severity: :error, error_class: nil, source: nil,
                backtrace: nil, context: {}, http_status: nil, request_url: nil,
                api_code: nil, api_subcode: nil, fingerprint: nil)
  now = Time.current
  fp  = presence(fingerprint) || build_fingerprint(category: category, error_class: error_class, source: source, message: message)

  log = find_or_initialize_by(fingerprint: fp)

  if log.persisted?
    log.occurrences += 1
    log.status = :open if log.status_resolved? || log.status_ignored?
  else
    log.assign_attributes(
      category: category, severity: severity, error_class: error_class, source: source,
      http_status: http_status, request_url: request_url, api_code: api_code, api_subcode: api_subcode,
      first_seen_at: now, status: :open
    )
  end

  log.message      = message.to_s.truncate(ErrorRadar.config.max_message_length)
  log.backtrace    = presence(Array(backtrace).join("\n")) || log.backtrace
  log.context      = (log.context || {}).merge(context.presence || {}).deep_stringify_keys if context.present? || log.context
  log.severity     = severity if log.new_record? || severity_rank(severity) > severity_rank(log.severity)
  log.last_seen_at = now
  log.save!
  log
rescue StandardError => e
  ErrorRadar::Tracking.warn_internal("ErrorLog.record failed: #{e.class}: #{e.message}")
  nil
end

.severity_rank(value) ⇒ Object



75
76
77
# File 'app/models/error_radar/error_log.rb', line 75

def self.severity_rank(value)
  severities[value.to_s] || 0
end

Instance Method Details

#reopen!Object



91
92
93
# File 'app/models/error_radar/error_log.rb', line 91

def reopen!
  update!(status: :open, resolved_at: nil)
end

#resolve!(by: nil, note: nil) ⇒ Object



87
88
89
# File 'app/models/error_radar/error_log.rb', line 87

def resolve!(by: nil, note: nil)
  update!(status: :resolved, resolved_at: Time.current, resolved_by: by, resolution_note: note.presence || resolution_note)
end

#short_messageObject



83
84
85
# File 'app/models/error_radar/error_log.rb', line 83

def short_message
  message.to_s.truncate(120)
end