Class: RailsPulse::Operation

Inherits:
ApplicationRecord show all
Defined in:
app/models/rails_pulse/operation.rb

Constant Summary collapse

OPERATION_TYPES =
%w[
  sql
  controller
  template
  partial
  layout
  collection
  cache_read
  cache_write
  http
  job
  mailer
  storage
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.persist_bulk(ops, context) ⇒ Object

Bulk-insert a batch of operation hashes captured during a request or job run. ‘context` is merged into every row to bind it to its parent — either `{ request_id: id }` or `{ job_run_id: id, request_id: nil }`.

SQL operations are handled in two phases to minimise DB round-trips:

1. Normalise each unique SQL source once and collect hashed→normalised pairs.
2. Resolve (or create) the corresponding Query records in bulk rather than
   one create_or_find_by per operation, which matters for N+1-heavy requests.

insert_all! is used instead of individual creates to bypass ActiveRecord callbacks and validations — normalisation and truncation are done inline above. All rows must have the same key set, so keys are union-normalised before insert.



52
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
# File 'app/models/rails_pulse/operation.rb', line 52

def self.persist_bulk(ops, context)
  return if ops.empty?

  # Phase 1: normalise each unique SQL source once.
  # norm_cache avoids re-normalising repeated SQL (e.g. N+1 queries).
  # norm_map feeds the bulk Query resolver: { hashed_sql => normalized_sql }.
  norm_cache = {}
  norm_map = {}
  ops.each do |op|
    next unless op[:operation_type] == "sql"
    sql_source = op[:actual_sql].presence || op[:label].presence
    next unless sql_source
    unless norm_cache.key?(sql_source)
      normalized = RailsPulse::SqlQueryNormalizer.normalize(sql_source)
      hashed = Digest::MD5.hexdigest(normalized)
      norm_cache[sql_source] = [ hashed, normalized ]
      norm_map[hashed] ||= normalized
    end
  end

  # Phase 2: resolve Query IDs in bulk (1 SELECT in steady state).
  query_id_map = RailsPulse::Query.bulk_find_or_create(norm_map)

  now = Time.current
  rows = ops.map do |op|
    op = op.merge(context).merge(created_at: now, updated_at: now)
    if op[:operation_type] == "sql"
      sql_source = op[:actual_sql].presence || op[:label].presence
      if sql_source && (meta = norm_cache[sql_source])
        hashed, normalized = meta
        op = op.merge(label: normalized.truncate(255), query_id: query_id_map[hashed])
      end
    end
    op[:label] = op[:label]&.truncate(255)
    op
  end
  # insert_all! requires every row to have identical keys.
  # cache_hit defaults to false for non-cache operations so we don't send explicit nil
  # into a column that may carry a NOT NULL constraint from the add_diagnostic_fields migration.
  all_keys = rows.flat_map(&:keys).uniq
  rows = rows.map { |r| all_keys.each_with_object({}) { |k, h| h[k] = r.fetch(k) { k == :cache_hit ? false : nil } } }
  insert_all!(rows)
end

.ransackable_associations(auth_object = nil) ⇒ Object



100
101
102
# File 'app/models/rails_pulse/operation.rb', line 100

def self.ransackable_associations(auth_object = nil)
  %w[]
end

.ransackable_attributes(auth_object = nil) ⇒ Object



96
97
98
# File 'app/models/rails_pulse/operation.rb', line 96

def self.ransackable_attributes(auth_object = nil)
  %w[id occurred_at label duration start_time average_query_time_ms query_count operation_type query_id]
end

Instance Method Details

#to_breadcrumbObject



129
130
131
# File 'app/models/rails_pulse/operation.rb', line 129

def to_breadcrumb
  label.to_s.truncate(60)
end

#to_sObject



133
134
135
# File 'app/models/rails_pulse/operation.rb', line 133

def to_s
  id
end