Module: RailsNexus::LoggedExceptionN1

Defined in:
app/models/rails_nexus/logged_exception_n1.rb

Class Method Summary collapse

Class Method Details

.extract_table_name(sql) ⇒ Object



78
79
80
81
82
83
84
85
# File 'app/models/rails_nexus/logged_exception_n1.rb', line 78

def self.extract_table_name(sql)
  return "unknown" if sql.blank?
  if sql =~ /(?:FROM|INTO|UPDATE|JOIN)\s+[`"']?(\w+)[`"']?/i
    $1.downcase
  else
    "unknown"
  end
end

.n_plus_one_patterns(limit: 20) ⇒ Object

N+1 Query Pattern Detection Analyzes breadcrumbs for repeated similar SQL queries



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'app/models/rails_nexus/logged_exception_n1.rb', line 7

def self.n_plus_one_patterns(limit: 20)
  patterns = {}

  LoggedException.where.not(breadcrumbs: nil)
    .where("created_at >= ?", 7.days.ago)
    .order(created_at: :desc)
    .limit(500)
    .find_each do |exception|
      crumbs = parse_breadcrumbs(exception.breadcrumbs)
      next unless crumbs.is_a?(Array)

      sql_crumbs = crumbs.select { |c| c[:type] == "sql" }
      next if sql_crumbs.empty?

      sql_crumbs.group_by { |c| sql_fingerprint(c[:name]) }.each do |fingerprint, queries|
        next if queries.length < 3

        table = extract_table_name(queries.first[:name])
        patterns[fingerprint] ||= {
          fingerprint: fingerprint,
          sql_sample: queries.first[:name]&.truncate(200),
          table: table,
          count: 0,
          exception_ids: [],
          avg_duration: 0
        }
        patterns[fingerprint][:count] += queries.length
        patterns[fingerprint][:exception_ids] << exception.id
        durations = queries.map { |q| q[:duration] }.compact
        if durations.any?
          patterns[fingerprint][:avg_duration] = (durations.sum / durations.size).round(2)
        end
      end
    end

  patterns.values
    .sort_by { |p| -p[:count] }
    .first(limit)
    .map { |p| p.merge(exception_ids: p[:exception_ids].uniq.first(5)) }
end

.n_plus_one_summaryObject



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'app/models/rails_nexus/logged_exception_n1.rb', line 48

def self.n_plus_one_summary
  patterns = n_plus_one_patterns(limit: 100)
  {
    total_patterns: patterns.sum { |p| p[:count] },
    unique_patterns: patterns.size,
    worst_pattern: patterns.first,
    top_tables: patterns.group_by { |p| p[:table] }
      .transform_values { |ps| ps.sum { |p| p[:count] } }
      .sort_by { |_, count| -count }
      .first(5)
      .map { |table, count| { table: table, count: count } }
  }
end

.parse_breadcrumbs(data) ⇒ Object



62
63
64
65
# File 'app/models/rails_nexus/logged_exception_n1.rb', line 62

def self.parse_breadcrumbs(data)
  return data if data.is_a?(Array)
  JSON.parse(data, symbolize_names: true) rescue []
end

.sql_fingerprint(sql) ⇒ Object



67
68
69
70
71
72
73
74
75
76
# File 'app/models/rails_nexus/logged_exception_n1.rb', line 67

def self.sql_fingerprint(sql)
  return "" if sql.blank?
  sql.strip
    .gsub(/\s+/, " ")
    .gsub(/\d+/, "?")
    .gsub(/'[^']*'/, "'?'")
    .gsub(/\b\d+\.\d+\b/, "?")
    .gsub(/\b0x[0-9a-f]+\b/i, "?")
    .strip
end