rails_pulse 0.2.4 → 0.2.5.pre.pre.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +269 -12
- data/Rakefile +142 -8
- data/app/assets/stylesheets/rails_pulse/components/table.css +16 -1
- data/app/assets/stylesheets/rails_pulse/components/tags.css +7 -2
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +3 -0
- data/app/controllers/concerns/chart_table_concern.rb +2 -1
- data/app/controllers/rails_pulse/application_controller.rb +11 -1
- data/app/controllers/rails_pulse/assets_controller.rb +18 -2
- data/app/controllers/rails_pulse/job_runs_controller.rb +37 -0
- data/app/controllers/rails_pulse/jobs_controller.rb +80 -0
- data/app/controllers/rails_pulse/operations_controller.rb +43 -31
- data/app/controllers/rails_pulse/queries_controller.rb +1 -1
- data/app/controllers/rails_pulse/requests_controller.rb +3 -9
- data/app/controllers/rails_pulse/routes_controller.rb +1 -1
- data/app/controllers/rails_pulse/tags_controller.rb +31 -5
- data/app/helpers/rails_pulse/application_helper.rb +32 -1
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +15 -1
- data/app/helpers/rails_pulse/status_helper.rb +16 -0
- data/app/helpers/rails_pulse/tags_helper.rb +39 -1
- data/app/javascript/rails_pulse/controllers/chart_controller.js +112 -8
- data/app/models/concerns/rails_pulse/taggable.rb +25 -2
- data/app/models/rails_pulse/charts/operations_chart.rb +33 -0
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +1 -2
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
- data/app/models/rails_pulse/job.rb +85 -0
- data/app/models/rails_pulse/job_run.rb +76 -0
- data/app/models/rails_pulse/jobs/cards/average_duration.rb +85 -0
- data/app/models/rails_pulse/jobs/cards/base.rb +70 -0
- data/app/models/rails_pulse/jobs/cards/failure_rate.rb +85 -0
- data/app/models/rails_pulse/jobs/cards/total_jobs.rb +74 -0
- data/app/models/rails_pulse/jobs/cards/total_runs.rb +48 -0
- data/app/models/rails_pulse/operation.rb +16 -3
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +3 -3
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +1 -1
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/tables/index.rb +2 -1
- data/app/models/rails_pulse/query.rb +10 -1
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +3 -2
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +1 -1
- data/app/models/rails_pulse/routes/tables/index.rb +2 -1
- data/app/models/rails_pulse/summary.rb +10 -3
- data/app/services/rails_pulse/summary_service.rb +46 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
- data/app/views/layouts/rails_pulse/application.html.erb +23 -0
- data/app/views/rails_pulse/components/_active_filters.html.erb +7 -6
- data/app/views/rails_pulse/components/_page_header.html.erb +8 -7
- data/app/views/rails_pulse/components/_table.html.erb +7 -4
- data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
- data/app/views/rails_pulse/job_runs/_operations.html.erb +78 -0
- data/app/views/rails_pulse/job_runs/index.html.erb +3 -0
- data/app/views/rails_pulse/job_runs/show.html.erb +51 -0
- data/app/views/rails_pulse/jobs/_job_runs_table.html.erb +35 -0
- data/app/views/rails_pulse/jobs/_table.html.erb +43 -0
- data/app/views/rails_pulse/jobs/index.html.erb +34 -0
- data/app/views/rails_pulse/jobs/show.html.erb +49 -0
- data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +29 -27
- data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +11 -9
- data/app/views/rails_pulse/operations/show.html.erb +10 -8
- data/app/views/rails_pulse/queries/_table.html.erb +3 -3
- data/app/views/rails_pulse/requests/_table.html.erb +6 -6
- data/app/views/rails_pulse/routes/_table.html.erb +3 -3
- data/app/views/rails_pulse/routes/show.html.erb +1 -1
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +7 -14
- data/config/brakeman.ignore +213 -0
- data/config/brakeman.yml +68 -0
- data/config/initializers/rails_pulse.rb +52 -0
- data/config/routes.rb +6 -0
- data/db/rails_pulse_migrate/20250113000000_add_jobs_to_rails_pulse.rb +95 -0
- data/db/rails_pulse_migrate/20250122000000_add_query_fingerprinting.rb +150 -0
- data/db/rails_pulse_migrate/20250202000000_add_index_to_request_uuid.rb +14 -0
- data/db/rails_pulse_schema.rb +186 -103
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +186 -103
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +30 -1
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +31 -0
- data/lib/rails_pulse/active_job_extensions.rb +13 -0
- data/lib/rails_pulse/adapters/delayed_job_plugin.rb +25 -0
- data/lib/rails_pulse/adapters/sidekiq_middleware.rb +41 -0
- data/lib/rails_pulse/cleanup_service.rb +65 -0
- data/lib/rails_pulse/configuration.rb +80 -7
- data/lib/rails_pulse/engine.rb +34 -3
- data/lib/rails_pulse/extensions/active_record.rb +82 -0
- data/lib/rails_pulse/job_run_collector.rb +172 -0
- data/lib/rails_pulse/middleware/request_collector.rb +20 -43
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +11 -5
- data/lib/rails_pulse/tracker.rb +82 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +2 -0
- data/lib/rails_pulse_server.ru +107 -0
- data/lib/tasks/rails_pulse_benchmark.rake +382 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +3 -2
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +1 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +3 -3
- metadata +37 -9
- data/app/models/rails_pulse/requests/charts/operations_chart.rb +0 -35
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +0 -23
|
@@ -4,10 +4,14 @@ module RailsPulse
|
|
|
4
4
|
:route_thresholds,
|
|
5
5
|
:request_thresholds,
|
|
6
6
|
:query_thresholds,
|
|
7
|
+
:job_thresholds,
|
|
7
8
|
:ignored_routes,
|
|
8
9
|
:ignored_requests,
|
|
9
10
|
:ignored_queries,
|
|
11
|
+
:ignored_jobs,
|
|
12
|
+
:ignored_queues,
|
|
10
13
|
:track_assets,
|
|
14
|
+
:track_jobs,
|
|
11
15
|
:custom_asset_patterns,
|
|
12
16
|
:mount_path,
|
|
13
17
|
:full_retention_period,
|
|
@@ -17,32 +21,60 @@ module RailsPulse
|
|
|
17
21
|
:authentication_enabled,
|
|
18
22
|
:authentication_method,
|
|
19
23
|
:authentication_redirect_path,
|
|
20
|
-
:tags
|
|
24
|
+
:tags,
|
|
25
|
+
:job_tracking_mode,
|
|
26
|
+
:job_adapters,
|
|
27
|
+
:capture_job_arguments,
|
|
28
|
+
:mount_dashboard,
|
|
29
|
+
:logger,
|
|
30
|
+
:async
|
|
21
31
|
|
|
22
32
|
def initialize
|
|
23
33
|
@enabled = true
|
|
24
34
|
@route_thresholds = { slow: 500, very_slow: 1500, critical: 3000 }
|
|
25
35
|
@request_thresholds = { slow: 700, very_slow: 2000, critical: 4000 }
|
|
26
36
|
@query_thresholds = { slow: 100, very_slow: 500, critical: 1000 }
|
|
37
|
+
@job_thresholds = { slow: 5_000, very_slow: 30_000, critical: 60_000 }
|
|
27
38
|
@ignored_routes = []
|
|
28
39
|
@ignored_requests = []
|
|
29
40
|
@ignored_queries = []
|
|
41
|
+
@ignored_jobs = []
|
|
42
|
+
@ignored_queues = []
|
|
30
43
|
@track_assets = false
|
|
44
|
+
@track_jobs = false
|
|
31
45
|
@custom_asset_patterns = []
|
|
32
46
|
@mount_path = nil
|
|
33
|
-
@full_retention_period =
|
|
47
|
+
@full_retention_period = 30.days
|
|
34
48
|
@archiving_enabled = true
|
|
35
49
|
@max_table_records = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
rails_pulse_queries:
|
|
50
|
+
rails_pulse_operations: 100_000,
|
|
51
|
+
rails_pulse_requests: 50_000,
|
|
52
|
+
rails_pulse_job_runs: 50_000,
|
|
53
|
+
rails_pulse_queries: 10_000,
|
|
54
|
+
rails_pulse_routes: 1_000,
|
|
55
|
+
rails_pulse_jobs: 1_000
|
|
40
56
|
}
|
|
41
57
|
@connects_to = nil
|
|
42
58
|
@authentication_enabled = Rails.env.production?
|
|
43
59
|
@authentication_method = nil
|
|
44
60
|
@authentication_redirect_path = "/"
|
|
45
61
|
@tags = [ "ignored", "critical", "experimental" ]
|
|
62
|
+
@job_tracking_mode = :universal
|
|
63
|
+
@job_adapters = {
|
|
64
|
+
sidekiq: { enabled: true, track_queue_depth: false },
|
|
65
|
+
solid_queue: { enabled: true, track_recurring: false },
|
|
66
|
+
good_job: { enabled: true, track_cron: false },
|
|
67
|
+
delayed_job: { enabled: true },
|
|
68
|
+
resque: { enabled: true }
|
|
69
|
+
}
|
|
70
|
+
@capture_job_arguments = false
|
|
71
|
+
|
|
72
|
+
# Dashboard settings
|
|
73
|
+
@mount_dashboard = true
|
|
74
|
+
@logger = nil
|
|
75
|
+
|
|
76
|
+
# Tracking mode settings
|
|
77
|
+
@async = true
|
|
46
78
|
|
|
47
79
|
validate_configuration!
|
|
48
80
|
end
|
|
@@ -67,6 +99,9 @@ module RailsPulse
|
|
|
67
99
|
validate_database_settings!
|
|
68
100
|
validate_authentication_settings!
|
|
69
101
|
validate_tags!
|
|
102
|
+
validate_job_settings!
|
|
103
|
+
validate_dashboard_settings!
|
|
104
|
+
validate_tracking_settings!
|
|
70
105
|
end
|
|
71
106
|
|
|
72
107
|
# Revalidate configuration after changes
|
|
@@ -77,7 +112,7 @@ module RailsPulse
|
|
|
77
112
|
private
|
|
78
113
|
|
|
79
114
|
def validate_thresholds!
|
|
80
|
-
[ @route_thresholds, @request_thresholds, @query_thresholds ].each do |thresholds|
|
|
115
|
+
[ @route_thresholds, @request_thresholds, @query_thresholds, @job_thresholds ].each do |thresholds|
|
|
81
116
|
thresholds.each do |key, value|
|
|
82
117
|
unless value.is_a?(Numeric) && value > 0
|
|
83
118
|
raise ArgumentError, "Threshold #{key} must be a positive number, got #{value}"
|
|
@@ -150,6 +185,44 @@ module RailsPulse
|
|
|
150
185
|
end
|
|
151
186
|
end
|
|
152
187
|
|
|
188
|
+
def validate_job_settings!
|
|
189
|
+
unless @ignored_jobs.is_a?(Array) && @ignored_queues.is_a?(Array)
|
|
190
|
+
raise ArgumentError, "ignored_jobs and ignored_queues must be arrays"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
unless [ true, false ].include?(@track_jobs)
|
|
194
|
+
raise ArgumentError, "track_jobs must be a boolean"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
unless @job_adapters.is_a?(Hash)
|
|
198
|
+
raise ArgumentError, "job_adapters must be a hash"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
unless @job_thresholds.is_a?(Hash)
|
|
202
|
+
raise ArgumentError, "job_thresholds must be a hash"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
unless @job_tracking_mode.is_a?(Symbol)
|
|
206
|
+
raise ArgumentError, "job_tracking_mode must be a symbol"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
unless [ true, false ].include?(@capture_job_arguments)
|
|
210
|
+
raise ArgumentError, "capture_job_arguments must be a boolean"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def validate_dashboard_settings!
|
|
215
|
+
unless [ true, false ].include?(@mount_dashboard)
|
|
216
|
+
raise ArgumentError, "mount_dashboard must be true or false, got #{@mount_dashboard}"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def validate_tracking_settings!
|
|
221
|
+
unless [ true, false ].include?(@async)
|
|
222
|
+
raise ArgumentError, "async must be true or false, got #{@async}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
153
226
|
# Default patterns for common asset types and paths
|
|
154
227
|
def default_asset_patterns
|
|
155
228
|
[
|
data/lib/rails_pulse/engine.rb
CHANGED
|
@@ -2,12 +2,14 @@ require "rails_pulse/version"
|
|
|
2
2
|
require "rails_pulse/middleware/request_collector"
|
|
3
3
|
require "rails_pulse/middleware/asset_server"
|
|
4
4
|
require "rails_pulse/subscribers/operation_subscriber"
|
|
5
|
+
require "rails_pulse/job_run_collector"
|
|
6
|
+
require "rails_pulse/active_job_extensions"
|
|
7
|
+
require "rails_pulse/extensions/active_record"
|
|
5
8
|
require "request_store"
|
|
6
9
|
require "rack/static"
|
|
7
10
|
require "ransack"
|
|
8
11
|
require "pagy"
|
|
9
12
|
require "turbo-rails"
|
|
10
|
-
require "groupdate"
|
|
11
13
|
|
|
12
14
|
module RailsPulse
|
|
13
15
|
class Engine < ::Rails::Engine
|
|
@@ -49,6 +51,30 @@ module RailsPulse
|
|
|
49
51
|
# Ensure Ransack is loaded before our models
|
|
50
52
|
end
|
|
51
53
|
|
|
54
|
+
initializer "rails_pulse.active_job" do
|
|
55
|
+
ActiveSupport.on_load(:active_job) do
|
|
56
|
+
include RailsPulse::ActiveJobExtensions
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
initializer "rails_pulse.configure_sidekiq", after: "rails_pulse.active_job" do
|
|
61
|
+
if defined?(Sidekiq) && RailsPulse.configuration.job_adapters.dig(:sidekiq, :enabled)
|
|
62
|
+
require "rails_pulse/adapters/sidekiq_middleware"
|
|
63
|
+
Sidekiq.configure_server do |config|
|
|
64
|
+
config.server_middleware do |chain|
|
|
65
|
+
chain.add RailsPulse::Adapters::SidekiqMiddleware
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
initializer "rails_pulse.configure_delayed_job", after: "rails_pulse.active_job" do
|
|
72
|
+
if defined?(Delayed::Job) && RailsPulse.configuration.job_adapters.dig(:delayed_job, :enabled)
|
|
73
|
+
require "rails_pulse/adapters/delayed_job_plugin"
|
|
74
|
+
Delayed::Worker.plugins << RailsPulse::Adapters::DelayedJobPlugin
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
52
78
|
initializer "rails_pulse.database_configuration", before: "active_record.initialize_timezone" do
|
|
53
79
|
# Ensure database configuration is applied early in the initialization process
|
|
54
80
|
# This allows models to properly connect to configured databases
|
|
@@ -56,9 +82,14 @@ module RailsPulse
|
|
|
56
82
|
|
|
57
83
|
initializer "rails_pulse.timezone" do
|
|
58
84
|
# Configure Rails Pulse to always use UTC for consistent time operations
|
|
59
|
-
# This prevents Groupdate timezone mismatch errors across different host applications
|
|
60
85
|
# Note: We don't set Time.zone_default as it would affect the entire application
|
|
61
|
-
#
|
|
86
|
+
# Our custom group_by_date extension works regardless of ActiveRecord.default_timezone
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
initializer "rails_pulse.configure_logger", before: :initialize_logger do
|
|
90
|
+
RailsPulse.configure do |config|
|
|
91
|
+
config.logger ||= Rails.logger
|
|
92
|
+
end
|
|
62
93
|
end
|
|
63
94
|
|
|
64
95
|
initializer "rails_pulse.disable_turbo" do
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsPulse
|
|
4
|
+
module Extensions
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
# Extends ActiveRecord::Relation with database-agnostic date grouping
|
|
7
|
+
# This is a replacement for Groupdate that works regardless of ActiveRecord.default_timezone
|
|
8
|
+
module QueryMethods
|
|
9
|
+
# Groups records by date extracted from a timestamp column
|
|
10
|
+
# Works across PostgreSQL, MySQL, and SQLite
|
|
11
|
+
#
|
|
12
|
+
# @param column [Symbol, String] the timestamp column to group by (default: :period_start)
|
|
13
|
+
# @return [ActiveRecord::Relation] relation with DATE grouping applied
|
|
14
|
+
#
|
|
15
|
+
# @example Group summaries by date
|
|
16
|
+
# RailsPulse::Summary.where(...).group_by_date(:period_start).sum(:count)
|
|
17
|
+
# # => { Date(2024-01-01) => 100, Date(2024-01-02) => 150, ... }
|
|
18
|
+
#
|
|
19
|
+
# @example Group by different column
|
|
20
|
+
# Model.group_by_date(:created_at).count
|
|
21
|
+
#
|
|
22
|
+
def group_by_date(column = :period_start)
|
|
23
|
+
group(Arel.sql(date_sql(column.to_s))).extending(DateResultTransformer)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Returns database-specific SQL for extracting date from timestamp
|
|
29
|
+
def date_sql(column)
|
|
30
|
+
adapter = connection.adapter_name.downcase
|
|
31
|
+
|
|
32
|
+
case adapter
|
|
33
|
+
when "postgresql"
|
|
34
|
+
"DATE(#{column})"
|
|
35
|
+
when "mysql", "mysql2"
|
|
36
|
+
"DATE(#{column})"
|
|
37
|
+
when "sqlite"
|
|
38
|
+
"DATE(#{column})"
|
|
39
|
+
else
|
|
40
|
+
# Fallback for unknown adapters
|
|
41
|
+
"DATE(#{column})"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Module to transform aggregation result keys from strings to Date objects
|
|
47
|
+
# This makes the API match Groupdate's behavior
|
|
48
|
+
module DateResultTransformer
|
|
49
|
+
def sum(*args)
|
|
50
|
+
super.transform_keys { |date_str| Date.parse(date_str.to_s) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def count(*args)
|
|
54
|
+
result = super
|
|
55
|
+
# count can return an integer or a hash depending on whether group is used
|
|
56
|
+
result.is_a?(Hash) ? result.transform_keys { |date_str| Date.parse(date_str.to_s) } : result
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def average(*args)
|
|
60
|
+
super.transform_keys { |date_str| Date.parse(date_str.to_s) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def maximum(*args)
|
|
64
|
+
super.transform_keys { |date_str| Date.parse(date_str.to_s) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def minimum(*args)
|
|
68
|
+
super.transform_keys { |date_str| Date.parse(date_str.to_s) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def pluck(*args)
|
|
72
|
+
result = super
|
|
73
|
+
# If grouping, transform the keys
|
|
74
|
+
result.is_a?(Hash) ? result.transform_keys { |date_str| Date.parse(date_str.to_s) } : result
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Extend ActiveRecord::Relation with our date grouping methods
|
|
82
|
+
ActiveRecord::Relation.include(RailsPulse::Extensions::ActiveRecord::QueryMethods)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module RailsPulse
|
|
4
|
+
class JobRunCollector
|
|
5
|
+
class << self
|
|
6
|
+
def track(active_job, adapter: detect_adapter)
|
|
7
|
+
return yield unless tracking_enabled?
|
|
8
|
+
return yield if ignore_job?(active_job)
|
|
9
|
+
|
|
10
|
+
previous_request_id = RequestStore.store[:rails_pulse_request_id]
|
|
11
|
+
previous_operations = RequestStore.store[:rails_pulse_operations]
|
|
12
|
+
previous_job_run_id = RequestStore.store[:rails_pulse_job_run_id]
|
|
13
|
+
|
|
14
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
15
|
+
occurred_at = Time.current
|
|
16
|
+
|
|
17
|
+
job = nil
|
|
18
|
+
job_run = nil
|
|
19
|
+
|
|
20
|
+
with_recording_suppressed do
|
|
21
|
+
job = find_or_create_job(active_job)
|
|
22
|
+
job_run = create_job_run(job, active_job, adapter, occurred_at)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
RequestStore.store[:rails_pulse_request_id] = nil
|
|
26
|
+
RequestStore.store[:rails_pulse_job_run_id] = job_run.id
|
|
27
|
+
RequestStore.store[:rails_pulse_operations] = []
|
|
28
|
+
|
|
29
|
+
yield
|
|
30
|
+
|
|
31
|
+
duration = elapsed_time_ms(start_time)
|
|
32
|
+
with_recording_suppressed do
|
|
33
|
+
job_run.update!(status: "success", duration: duration)
|
|
34
|
+
end
|
|
35
|
+
rescue => error
|
|
36
|
+
duration = elapsed_time_ms(start_time)
|
|
37
|
+
with_recording_suppressed do
|
|
38
|
+
job_run.update!(
|
|
39
|
+
status: failure_status_for(error),
|
|
40
|
+
duration: duration,
|
|
41
|
+
error_class: error.class.name,
|
|
42
|
+
error_message: error.message
|
|
43
|
+
) if job_run
|
|
44
|
+
end
|
|
45
|
+
raise
|
|
46
|
+
ensure
|
|
47
|
+
begin
|
|
48
|
+
save_operations(job_run)
|
|
49
|
+
rescue => e
|
|
50
|
+
Rails.logger.error "[RailsPulse] Failed to persist job operations: #{e.class} - #{e.message}"
|
|
51
|
+
ensure
|
|
52
|
+
RequestStore.store[:rails_pulse_job_run_id] = previous_job_run_id
|
|
53
|
+
RequestStore.store[:rails_pulse_operations] = previous_operations
|
|
54
|
+
RequestStore.store[:rails_pulse_request_id] = previous_request_id
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def should_ignore_job?(job)
|
|
59
|
+
ignore_job?(job)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def tracking_enabled?
|
|
65
|
+
config = RailsPulse.configuration
|
|
66
|
+
config.enabled && config.track_jobs
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def ignore_job?(job)
|
|
70
|
+
config = RailsPulse.configuration
|
|
71
|
+
job_class = job.class.name
|
|
72
|
+
queue_name = job.queue_name
|
|
73
|
+
|
|
74
|
+
return true if config.ignored_jobs&.include?(job_class)
|
|
75
|
+
return true if config.ignored_queues&.include?(queue_name)
|
|
76
|
+
return true if job_class.start_with?("RailsPulse::")
|
|
77
|
+
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def find_or_create_job(active_job)
|
|
82
|
+
RailsPulse::Job.find_or_create_by!(name: active_job.class.name) do |job|
|
|
83
|
+
job.queue_name = active_job.queue_name
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def create_job_run(job, active_job, adapter, occurred_at)
|
|
88
|
+
RailsPulse::JobRun.create!(
|
|
89
|
+
job: job,
|
|
90
|
+
run_id: active_job.job_id || SecureRandom.uuid,
|
|
91
|
+
status: initial_status_for(active_job),
|
|
92
|
+
enqueued_at: safe_timestamp(active_job.try(:enqueued_at)),
|
|
93
|
+
occurred_at: occurred_at,
|
|
94
|
+
attempts: (active_job.respond_to?(:executions) ? active_job.executions : 0),
|
|
95
|
+
adapter: adapter,
|
|
96
|
+
arguments: serialized_arguments(active_job)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def serialized_arguments(active_job)
|
|
101
|
+
return unless RailsPulse.configuration.capture_job_arguments
|
|
102
|
+
|
|
103
|
+
Array(active_job.arguments).to_json
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
Rails.logger.debug "[RailsPulse] Unable to serialize job arguments: #{e.class} - #{e.message}"
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def initial_status_for(active_job)
|
|
110
|
+
active_job.respond_to?(:scheduled_at) ? "enqueued" : "running"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def failure_status_for(error)
|
|
114
|
+
error.is_a?(StandardError) ? "failed" : "discarded"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def save_operations(job_run)
|
|
118
|
+
return unless job_run
|
|
119
|
+
|
|
120
|
+
operations_data = RequestStore.store[:rails_pulse_operations] || []
|
|
121
|
+
operations_data.each do |operation_data|
|
|
122
|
+
operation_data[:job_run_id] = job_run.id
|
|
123
|
+
operation_data[:request_id] = nil
|
|
124
|
+
|
|
125
|
+
with_recording_suppressed do
|
|
126
|
+
RailsPulse::Operation.create!(operation_data)
|
|
127
|
+
end
|
|
128
|
+
rescue => e
|
|
129
|
+
Rails.logger.error "[RailsPulse] Failed to save job operation: #{e.class} - #{e.message}"
|
|
130
|
+
end
|
|
131
|
+
ensure
|
|
132
|
+
RequestStore.store[:rails_pulse_operations] = nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def detect_adapter
|
|
136
|
+
return "sidekiq" if defined?(::Sidekiq)
|
|
137
|
+
return "solid_queue" if defined?(::SolidQueue)
|
|
138
|
+
return "good_job" if defined?(::GoodJob)
|
|
139
|
+
return "delayed_job" if defined?(::Delayed::Job)
|
|
140
|
+
return "resque" if defined?(::Resque)
|
|
141
|
+
return "que" if defined?(::Que)
|
|
142
|
+
|
|
143
|
+
"active_job"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def elapsed_time_ms(start_time)
|
|
147
|
+
return 0.0 unless start_time
|
|
148
|
+
|
|
149
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def safe_timestamp(value)
|
|
153
|
+
case value
|
|
154
|
+
when Time, ActiveSupport::TimeWithZone
|
|
155
|
+
value
|
|
156
|
+
when Integer
|
|
157
|
+
Time.at(value)
|
|
158
|
+
else
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def with_recording_suppressed
|
|
164
|
+
previous = RequestStore.store[:skip_recording_rails_pulse_activity]
|
|
165
|
+
RequestStore.store[:skip_recording_rails_pulse_activity] = true
|
|
166
|
+
yield
|
|
167
|
+
ensure
|
|
168
|
+
RequestStore.store[:skip_recording_rails_pulse_activity] = previous
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -31,52 +31,35 @@ module RailsPulse
|
|
|
31
31
|
return result
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Clear any previous request
|
|
34
|
+
# Clear any previous request data
|
|
35
35
|
RequestStore.store[:rails_pulse_request_id] = nil
|
|
36
|
+
RequestStore.store[:rails_pulse_operations] = []
|
|
36
37
|
|
|
37
38
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
38
|
-
|
|
39
|
-
# Temporarily skip recording while we create the route and request
|
|
40
|
-
RequestStore.store[:skip_recording_rails_pulse_activity] = true
|
|
41
|
-
route = find_or_create_route(req)
|
|
42
39
|
controller_action = "#{env['action_dispatch.request.parameters']&.[]('controller')&.classify}##{env['action_dispatch.request.parameters']&.[]('action')}"
|
|
43
40
|
occurred_at = Time.current
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
if route
|
|
47
|
-
request = RailsPulse::Request.create!(
|
|
48
|
-
route: route,
|
|
49
|
-
duration: 0, # will update after response
|
|
50
|
-
status: 0, # will update after response
|
|
51
|
-
is_error: false,
|
|
52
|
-
request_uuid: req.uuid,
|
|
53
|
-
controller_action: controller_action,
|
|
54
|
-
occurred_at: occurred_at
|
|
55
|
-
)
|
|
56
|
-
RequestStore.store[:rails_pulse_request_id] = request.id
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Re-enable recording for the actual request processing
|
|
60
|
-
RequestStore.store[:skip_recording_rails_pulse_activity] = false
|
|
61
|
-
|
|
42
|
+
# Process request
|
|
62
43
|
status, headers, response = @app.call(env)
|
|
63
44
|
duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
|
|
64
45
|
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
46
|
+
# Collect all tracking data
|
|
47
|
+
# Deep copy operations array to prevent race condition in async mode
|
|
48
|
+
operations = RequestStore.store[:rails_pulse_operations] || []
|
|
49
|
+
tracking_data = {
|
|
50
|
+
method: req.request_method,
|
|
51
|
+
path: req.path,
|
|
52
|
+
duration: duration,
|
|
53
|
+
status: status,
|
|
54
|
+
is_error: status.to_i >= 500,
|
|
55
|
+
request_uuid: req.uuid,
|
|
56
|
+
controller_action: controller_action,
|
|
57
|
+
occurred_at: occurred_at,
|
|
58
|
+
operations: operations.map(&:dup)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Send to tracker (non-blocking if async mode enabled)
|
|
62
|
+
RailsPulse::Tracker.track_request(tracking_data)
|
|
80
63
|
|
|
81
64
|
[ status, headers, response ]
|
|
82
65
|
ensure
|
|
@@ -87,12 +70,6 @@ module RailsPulse
|
|
|
87
70
|
|
|
88
71
|
private
|
|
89
72
|
|
|
90
|
-
def find_or_create_route(req)
|
|
91
|
-
method = req.request_method
|
|
92
|
-
path = req.path
|
|
93
|
-
RailsPulse::Route.find_or_create_by(method: method, path: path)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
73
|
def should_ignore_route?(req)
|
|
97
74
|
# Get ignored routes from configuration
|
|
98
75
|
ignored_routes = RailsPulse.configuration.ignored_routes || []
|
|
@@ -59,7 +59,8 @@ module RailsPulse
|
|
|
59
59
|
return if RequestStore.store[:skip_recording_rails_pulse_activity]
|
|
60
60
|
|
|
61
61
|
request_id = RequestStore.store[:rails_pulse_request_id]
|
|
62
|
-
|
|
62
|
+
job_run_id = RequestStore.store[:rails_pulse_job_run_id]
|
|
63
|
+
return unless request_id || job_run_id
|
|
63
64
|
|
|
64
65
|
# Skip RailsPulse-related operations to prevent recursion
|
|
65
66
|
if operation_type == "sql"
|
|
@@ -91,6 +92,7 @@ module RailsPulse
|
|
|
91
92
|
|
|
92
93
|
operation_data = {
|
|
93
94
|
request_id: request_id,
|
|
95
|
+
job_run_id: job_run_id,
|
|
94
96
|
operation_type: operation_type,
|
|
95
97
|
label: label,
|
|
96
98
|
duration: (finish - start) * 1000,
|
|
@@ -174,6 +176,7 @@ module RailsPulse
|
|
|
174
176
|
codebase_location = find_app_frame || caller_locations(2, 1).first&.path
|
|
175
177
|
operation_data = {
|
|
176
178
|
request_id: RequestStore.store[:rails_pulse_request_id],
|
|
179
|
+
job_run_id: RequestStore.store[:rails_pulse_job_run_id],
|
|
177
180
|
operation_type: "http",
|
|
178
181
|
label: label,
|
|
179
182
|
duration: (finish - start) * 1000,
|
|
@@ -182,7 +185,7 @@ module RailsPulse
|
|
|
182
185
|
occurred_at: Time.zone.at(start)
|
|
183
186
|
}
|
|
184
187
|
|
|
185
|
-
if operation_data[:request_id]
|
|
188
|
+
if operation_data[:request_id] || operation_data[:job_run_id]
|
|
186
189
|
RequestStore.store[:rails_pulse_operations] ||= []
|
|
187
190
|
RequestStore.store[:rails_pulse_operations] << operation_data
|
|
188
191
|
end
|
|
@@ -199,6 +202,7 @@ module RailsPulse
|
|
|
199
202
|
codebase_location = find_app_frame || caller_locations(2, 1).first&.path
|
|
200
203
|
operation_data = {
|
|
201
204
|
request_id: RequestStore.store[:rails_pulse_request_id],
|
|
205
|
+
job_run_id: RequestStore.store[:rails_pulse_job_run_id],
|
|
202
206
|
operation_type: "job",
|
|
203
207
|
label: label,
|
|
204
208
|
duration: (finish - start) * 1000,
|
|
@@ -207,7 +211,7 @@ module RailsPulse
|
|
|
207
211
|
occurred_at: Time.zone.at(start)
|
|
208
212
|
}
|
|
209
213
|
|
|
210
|
-
if operation_data[:request_id]
|
|
214
|
+
if operation_data[:request_id] || operation_data[:job_run_id]
|
|
211
215
|
RequestStore.store[:rails_pulse_operations] ||= []
|
|
212
216
|
RequestStore.store[:rails_pulse_operations] << operation_data
|
|
213
217
|
end
|
|
@@ -233,6 +237,7 @@ module RailsPulse
|
|
|
233
237
|
codebase_location = find_app_frame || caller_locations(2, 1).first&.path
|
|
234
238
|
operation_data = {
|
|
235
239
|
request_id: RequestStore.store[:rails_pulse_request_id],
|
|
240
|
+
job_run_id: RequestStore.store[:rails_pulse_job_run_id],
|
|
236
241
|
operation_type: "mailer",
|
|
237
242
|
label: label,
|
|
238
243
|
duration: (finish - start) * 1000,
|
|
@@ -241,7 +246,7 @@ module RailsPulse
|
|
|
241
246
|
occurred_at: Time.zone.at(start)
|
|
242
247
|
}
|
|
243
248
|
|
|
244
|
-
if operation_data[:request_id]
|
|
249
|
+
if operation_data[:request_id] || operation_data[:job_run_id]
|
|
245
250
|
RequestStore.store[:rails_pulse_operations] ||= []
|
|
246
251
|
RequestStore.store[:rails_pulse_operations] << operation_data
|
|
247
252
|
end
|
|
@@ -258,6 +263,7 @@ module RailsPulse
|
|
|
258
263
|
codebase_location = find_app_frame || caller_locations(2, 1).first&.path
|
|
259
264
|
operation_data = {
|
|
260
265
|
request_id: RequestStore.store[:rails_pulse_request_id],
|
|
266
|
+
job_run_id: RequestStore.store[:rails_pulse_job_run_id],
|
|
261
267
|
operation_type: "storage",
|
|
262
268
|
label: label,
|
|
263
269
|
duration: (finish - start) * 1000,
|
|
@@ -266,7 +272,7 @@ module RailsPulse
|
|
|
266
272
|
occurred_at: Time.zone.at(start)
|
|
267
273
|
}
|
|
268
274
|
|
|
269
|
-
if operation_data[:request_id]
|
|
275
|
+
if operation_data[:request_id] || operation_data[:job_run_id]
|
|
270
276
|
RequestStore.store[:rails_pulse_operations] ||= []
|
|
271
277
|
RequestStore.store[:rails_pulse_operations] << operation_data
|
|
272
278
|
end
|