Class: RailsPulse::Subscribers::OperationSubscriber

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_pulse/subscribers/operation_subscriber.rb

Class Method Summary collapse

Class Method Details

.capture_operation(event_name, start, finish, payload, operation_type, label_key = nil) ⇒ Object

Helper method to capture operation data



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
# File 'lib/rails_pulse/subscribers/operation_subscriber.rb', line 57

def self.capture_operation(event_name, start, finish, payload, operation_type, label_key = nil)
  return unless RailsPulse.configuration.enabled
  return if RequestStore.store[:skip_recording_rails_pulse_activity]

  request_id = RequestStore.store[:rails_pulse_request_id]
  return unless request_id

  # Skip RailsPulse-related operations to prevent recursion
  if operation_type == "sql"
    sql = payload[:sql]
    return if sql&.include?("rails_pulse_")
  end

  label = case label_key
  when :sql then clean_sql_label(payload[:sql])
  when :template then relative_path(payload[:identifier] || payload[:template])
  when :partial then relative_path(payload[:identifier] || payload[:partial])
  when :controller then "#{payload[:controller]}##{payload[:action]}"
  when :cache then payload[:key]
  else payload[label_key] || event_name
  end

  codebase_location =
    if payload[:identifier]
      relative_path(payload[:identifier])
    elsif payload[:template]
      relative_path(payload[:template])
    elsif operation_type == "controller"
      controller_action_source_location(payload) || find_app_frame || caller_locations(3, 1).first&.path
    elsif operation_type == "sql"
      relative_path(find_app_frame || caller_locations(3, 1).first&.path)
    else
      find_app_frame || caller_locations(3, 1).first&.path
    end

  operation_data = {
    request_id: request_id,
    operation_type: operation_type,
    label: label,
    duration: (finish - start) * 1000,
    codebase_location: codebase_location,
    start_time: start.to_f,
    occurred_at: Time.zone.at(start)
  }

  RequestStore.store[:rails_pulse_operations] ||= []
  RequestStore.store[:rails_pulse_operations] << operation_data
end

.clean_sql_label(sql) ⇒ Object

Helper method to clean SQL labels by removing Rails comments



6
7
8
9
10
# File 'lib/rails_pulse/subscribers/operation_subscriber.rb', line 6

def self.clean_sql_label(sql)
  return sql unless sql
  # Remove Rails SQL comments like /*action='search',application='Dummy',controller='home'*/
  sql.gsub(/\/\*[^*]*\*\//, "").strip
end

.controller_action_source_location(payload) ⇒ Object

Helper method to resolve controller action source location



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/rails_pulse/subscribers/operation_subscriber.rb', line 35

def self.controller_action_source_location(payload)
  return nil unless payload[:controller] && payload[:action]
  begin
    controller_klass = payload[:controller].constantize
    if controller_klass.instance_methods(false).include?(payload[:action].to_sym)
      file, line = controller_klass.instance_method(payload[:action]).source_location
      return "#{relative_path(file)}:#{line}" if file && line
    end
    # fallback: try superclass (for ApplicationController actions)
    if controller_klass.superclass.respond_to?(:instance_method)
      if controller_klass.superclass.instance_methods(false).include?(payload[:action].to_sym)
        file, line = controller_klass.superclass.instance_method(payload[:action]).source_location
        return "#{relative_path(file)}:#{line}" if file && line
      end
    end
  rescue => e
    Rails.logger.debug "[RailsPulse] Could not resolve controller source location: #{e.class} - #{e.message}"
  end
  nil
end

.find_app_frameObject

Helper method to find the first app frame in the call stack



25
26
27
28
29
30
31
32
# File 'lib/rails_pulse/subscribers/operation_subscriber.rb', line 25

def self.find_app_frame
  app_path = Rails.root.join("app").to_s
  caller_locations.each do |loc|
    path = loc.absolute_path || loc.path
    return path if path && path.start_with?(app_path)
  end
  nil
end

.relative_path(absolute_path) ⇒ Object

Helper method to convert absolute paths to relative paths



13
14
15
16
17
18
19
20
21
22
# File 'lib/rails_pulse/subscribers/operation_subscriber.rb', line 13

def self.relative_path(absolute_path)
  return absolute_path unless absolute_path&.start_with?("/")

  rails_root = Rails.root.to_s
  if absolute_path.start_with?(rails_root)
    absolute_path.sub(rails_root + "/", "")
  else
    absolute_path
  end
end

.subscribe!Object



4
5
6
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
47
48
49
50
51
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/rails_pulse/subscribers/operation_subscriber.rb', line 4

def self.subscribe!
  # Helper method to clean SQL labels by removing Rails comments
  def self.clean_sql_label(sql)
    return sql unless sql
    # Remove Rails SQL comments like /*action='search',application='Dummy',controller='home'*/
    sql.gsub(/\/\*[^*]*\*\//, "").strip
  end

  # Helper method to convert absolute paths to relative paths
  def self.relative_path(absolute_path)
    return absolute_path unless absolute_path&.start_with?("/")

    rails_root = Rails.root.to_s
    if absolute_path.start_with?(rails_root)
      absolute_path.sub(rails_root + "/", "")
    else
      absolute_path
    end
  end

  # Helper method to find the first app frame in the call stack
  def self.find_app_frame
    app_path = Rails.root.join("app").to_s
    caller_locations.each do |loc|
      path = loc.absolute_path || loc.path
      return path if path && path.start_with?(app_path)
    end
    nil
  end

  # Helper method to resolve controller action source location
  def self.controller_action_source_location(payload)
    return nil unless payload[:controller] && payload[:action]
    begin
      controller_klass = payload[:controller].constantize
      if controller_klass.instance_methods(false).include?(payload[:action].to_sym)
        file, line = controller_klass.instance_method(payload[:action]).source_location
        return "#{relative_path(file)}:#{line}" if file && line
      end
      # fallback: try superclass (for ApplicationController actions)
      if controller_klass.superclass.respond_to?(:instance_method)
        if controller_klass.superclass.instance_methods(false).include?(payload[:action].to_sym)
          file, line = controller_klass.superclass.instance_method(payload[:action]).source_location
          return "#{relative_path(file)}:#{line}" if file && line
        end
      end
    rescue => e
      Rails.logger.debug "[RailsPulse] Could not resolve controller source location: #{e.class} - #{e.message}"
    end
    nil
  end

  # Helper method to capture operation data
  def self.capture_operation(event_name, start, finish, payload, operation_type, label_key = nil)
    return unless RailsPulse.configuration.enabled
    return if RequestStore.store[:skip_recording_rails_pulse_activity]

    request_id = RequestStore.store[:rails_pulse_request_id]
    return unless request_id

    # Skip RailsPulse-related operations to prevent recursion
    if operation_type == "sql"
      sql = payload[:sql]
      return if sql&.include?("rails_pulse_")
    end

    label = case label_key
    when :sql then clean_sql_label(payload[:sql])
    when :template then relative_path(payload[:identifier] || payload[:template])
    when :partial then relative_path(payload[:identifier] || payload[:partial])
    when :controller then "#{payload[:controller]}##{payload[:action]}"
    when :cache then payload[:key]
    else payload[label_key] || event_name
    end

    codebase_location =
      if payload[:identifier]
        relative_path(payload[:identifier])
      elsif payload[:template]
        relative_path(payload[:template])
      elsif operation_type == "controller"
        controller_action_source_location(payload) || find_app_frame || caller_locations(3, 1).first&.path
      elsif operation_type == "sql"
        relative_path(find_app_frame || caller_locations(3, 1).first&.path)
      else
        find_app_frame || caller_locations(3, 1).first&.path
      end

    operation_data = {
      request_id: request_id,
      operation_type: operation_type,
      label: label,
      duration: (finish - start) * 1000,
      codebase_location: codebase_location,
      start_time: start.to_f,
      occurred_at: Time.zone.at(start)
    }

    RequestStore.store[:rails_pulse_operations] ||= []
    RequestStore.store[:rails_pulse_operations] << operation_data
  end

  # SQL queries
  ActiveSupport::Notifications.subscribe "sql.active_record" do |name, start, finish, id, payload|
    begin
      next if payload[:name] == "SCHEMA"
      capture_operation(name, start, finish, payload, "sql", :sql)
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in SQL subscriber: #{e.class} - #{e.message}"
    end
  end

  # Controller action processing
  ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, start, finish, id, payload|
    begin
      capture_operation(name, start, finish, payload, "controller", :controller)
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in controller subscriber: #{e.class} - #{e.message}"
    end
  end

  # Template rendering
  ActiveSupport::Notifications.subscribe "render_template.action_view" do |name, start, finish, id, payload|
    begin
      capture_operation(name, start, finish, payload, "template", :template)
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in template subscriber: #{e.class} - #{e.message}"
    end
  end

  # Partial rendering
  ActiveSupport::Notifications.subscribe "render_partial.action_view" do |name, start, finish, id, payload|
    begin
      capture_operation(name, start, finish, payload, "partial", :partial)
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in partial subscriber: #{e.class} - #{e.message}"
    end
  end

  # Layout rendering
  ActiveSupport::Notifications.subscribe "render_layout.action_view" do |name, start, finish, id, payload|
    begin
      capture_operation(name, start, finish, payload, "layout", :template)
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in layout subscriber: #{e.class} - #{e.message}"
    end
  end

  # Cache operations
  ActiveSupport::Notifications.subscribe "cache_read.active_support" do |name, start, finish, id, payload|
    begin
      capture_operation(name, start, finish, payload, "cache_read", :cache)
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in cache_read subscriber: #{e.class} - #{e.message}"
    end
  end

  ActiveSupport::Notifications.subscribe "cache_write.active_support" do |name, start, finish, id, payload|
    begin
      capture_operation(name, start, finish, payload, "cache_write", :cache)
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in cache_write subscriber: #{e.class} - #{e.message}"
    end
  end

  # HTTP client requests (if using Net::HTTP)
  ActiveSupport::Notifications.subscribe "request.net_http" do |name, start, finish, id, payload|
    begin
      next unless RailsPulse.configuration.enabled
      label = "#{payload[:method]} #{payload[:uri]}"
      codebase_location = find_app_frame || caller_locations(2, 1).first&.path
      operation_data = {
        request_id: RequestStore.store[:rails_pulse_request_id],
        operation_type: "http",
        label: label,
        duration: (finish - start) * 1000,
        codebase_location: codebase_location,
        start_time: start.to_f,
        occurred_at: Time.zone.at(start)
      }

      if operation_data[:request_id]
        RequestStore.store[:rails_pulse_operations] ||= []
        RequestStore.store[:rails_pulse_operations] << operation_data
      end
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in HTTP subscriber: #{e.class} - #{e.message}"
    end
  end

  # Active Job processing
  ActiveSupport::Notifications.subscribe "perform.active_job" do |name, start, finish, id, payload|
    begin
      next unless RailsPulse.configuration.enabled
      label = "#{payload[:job].class.name}"
      codebase_location = find_app_frame || caller_locations(2, 1).first&.path
      operation_data = {
        request_id: RequestStore.store[:rails_pulse_request_id],
        operation_type: "job",
        label: label,
        duration: (finish - start) * 1000,
        codebase_location: codebase_location,
        start_time: start.to_f,
        occurred_at: Time.zone.at(start)
      }

      if operation_data[:request_id]
        RequestStore.store[:rails_pulse_operations] ||= []
        RequestStore.store[:rails_pulse_operations] << operation_data
      end
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in job subscriber: #{e.class} - #{e.message}"
    end
  end

  # Collection rendering (for rendering collections)
  ActiveSupport::Notifications.subscribe "render_collection.action_view" do |name, start, finish, id, payload|
    begin
      capture_operation(name, start, finish, payload, "collection", :template)
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in collection subscriber: #{e.class} - #{e.message}"
    end
  end

  # Action Mailer
  ActiveSupport::Notifications.subscribe "deliver.action_mailer" do |name, start, finish, id, payload|
    begin
      next unless RailsPulse.configuration.enabled
      label = "#{payload[:mailer]}##{payload[:action]}"
      codebase_location = find_app_frame || caller_locations(2, 1).first&.path
      operation_data = {
        request_id: RequestStore.store[:rails_pulse_request_id],
        operation_type: "mailer",
        label: label,
        duration: (finish - start) * 1000,
        codebase_location: codebase_location,
        start_time: start.to_f,
        occurred_at: Time.zone.at(start)
      }

      if operation_data[:request_id]
        RequestStore.store[:rails_pulse_operations] ||= []
        RequestStore.store[:rails_pulse_operations] << operation_data
      end
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in mailer subscriber: #{e.class} - #{e.message}"
    end
  end

  # Active Storage
  ActiveSupport::Notifications.subscribe "service_upload.active_storage" do |name, start, finish, id, payload|
    begin
      next unless RailsPulse.configuration.enabled
      label = "Upload: #{payload[:key]}"
      codebase_location = find_app_frame || caller_locations(2, 1).first&.path
      operation_data = {
        request_id: RequestStore.store[:rails_pulse_request_id],
        operation_type: "storage",
        label: label,
        duration: (finish - start) * 1000,
        codebase_location: codebase_location,
        start_time: start.to_f,
        occurred_at: Time.zone.at(start)
      }

      if operation_data[:request_id]
        RequestStore.store[:rails_pulse_operations] ||= []
        RequestStore.store[:rails_pulse_operations] << operation_data
      end
    rescue => e
      Rails.logger.error "[RailsPulse] Exception in storage subscriber: #{e.class} - #{e.message}"
    end
  end
end