rails_pulse 0.2.4 → 0.2.5.pre.pre.2
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 +35 -7
- data/app/models/rails_pulse/requests/charts/operations_chart.rb +0 -35
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +0 -23
|
@@ -3,128 +3,211 @@
|
|
|
3
3
|
# Load with: rails db:schema:load:rails_pulse or db:prepare
|
|
4
4
|
|
|
5
5
|
RailsPulse::Schema = lambda do |connection|
|
|
6
|
+
adapter = connection.adapter_name.downcase
|
|
6
7
|
# Skip if all tables already exist to prevent conflicts
|
|
7
|
-
required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_summaries ]
|
|
8
|
+
required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_jobs, :rails_pulse_job_runs, :rails_pulse_summaries ]
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
# Check which tables already exist
|
|
11
|
+
existing_tables = required_tables.select { |table| connection.table_exists?(table) }
|
|
12
|
+
missing_tables = required_tables - existing_tables
|
|
13
|
+
|
|
14
|
+
# Always log for transparency (not just in CI)
|
|
15
|
+
if existing_tables.any?
|
|
16
|
+
puts "[RailsPulse::Schema] Existing tables detected: #{existing_tables.join(', ')}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if missing_tables.any?
|
|
20
|
+
puts "[RailsPulse::Schema] Creating missing tables: #{missing_tables.join(', ')}"
|
|
14
21
|
end
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
# If all tables exist, skip creation entirely
|
|
24
|
+
if missing_tables.empty?
|
|
25
|
+
puts "[RailsPulse::Schema] All Rails Pulse tables already exist. Skipping schema load."
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
unless connection.table_exists?(:rails_pulse_routes)
|
|
30
|
+
connection.create_table :rails_pulse_routes do |t|
|
|
31
|
+
t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
|
|
32
|
+
t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
|
|
33
|
+
t.text :tags, comment: "JSON array of tags for filtering and categorization"
|
|
34
|
+
t.timestamps
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
connection.add_index :rails_pulse_routes, [ :method, :path ], unique: true, name: "index_rails_pulse_routes_on_method_and_path"
|
|
38
|
+
end
|
|
17
39
|
|
|
18
|
-
connection.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
unless connection.table_exists?(:rails_pulse_queries)
|
|
41
|
+
connection.create_table :rails_pulse_queries do |t|
|
|
42
|
+
t.string :hashed_sql, limit: 32, null: false, comment: "MD5 hash of normalized SQL for indexing"
|
|
43
|
+
t.text :normalized_sql, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
|
|
44
|
+
t.datetime :analyzed_at, comment: "When query analysis was last performed"
|
|
45
|
+
t.text :explain_plan, comment: "EXPLAIN output from actual SQL execution"
|
|
46
|
+
t.text :issues, comment: "JSON array of detected performance issues"
|
|
47
|
+
t.text :metadata, comment: "JSON object containing query complexity metrics"
|
|
48
|
+
t.text :query_stats, comment: "JSON object with query characteristics analysis"
|
|
49
|
+
t.text :backtrace_analysis, comment: "JSON object with call chain and N+1 detection"
|
|
50
|
+
t.text :index_recommendations, comment: "JSON array of database index recommendations"
|
|
51
|
+
t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
|
|
52
|
+
t.text :suggestions, comment: "JSON array of optimization recommendations"
|
|
53
|
+
t.text :tags, comment: "JSON array of tags for filtering and categorization"
|
|
54
|
+
t.timestamps
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
connection.add_index :rails_pulse_queries, :hashed_sql, unique: true, name: "index_rails_pulse_queries_on_hashed_sql"
|
|
23
58
|
end
|
|
24
59
|
|
|
25
|
-
connection.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
unless connection.table_exists?(:rails_pulse_requests)
|
|
61
|
+
connection.create_table :rails_pulse_requests do |t|
|
|
62
|
+
t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
|
|
63
|
+
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
|
|
64
|
+
t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
|
|
65
|
+
t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
|
|
66
|
+
t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
|
|
67
|
+
t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
|
|
68
|
+
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
|
69
|
+
t.text :tags, comment: "JSON array of tags for filtering and categorization"
|
|
70
|
+
t.timestamps
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
|
|
74
|
+
connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
|
|
75
|
+
connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
|
|
40
76
|
end
|
|
41
77
|
|
|
42
|
-
connection.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
unless connection.table_exists?(:rails_pulse_jobs)
|
|
79
|
+
connection.create_table :rails_pulse_jobs do |t|
|
|
80
|
+
t.string :name, null: false, comment: "Job class name"
|
|
81
|
+
t.string :queue_name, comment: "Default queue"
|
|
82
|
+
t.text :description, comment: "Optional description"
|
|
83
|
+
t.integer :runs_count, null: false, default: 0, comment: "Cache of total runs"
|
|
84
|
+
t.integer :failures_count, null: false, default: 0, comment: "Cache of failed runs"
|
|
85
|
+
t.integer :retries_count, null: false, default: 0, comment: "Cache of retried runs"
|
|
86
|
+
t.decimal :avg_duration, precision: 15, scale: 6, comment: "Average duration in milliseconds"
|
|
87
|
+
t.text :tags, comment: "JSON array of tags"
|
|
88
|
+
t.timestamps
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
connection.add_index :rails_pulse_jobs, :name, unique: true, name: "index_rails_pulse_jobs_on_name"
|
|
92
|
+
connection.add_index :rails_pulse_jobs, :queue_name, name: "index_rails_pulse_jobs_on_queue"
|
|
93
|
+
connection.add_index :rails_pulse_jobs, :runs_count, name: "index_rails_pulse_jobs_on_runs_count"
|
|
54
94
|
end
|
|
55
95
|
|
|
56
|
-
connection.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
96
|
+
unless connection.table_exists?(:rails_pulse_job_runs)
|
|
97
|
+
connection.create_table :rails_pulse_job_runs do |t|
|
|
98
|
+
t.references :job, null: false, foreign_key: { to_table: :rails_pulse_jobs }, comment: "Link to job definition"
|
|
99
|
+
t.string :run_id, null: false, comment: "Adapter specific run id"
|
|
100
|
+
t.decimal :duration, precision: 15, scale: 6, comment: "Execution duration in milliseconds"
|
|
101
|
+
t.string :status, null: false, comment: "Execution status"
|
|
102
|
+
t.string :error_class, comment: "Error class name"
|
|
103
|
+
t.text :error_message, comment: "Error message"
|
|
104
|
+
t.integer :attempts, null: false, default: 0, comment: "Retry attempts"
|
|
105
|
+
t.timestamp :occurred_at, null: false, comment: "When the job started"
|
|
106
|
+
t.timestamp :enqueued_at, comment: "When the job was enqueued"
|
|
107
|
+
t.text :arguments, comment: "Serialized arguments"
|
|
108
|
+
t.string :adapter, comment: "Queue adapter"
|
|
109
|
+
t.text :tags, comment: "Execution tags"
|
|
110
|
+
t.timestamps
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
connection.add_index :rails_pulse_job_runs, :run_id, unique: true, name: "index_rails_pulse_job_runs_on_run_id"
|
|
114
|
+
connection.add_index :rails_pulse_job_runs, [ :job_id, :occurred_at ], name: "index_rails_pulse_job_runs_on_job_and_occurred"
|
|
115
|
+
connection.add_index :rails_pulse_job_runs, :occurred_at, name: "index_rails_pulse_job_runs_on_occurred_at"
|
|
116
|
+
connection.add_index :rails_pulse_job_runs, :status, name: "index_rails_pulse_job_runs_on_status"
|
|
117
|
+
connection.add_index :rails_pulse_job_runs, [ :job_id, :status ], name: "index_rails_pulse_job_runs_on_job_and_status"
|
|
70
118
|
end
|
|
71
119
|
|
|
72
|
-
connection.
|
|
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
|
-
t.float :total_duration, comment: "Total duration in milliseconds"
|
|
98
|
-
t.float :stddev_duration, comment: "Standard deviation of duration"
|
|
99
|
-
|
|
100
|
-
# Request/Route specific metrics
|
|
101
|
-
t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
|
|
102
|
-
t.integer :success_count, default: 0, comment: "Number of successful responses"
|
|
103
|
-
t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
|
|
104
|
-
t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
|
|
105
|
-
t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
|
|
106
|
-
t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
|
|
107
|
-
|
|
108
|
-
t.timestamps
|
|
120
|
+
unless connection.table_exists?(:rails_pulse_operations)
|
|
121
|
+
connection.create_table :rails_pulse_operations do |t|
|
|
122
|
+
t.references :request, null: true, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
|
|
123
|
+
t.references :job_run, null: true, foreign_key: { to_table: :rails_pulse_job_runs }, comment: "Link to a background job execution"
|
|
124
|
+
t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
|
|
125
|
+
t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
|
|
126
|
+
t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
|
|
127
|
+
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
|
|
128
|
+
t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
|
|
129
|
+
t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
|
|
130
|
+
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
|
131
|
+
t.timestamps
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
|
|
135
|
+
connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
|
|
136
|
+
connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
|
|
137
|
+
connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
|
|
138
|
+
connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
|
|
139
|
+
|
|
140
|
+
if adapter.include?("postgres") || adapter.include?("mysql")
|
|
141
|
+
connection.add_check_constraint :rails_pulse_operations,
|
|
142
|
+
"(request_id IS NOT NULL OR job_run_id IS NOT NULL)",
|
|
143
|
+
name: "rails_pulse_operations_request_or_job_run"
|
|
144
|
+
end
|
|
109
145
|
end
|
|
110
146
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
unless connection.table_exists?(:rails_pulse_summaries)
|
|
148
|
+
connection.create_table :rails_pulse_summaries do |t|
|
|
149
|
+
# Time fields
|
|
150
|
+
t.datetime :period_start, null: false, comment: "Start of the aggregation period"
|
|
151
|
+
t.datetime :period_end, null: false, comment: "End of the aggregation period"
|
|
152
|
+
t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
|
|
153
|
+
|
|
154
|
+
# Polymorphic association to handle both routes and queries
|
|
155
|
+
t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
|
|
156
|
+
# This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
|
|
157
|
+
# and summarizable_id (route_id or query_id)
|
|
158
|
+
|
|
159
|
+
# Universal metrics
|
|
160
|
+
t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
|
|
161
|
+
t.float :avg_duration, comment: "Average duration in milliseconds"
|
|
162
|
+
t.float :min_duration, comment: "Minimum duration in milliseconds"
|
|
163
|
+
t.float :max_duration, comment: "Maximum duration in milliseconds"
|
|
164
|
+
t.float :p50_duration, comment: "50th percentile duration"
|
|
165
|
+
t.float :p95_duration, comment: "95th percentile duration"
|
|
166
|
+
t.float :p99_duration, comment: "99th percentile duration"
|
|
167
|
+
t.float :total_duration, comment: "Total duration in milliseconds"
|
|
168
|
+
t.float :stddev_duration, comment: "Standard deviation of duration"
|
|
169
|
+
|
|
170
|
+
# Request/Route specific metrics
|
|
171
|
+
t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
|
|
172
|
+
t.integer :success_count, default: 0, comment: "Number of successful responses"
|
|
173
|
+
t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
|
|
174
|
+
t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
|
|
175
|
+
t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
|
|
176
|
+
t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
|
|
177
|
+
|
|
178
|
+
t.timestamps
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Unique constraint and indexes for summaries
|
|
182
|
+
connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
|
|
183
|
+
unique: true,
|
|
184
|
+
name: "idx_pulse_summaries_unique"
|
|
185
|
+
connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
|
|
186
|
+
connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
|
|
187
|
+
end
|
|
117
188
|
|
|
118
189
|
# Add indexes to existing tables for efficient aggregation
|
|
119
|
-
connection.
|
|
120
|
-
|
|
190
|
+
unless connection.index_exists?(:rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation")
|
|
191
|
+
connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
unless connection.index_exists?(:rails_pulse_requests, :created_at, name: "idx_requests_created_at")
|
|
195
|
+
connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
|
|
196
|
+
end
|
|
121
197
|
|
|
122
|
-
connection.
|
|
123
|
-
|
|
198
|
+
unless connection.index_exists?(:rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation")
|
|
199
|
+
connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
unless connection.index_exists?(:rails_pulse_operations, :created_at, name: "idx_operations_created_at")
|
|
203
|
+
connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
|
|
204
|
+
end
|
|
124
205
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
206
|
+
# Log successful creation
|
|
207
|
+
created_tables = required_tables.select { |table| connection.table_exists?(table) }
|
|
208
|
+
newly_created = created_tables - existing_tables
|
|
209
|
+
if newly_created.any?
|
|
210
|
+
puts "[RailsPulse::Schema] Successfully created tables: #{newly_created.join(', ')}"
|
|
128
211
|
end
|
|
129
212
|
end
|
|
130
213
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Generated from Rails Pulse schema - automatically loads current schema definition
|
|
2
2
|
class InstallRailsPulseTables < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
|
3
|
-
def
|
|
3
|
+
def up
|
|
4
|
+
# Check if Rails Pulse is already installed
|
|
5
|
+
if rails_pulse_installed?
|
|
6
|
+
say "Rails Pulse tables already exist. Skipping installation.", :yellow
|
|
7
|
+
return
|
|
8
|
+
end
|
|
9
|
+
|
|
4
10
|
# Load and execute the Rails Pulse schema directly
|
|
5
11
|
# This ensures the migration is always in sync with the schema file
|
|
6
12
|
schema_file = File.join(::Rails.root.to_s, "db/rails_pulse_schema.rb")
|
|
@@ -20,4 +26,27 @@ class InstallRailsPulseTables < ActiveRecord::Migration[<%= Rails::VERSION::MAJO
|
|
|
20
26
|
raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb"
|
|
21
27
|
end
|
|
22
28
|
end
|
|
29
|
+
|
|
30
|
+
def down
|
|
31
|
+
# Rollback: drop all Rails Pulse tables in reverse dependency order
|
|
32
|
+
say "Dropping Rails Pulse tables..."
|
|
33
|
+
|
|
34
|
+
drop_table :rails_pulse_operations if table_exists?(:rails_pulse_operations)
|
|
35
|
+
drop_table :rails_pulse_job_runs if table_exists?(:rails_pulse_job_runs)
|
|
36
|
+
drop_table :rails_pulse_jobs if table_exists?(:rails_pulse_jobs)
|
|
37
|
+
drop_table :rails_pulse_summaries if table_exists?(:rails_pulse_summaries)
|
|
38
|
+
drop_table :rails_pulse_requests if table_exists?(:rails_pulse_requests)
|
|
39
|
+
drop_table :rails_pulse_routes if table_exists?(:rails_pulse_routes)
|
|
40
|
+
drop_table :rails_pulse_queries if table_exists?(:rails_pulse_queries)
|
|
41
|
+
|
|
42
|
+
say "Rails Pulse tables dropped successfully"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def rails_pulse_installed?
|
|
48
|
+
# Check if core Rails Pulse tables exist
|
|
49
|
+
# We check for routes and requests as they are foundational tables
|
|
50
|
+
table_exists?(:rails_pulse_routes) && table_exists?(:rails_pulse_requests)
|
|
51
|
+
end
|
|
23
52
|
end
|
|
@@ -92,6 +92,37 @@ RailsPulse.configure do |config|
|
|
|
92
92
|
|
|
93
93
|
config.tags = [ "ignored", "critical", "experimental" ]
|
|
94
94
|
|
|
95
|
+
# ====================================================================================================
|
|
96
|
+
# BACKGROUND JOBS
|
|
97
|
+
# ====================================================================================================
|
|
98
|
+
# Configure background job monitoring and tracking.
|
|
99
|
+
# When enabled, Rails Pulse will track job executions, durations, failures, and retries.
|
|
100
|
+
# Supports ActiveJob, Sidekiq, and Delayed Job.
|
|
101
|
+
|
|
102
|
+
# Enable or disable background job tracking
|
|
103
|
+
config.track_jobs = false
|
|
104
|
+
|
|
105
|
+
# Thresholds for job execution times (in milliseconds)
|
|
106
|
+
config.job_thresholds = {
|
|
107
|
+
slow: 5_000, # 5 seconds
|
|
108
|
+
very_slow: 30_000, # 30 seconds
|
|
109
|
+
critical: 60_000 # 1 minute
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Job classes to ignore from tracking (by class name)
|
|
113
|
+
# Examples:
|
|
114
|
+
# config.ignored_jobs = ["ActionMailer::MailDeliveryJob", "MyApp::HealthCheckJob"]
|
|
115
|
+
config.ignored_jobs = []
|
|
116
|
+
|
|
117
|
+
# Queue names to ignore from tracking
|
|
118
|
+
# Examples:
|
|
119
|
+
# config.ignored_queues = ["low_priority", "mailers"]
|
|
120
|
+
config.ignored_queues = []
|
|
121
|
+
|
|
122
|
+
# Capture job arguments for debugging (may contain sensitive data)
|
|
123
|
+
# Set to false in production to avoid storing potentially sensitive information
|
|
124
|
+
config.capture_job_arguments = true
|
|
125
|
+
|
|
95
126
|
# ====================================================================================================
|
|
96
127
|
# DATABASE CONFIGURATION
|
|
97
128
|
# ====================================================================================================
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Adapters
|
|
3
|
+
class DelayedJobPlugin < Delayed::Plugin
|
|
4
|
+
callbacks do |lifecycle|
|
|
5
|
+
lifecycle.around(:perform) do |worker, job_data, &block|
|
|
6
|
+
next block.call(worker, job_data) unless RailsPulse.configuration.enabled
|
|
7
|
+
next block.call(worker, job_data) unless RailsPulse.configuration.track_jobs
|
|
8
|
+
|
|
9
|
+
job_wrapper = JobWrapper.new(
|
|
10
|
+
job_id: job_data.id.to_s,
|
|
11
|
+
class_name: job_data.payload_object.class.name,
|
|
12
|
+
queue_name: job_data.queue,
|
|
13
|
+
arguments: job_data.payload_object.args,
|
|
14
|
+
enqueued_at: job_data.created_at,
|
|
15
|
+
executions: job_data.attempts
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
RailsPulse::JobRunCollector.track(job_wrapper, adapter: "delayed_job") do
|
|
19
|
+
block.call(worker, job_data)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Adapters
|
|
3
|
+
class SidekiqMiddleware
|
|
4
|
+
def call(worker, job_data, queue)
|
|
5
|
+
return yield unless RailsPulse.configuration.enabled
|
|
6
|
+
return yield unless RailsPulse.configuration.track_jobs
|
|
7
|
+
|
|
8
|
+
# Create ActiveJob-like wrapper for tracking
|
|
9
|
+
job_wrapper = JobWrapper.new(
|
|
10
|
+
job_id: job_data["jid"],
|
|
11
|
+
class_name: worker.class.name,
|
|
12
|
+
queue_name: queue,
|
|
13
|
+
arguments: job_data["args"],
|
|
14
|
+
enqueued_at: Time.at(job_data["enqueued_at"] || Time.current.to_f),
|
|
15
|
+
executions: job_data["retry_count"] || 0
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
RailsPulse::JobRunCollector.track(job_wrapper, adapter: "sidekiq") do
|
|
19
|
+
yield
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class JobWrapper
|
|
25
|
+
attr_reader :job_id, :queue_name, :arguments, :enqueued_at, :executions
|
|
26
|
+
|
|
27
|
+
def initialize(job_id:, class_name:, queue_name:, arguments:, enqueued_at:, executions:)
|
|
28
|
+
@job_id = job_id
|
|
29
|
+
@class_name = class_name
|
|
30
|
+
@queue_name = queue_name
|
|
31
|
+
@arguments = arguments
|
|
32
|
+
@enqueued_at = enqueued_at
|
|
33
|
+
@executions = executions
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def class
|
|
37
|
+
OpenStruct.new(name: @class_name)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -39,9 +39,11 @@ module RailsPulse
|
|
|
39
39
|
|
|
40
40
|
# Clean up in order that respects foreign key constraints
|
|
41
41
|
@stats[:time_based][:operations] = cleanup_operations_by_time(cutoff_time)
|
|
42
|
+
@stats[:time_based][:job_runs] = cleanup_job_runs_by_time(cutoff_time)
|
|
42
43
|
@stats[:time_based][:requests] = cleanup_requests_by_time(cutoff_time)
|
|
43
44
|
@stats[:time_based][:queries] = cleanup_queries_by_time(cutoff_time)
|
|
44
45
|
@stats[:time_based][:routes] = cleanup_routes_by_time(cutoff_time)
|
|
46
|
+
@stats[:time_based][:jobs] = cleanup_jobs_by_time(cutoff_time)
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def perform_count_based_cleanup
|
|
@@ -51,9 +53,11 @@ module RailsPulse
|
|
|
51
53
|
|
|
52
54
|
# Clean up in order that respects foreign key constraints
|
|
53
55
|
@stats[:count_based][:operations] = cleanup_operations_by_count
|
|
56
|
+
@stats[:count_based][:job_runs] = cleanup_job_runs_by_count
|
|
54
57
|
@stats[:count_based][:requests] = cleanup_requests_by_count
|
|
55
58
|
@stats[:count_based][:queries] = cleanup_queries_by_count
|
|
56
59
|
@stats[:count_based][:routes] = cleanup_routes_by_count
|
|
60
|
+
@stats[:count_based][:jobs] = cleanup_jobs_by_count
|
|
57
61
|
end
|
|
58
62
|
|
|
59
63
|
# Time-based cleanup methods
|
|
@@ -110,6 +114,27 @@ module RailsPulse
|
|
|
110
114
|
count
|
|
111
115
|
end
|
|
112
116
|
|
|
117
|
+
def cleanup_job_runs_by_time(cutoff_time)
|
|
118
|
+
return 0 unless defined?(RailsPulse::JobRun)
|
|
119
|
+
|
|
120
|
+
job_runs = RailsPulse::JobRun.where("occurred_at < ?", cutoff_time)
|
|
121
|
+
count = job_runs.count
|
|
122
|
+
job_runs.delete_all
|
|
123
|
+
count
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def cleanup_jobs_by_time(cutoff_time)
|
|
127
|
+
return 0 unless defined?(RailsPulse::Job)
|
|
128
|
+
|
|
129
|
+
job_ids_with_runs = RailsPulse::JobRun.distinct.pluck(:job_id).compact
|
|
130
|
+
jobs = RailsPulse::Job
|
|
131
|
+
.where("created_at < ?", cutoff_time)
|
|
132
|
+
.where.not(id: job_ids_with_runs)
|
|
133
|
+
count = jobs.count
|
|
134
|
+
jobs.delete_all
|
|
135
|
+
count
|
|
136
|
+
end
|
|
137
|
+
|
|
113
138
|
# Count-based cleanup methods
|
|
114
139
|
def cleanup_operations_by_count
|
|
115
140
|
return 0 unless defined?(RailsPulse::Operation)
|
|
@@ -196,6 +221,46 @@ module RailsPulse
|
|
|
196
221
|
records_to_delete
|
|
197
222
|
end
|
|
198
223
|
|
|
224
|
+
def cleanup_job_runs_by_count
|
|
225
|
+
return 0 unless defined?(RailsPulse::JobRun)
|
|
226
|
+
|
|
227
|
+
max_records = @config.max_table_records[:rails_pulse_job_runs]
|
|
228
|
+
return 0 unless max_records
|
|
229
|
+
|
|
230
|
+
current_count = RailsPulse::JobRun.count
|
|
231
|
+
return 0 if current_count <= max_records
|
|
232
|
+
|
|
233
|
+
records_to_delete = current_count - max_records
|
|
234
|
+
ids_to_delete = RailsPulse::JobRun
|
|
235
|
+
.order(:occurred_at)
|
|
236
|
+
.limit(records_to_delete)
|
|
237
|
+
.pluck(:id)
|
|
238
|
+
|
|
239
|
+
RailsPulse::JobRun.where(id: ids_to_delete).delete_all
|
|
240
|
+
records_to_delete
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def cleanup_jobs_by_count
|
|
244
|
+
return 0 unless defined?(RailsPulse::Job)
|
|
245
|
+
|
|
246
|
+
max_records = @config.max_table_records[:rails_pulse_jobs]
|
|
247
|
+
return 0 unless max_records
|
|
248
|
+
|
|
249
|
+
job_ids_with_runs = RailsPulse::JobRun.distinct.pluck(:job_id).compact
|
|
250
|
+
available_jobs = RailsPulse::Job.where.not(id: job_ids_with_runs)
|
|
251
|
+
current_count = available_jobs.count
|
|
252
|
+
return 0 if current_count <= max_records
|
|
253
|
+
|
|
254
|
+
records_to_delete = current_count - max_records
|
|
255
|
+
ids_to_delete = available_jobs
|
|
256
|
+
.order(:created_at)
|
|
257
|
+
.limit(records_to_delete)
|
|
258
|
+
.pluck(:id)
|
|
259
|
+
|
|
260
|
+
RailsPulse::Job.where(id: ids_to_delete).delete_all
|
|
261
|
+
records_to_delete
|
|
262
|
+
end
|
|
263
|
+
|
|
199
264
|
def log_cleanup_summary
|
|
200
265
|
total_time_based = @stats[:time_based].values.sum
|
|
201
266
|
total_count_based = @stats[:count_based].values.sum
|