rails_pulse 0.1.1 → 0.1.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 +79 -177
- data/Rakefile +77 -2
- data/app/assets/images/rails_pulse/dashboard.png +0 -0
- data/app/assets/images/rails_pulse/request.png +0 -0
- data/app/assets/stylesheets/rails_pulse/application.css +28 -17
- data/app/assets/stylesheets/rails_pulse/components/badge.css +13 -0
- data/app/assets/stylesheets/rails_pulse/components/base.css +12 -2
- data/app/assets/stylesheets/rails_pulse/components/collapsible.css +30 -0
- data/app/assets/stylesheets/rails_pulse/components/popover.css +0 -1
- data/app/assets/stylesheets/rails_pulse/components/row.css +55 -3
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +23 -0
- data/app/controllers/concerns/chart_table_concern.rb +21 -4
- data/app/controllers/concerns/response_range_concern.rb +6 -3
- data/app/controllers/concerns/time_range_concern.rb +5 -10
- data/app/controllers/concerns/zoom_range_concern.rb +32 -1
- data/app/controllers/rails_pulse/application_controller.rb +13 -5
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -0
- data/app/controllers/rails_pulse/queries_controller.rb +111 -51
- data/app/controllers/rails_pulse/requests_controller.rb +37 -12
- data/app/controllers/rails_pulse/routes_controller.rb +98 -24
- data/app/helpers/rails_pulse/application_helper.rb +0 -1
- data/app/helpers/rails_pulse/chart_formatters.rb +3 -3
- data/app/helpers/rails_pulse/chart_helper.rb +21 -9
- data/app/helpers/rails_pulse/status_helper.rb +10 -4
- data/app/javascript/rails_pulse/application.js +34 -3
- data/app/javascript/rails_pulse/controllers/collapsible_controller.js +32 -0
- data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +2 -1
- data/app/javascript/rails_pulse/controllers/expandable_rows_controller.js +58 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +353 -39
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +17 -27
- data/app/javascript/rails_pulse/controllers/popover_controller.js +28 -4
- data/app/javascript/rails_pulse/controllers/table_sort_controller.js +14 -0
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +41 -0
- data/app/jobs/rails_pulse/summary_job.rb +53 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +28 -7
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +18 -22
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +18 -7
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +34 -41
- data/app/models/rails_pulse/operation.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +49 -25
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +40 -28
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +37 -43
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +23 -97
- data/app/models/rails_pulse/queries/tables/index.rb +74 -0
- data/app/models/rails_pulse/query.rb +47 -0
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +23 -84
- data/app/models/rails_pulse/route.rb +1 -6
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +45 -25
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +43 -45
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +36 -44
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +37 -27
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +23 -100
- data/app/models/rails_pulse/routes/tables/index.rb +57 -40
- data/app/models/rails_pulse/summary.rb +143 -0
- data/app/services/rails_pulse/analysis/backtrace_analyzer.rb +256 -0
- data/app/services/rails_pulse/analysis/base_analyzer.rb +67 -0
- data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +206 -0
- data/app/services/rails_pulse/analysis/index_recommendation_engine.rb +326 -0
- data/app/services/rails_pulse/analysis/n_plus_one_detector.rb +241 -0
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +146 -0
- data/app/services/rails_pulse/analysis/suggestion_generator.rb +217 -0
- data/app/services/rails_pulse/query_analysis_service.rb +125 -0
- data/app/services/rails_pulse/summary_service.rb +199 -0
- data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +0 -1
- data/app/views/layouts/rails_pulse/application.html.erb +4 -6
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +1 -1
- data/app/views/rails_pulse/components/_code_panel.html.erb +17 -3
- data/app/views/rails_pulse/components/_empty_state.html.erb +11 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +37 -28
- data/app/views/rails_pulse/components/_panel.html.erb +1 -1
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +5 -7
- data/app/views/rails_pulse/components/_table_head.html.erb +6 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +55 -37
- data/app/views/rails_pulse/operations/show.html.erb +17 -15
- data/app/views/rails_pulse/queries/_analysis_error.html.erb +15 -0
- data/app/views/rails_pulse/queries/_analysis_prompt.html.erb +27 -0
- data/app/views/rails_pulse/queries/_analysis_results.html.erb +87 -0
- data/app/views/rails_pulse/queries/_analysis_section.html.erb +39 -0
- data/app/views/rails_pulse/queries/_show_table.html.erb +2 -2
- data/app/views/rails_pulse/queries/_table.html.erb +11 -13
- data/app/views/rails_pulse/queries/index.html.erb +32 -28
- data/app/views/rails_pulse/queries/show.html.erb +45 -34
- data/app/views/rails_pulse/requests/_operations.html.erb +38 -45
- data/app/views/rails_pulse/requests/_table.html.erb +3 -3
- data/app/views/rails_pulse/requests/index.html.erb +33 -28
- data/app/views/rails_pulse/routes/_table.html.erb +14 -14
- data/app/views/rails_pulse/routes/index.html.erb +34 -29
- data/app/views/rails_pulse/routes/show.html.erb +43 -36
- data/config/initializers/rails_pulse.rb +0 -12
- data/config/routes.rb +5 -1
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +54 -0
- data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +13 -0
- data/db/rails_pulse_schema.rb +130 -0
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +65 -0
- data/lib/generators/rails_pulse/install_generator.rb +94 -4
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +60 -0
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +22 -0
- data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +19 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +0 -12
- data/lib/generators/rails_pulse/upgrade_generator.rb +225 -0
- data/lib/rails_pulse/configuration.rb +0 -11
- data/lib/rails_pulse/engine.rb +0 -1
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/tasks/rails_pulse.rake +77 -0
- 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 +53 -53
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- data/public/rails-pulse-assets/search.svg +43 -0
- metadata +48 -14
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/images/rails_pulse/routes.png +0 -0
- data/app/controllers/rails_pulse/caches_controller.rb +0 -115
- data/app/helpers/rails_pulse/cached_component_helper.rb +0 -73
- data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +0 -67
- data/app/models/rails_pulse/component_cache_key.rb +0 -33
- data/app/views/rails_pulse/caches/show.html.erb +0 -9
- data/db/migrate/20250227235904_create_routes.rb +0 -12
- data/db/migrate/20250227235915_create_requests.rb +0 -19
- data/db/migrate/20250228000000_create_queries.rb +0 -14
- data/db/migrate/20250228000056_create_operations.rb +0 -24
- data/lib/rails_pulse/migration.rb +0 -29
@@ -0,0 +1,54 @@
|
|
1
|
+
class CreateRailsPulseSummaries < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
create_table :rails_pulse_summaries do |t|
|
4
|
+
# Time fields
|
5
|
+
t.datetime :period_start, null: false
|
6
|
+
t.datetime :period_end, null: false
|
7
|
+
t.string :period_type, null: false # 'hour', 'day', 'week', 'month'
|
8
|
+
|
9
|
+
# Polymorphic association to handle both routes and queries
|
10
|
+
t.references :summarizable, polymorphic: true, null: false, index: true
|
11
|
+
# This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
|
12
|
+
# and summarizable_id (route_id or query_id)
|
13
|
+
|
14
|
+
# Universal metrics
|
15
|
+
t.integer :count, default: 0, null: false
|
16
|
+
t.float :avg_duration
|
17
|
+
t.float :min_duration
|
18
|
+
t.float :max_duration
|
19
|
+
t.float :p50_duration
|
20
|
+
t.float :p95_duration
|
21
|
+
t.float :p99_duration
|
22
|
+
t.float :total_duration
|
23
|
+
t.float :stddev_duration
|
24
|
+
|
25
|
+
# Request/Route specific metrics
|
26
|
+
t.integer :error_count, default: 0
|
27
|
+
t.integer :success_count, default: 0
|
28
|
+
t.integer :status_2xx, default: 0
|
29
|
+
t.integer :status_3xx, default: 0
|
30
|
+
t.integer :status_4xx, default: 0
|
31
|
+
t.integer :status_5xx, default: 0
|
32
|
+
|
33
|
+
t.timestamps
|
34
|
+
|
35
|
+
# Unique constraint and indexes
|
36
|
+
t.index [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
|
37
|
+
unique: true,
|
38
|
+
name: 'idx_pulse_summaries_unique'
|
39
|
+
t.index [ :period_type, :period_start ]
|
40
|
+
t.index :created_at
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add indexes to existing tables for efficient aggregation
|
44
|
+
add_index :rails_pulse_requests, [ :created_at, :route_id ],
|
45
|
+
name: 'idx_requests_for_aggregation'
|
46
|
+
add_index :rails_pulse_requests, :created_at,
|
47
|
+
name: 'idx_requests_created_at'
|
48
|
+
|
49
|
+
add_index :rails_pulse_operations, [ :created_at, :query_id ],
|
50
|
+
name: 'idx_operations_for_aggregation'
|
51
|
+
add_index :rails_pulse_operations, :created_at,
|
52
|
+
name: 'idx_operations_created_at'
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class AddAnalysisToRailsPulseQueries < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
add_column :rails_pulse_queries, :analyzed_at, :datetime, comment: "When query analysis was last performed"
|
4
|
+
add_column :rails_pulse_queries, :explain_plan, :text, comment: "EXPLAIN output from actual SQL execution"
|
5
|
+
add_column :rails_pulse_queries, :issues, :text, comment: "JSON array of detected performance issues"
|
6
|
+
add_column :rails_pulse_queries, :metadata, :text, comment: "JSON object containing query complexity metrics"
|
7
|
+
add_column :rails_pulse_queries, :query_stats, :text, comment: "JSON object with query characteristics analysis"
|
8
|
+
add_column :rails_pulse_queries, :backtrace_analysis, :text, comment: "JSON object with call chain and N+1 detection"
|
9
|
+
add_column :rails_pulse_queries, :suggestions, :text, comment: "JSON array of optimization recommendations"
|
10
|
+
add_column :rails_pulse_queries, :index_recommendations, :text, comment: "JSON array of database index recommendations"
|
11
|
+
add_column :rails_pulse_queries, :n_plus_one_analysis, :text, comment: "JSON object with enhanced N+1 query detection results"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# Rails Pulse Database Schema
|
2
|
+
# This file contains the complete schema for Rails Pulse tables
|
3
|
+
# Load with: rails db:schema:load:rails_pulse or db:prepare
|
4
|
+
|
5
|
+
RailsPulse::Schema = lambda do |connection|
|
6
|
+
# 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
|
+
|
9
|
+
if ENV["CI"] == "true"
|
10
|
+
existing_tables = required_tables.select { |table| connection.table_exists?(table) }
|
11
|
+
missing_tables = required_tables - existing_tables
|
12
|
+
puts "[RailsPulse::Schema] Existing tables: #{existing_tables.join(', ')}" if existing_tables.any?
|
13
|
+
puts "[RailsPulse::Schema] Missing tables: #{missing_tables.join(', ')}" if missing_tables.any?
|
14
|
+
end
|
15
|
+
|
16
|
+
return if required_tables.all? { |table| connection.table_exists?(table) }
|
17
|
+
|
18
|
+
connection.create_table :rails_pulse_routes do |t|
|
19
|
+
t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
|
20
|
+
t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
|
24
|
+
connection.add_index :rails_pulse_routes, [ :method, :path ], unique: true, name: "index_rails_pulse_routes_on_method_and_path"
|
25
|
+
|
26
|
+
connection.create_table :rails_pulse_queries do |t|
|
27
|
+
t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
|
28
|
+
t.datetime :analyzed_at, comment: "When query analysis was last performed"
|
29
|
+
t.text :explain_plan, comment: "EXPLAIN output from actual SQL execution"
|
30
|
+
t.text :issues, comment: "JSON array of detected performance issues"
|
31
|
+
t.text :metadata, comment: "JSON object containing query complexity metrics"
|
32
|
+
t.text :query_stats, comment: "JSON object with query characteristics analysis"
|
33
|
+
t.text :backtrace_analysis, comment: "JSON object with call chain and N+1 detection"
|
34
|
+
t.text :index_recommendations, comment: "JSON array of database index recommendations"
|
35
|
+
t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
|
36
|
+
t.text :suggestions, comment: "JSON array of optimization recommendations"
|
37
|
+
t.timestamps
|
38
|
+
end
|
39
|
+
|
40
|
+
connection.add_index :rails_pulse_queries, :normalized_sql, unique: true, name: "index_rails_pulse_queries_on_normalized_sql", length: 191
|
41
|
+
|
42
|
+
connection.create_table :rails_pulse_requests do |t|
|
43
|
+
t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
|
44
|
+
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
|
45
|
+
t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
|
46
|
+
t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
|
47
|
+
t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
|
48
|
+
t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
|
49
|
+
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
50
|
+
t.timestamps
|
51
|
+
end
|
52
|
+
|
53
|
+
connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
|
54
|
+
connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
|
55
|
+
connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
|
56
|
+
|
57
|
+
connection.create_table :rails_pulse_operations do |t|
|
58
|
+
t.references :request, null: false, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
|
59
|
+
t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
|
60
|
+
t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
|
61
|
+
t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
|
62
|
+
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
|
63
|
+
t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
|
64
|
+
t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
|
65
|
+
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
66
|
+
t.timestamps
|
67
|
+
end
|
68
|
+
|
69
|
+
connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
|
70
|
+
connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
|
71
|
+
connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
|
72
|
+
connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
|
73
|
+
connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
|
74
|
+
|
75
|
+
connection.create_table :rails_pulse_summaries do |t|
|
76
|
+
# Time fields
|
77
|
+
t.datetime :period_start, null: false, comment: "Start of the aggregation period"
|
78
|
+
t.datetime :period_end, null: false, comment: "End of the aggregation period"
|
79
|
+
t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
|
80
|
+
|
81
|
+
# Polymorphic association to handle both routes and queries
|
82
|
+
t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
|
83
|
+
# This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
|
84
|
+
# and summarizable_id (route_id or query_id)
|
85
|
+
|
86
|
+
# Universal metrics
|
87
|
+
t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
|
88
|
+
t.float :avg_duration, comment: "Average duration in milliseconds"
|
89
|
+
t.float :min_duration, comment: "Minimum duration in milliseconds"
|
90
|
+
t.float :max_duration, comment: "Maximum duration in milliseconds"
|
91
|
+
t.float :p50_duration, comment: "50th percentile duration"
|
92
|
+
t.float :p95_duration, comment: "95th percentile duration"
|
93
|
+
t.float :p99_duration, comment: "99th percentile duration"
|
94
|
+
t.float :total_duration, comment: "Total duration in milliseconds"
|
95
|
+
t.float :stddev_duration, comment: "Standard deviation of duration"
|
96
|
+
|
97
|
+
# Request/Route specific metrics
|
98
|
+
t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
|
99
|
+
t.integer :success_count, default: 0, comment: "Number of successful responses"
|
100
|
+
t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
|
101
|
+
t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
|
102
|
+
t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
|
103
|
+
t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
|
104
|
+
|
105
|
+
t.timestamps
|
106
|
+
end
|
107
|
+
|
108
|
+
# Unique constraint and indexes for summaries
|
109
|
+
connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
|
110
|
+
unique: true,
|
111
|
+
name: "idx_pulse_summaries_unique"
|
112
|
+
connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
|
113
|
+
connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
|
114
|
+
|
115
|
+
# Add indexes to existing tables for efficient aggregation
|
116
|
+
connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
|
117
|
+
connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
|
118
|
+
|
119
|
+
connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
|
120
|
+
connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
|
121
|
+
|
122
|
+
if ENV["CI"] == "true"
|
123
|
+
created_tables = required_tables.select { |table| connection.table_exists?(table) }
|
124
|
+
puts "[RailsPulse::Schema] Successfully created tables: #{created_tables.join(', ')}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
if defined?(RailsPulse::ApplicationRecord)
|
129
|
+
RailsPulse::Schema.call(RailsPulse::ApplicationRecord.connection)
|
130
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module Generators
|
3
|
+
class ConvertToMigrationsGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
source_root File.expand_path("templates", __dir__)
|
6
|
+
|
7
|
+
desc "Convert Rails Pulse schema file to migrations for single database setup"
|
8
|
+
|
9
|
+
def self.next_migration_number(path)
|
10
|
+
next_migration_number = current_migration_number(path) + 1
|
11
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_schema_file
|
15
|
+
unless File.exist?("db/rails_pulse_schema.rb")
|
16
|
+
say "No db/rails_pulse_schema.rb file found. Run 'rails generate rails_pulse:install' first.", :red
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
20
|
+
if rails_pulse_tables_exist?
|
21
|
+
say "Rails Pulse tables already exist. No conversion needed.", :yellow
|
22
|
+
say "Use 'rails generate rails_pulse:upgrade' to update existing installation.", :blue
|
23
|
+
exit 0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_conversion_migration
|
28
|
+
say "Converting db/rails_pulse_schema.rb to migration...", :green
|
29
|
+
|
30
|
+
migration_template(
|
31
|
+
"migrations/install_rails_pulse_tables.rb",
|
32
|
+
"db/migrate/install_rails_pulse_tables.rb"
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def display_completion_message
|
37
|
+
say <<~MESSAGE
|
38
|
+
|
39
|
+
Conversion complete!
|
40
|
+
|
41
|
+
Next steps:
|
42
|
+
1. Run: rails db:migrate
|
43
|
+
2. Delete: db/rails_pulse_schema.rb (no longer needed)
|
44
|
+
3. Remove db/rails_pulse_migrate/ directory if it exists
|
45
|
+
4. Restart your Rails server
|
46
|
+
|
47
|
+
Future Rails Pulse updates will come as regular migrations.
|
48
|
+
|
49
|
+
MESSAGE
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def rails_pulse_tables_exist?
|
55
|
+
return false unless defined?(ActiveRecord::Base)
|
56
|
+
|
57
|
+
connection = ActiveRecord::Base.connection
|
58
|
+
%w[rails_pulse_routes rails_pulse_queries rails_pulse_requests rails_pulse_operations rails_pulse_summaries]
|
59
|
+
.all? { |table| connection.table_exists?(table) }
|
60
|
+
rescue
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -1,17 +1,107 @@
|
|
1
1
|
module RailsPulse
|
2
2
|
module Generators
|
3
3
|
class InstallGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
4
5
|
source_root File.expand_path("templates", __dir__)
|
5
6
|
|
6
|
-
desc "
|
7
|
-
|
8
|
-
|
7
|
+
desc "Install Rails Pulse with flexible database setup options"
|
8
|
+
|
9
|
+
class_option :database, type: :string, default: "single",
|
10
|
+
desc: "Database setup: 'single' (default) or 'separate'"
|
11
|
+
|
12
|
+
def self.next_migration_number(path)
|
13
|
+
next_migration_number = current_migration_number(path) + 1
|
14
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
15
|
+
end
|
16
|
+
|
17
|
+
def copy_schema
|
18
|
+
copy_file "db/rails_pulse_schema.rb", "db/rails_pulse_schema.rb"
|
9
19
|
end
|
10
20
|
|
11
|
-
desc "Copies Rails Pulse example configuration file to the application."
|
12
21
|
def copy_initializer
|
13
22
|
copy_file "rails_pulse.rb", "config/initializers/rails_pulse.rb"
|
14
23
|
end
|
24
|
+
|
25
|
+
def setup_database_configuration
|
26
|
+
if separate_database?
|
27
|
+
create_separate_database_setup
|
28
|
+
else
|
29
|
+
create_single_database_setup
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def display_post_install_message
|
34
|
+
if separate_database?
|
35
|
+
display_separate_database_message
|
36
|
+
else
|
37
|
+
display_single_database_message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def separate_database?
|
44
|
+
options[:database] == "separate"
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_separate_database_setup
|
48
|
+
create_file "db/rails_pulse_migrate/.keep"
|
49
|
+
|
50
|
+
say "Setting up separate database configuration...", :green
|
51
|
+
|
52
|
+
# Could add database.yml configuration here if needed
|
53
|
+
# For now, users will configure manually
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_single_database_setup
|
57
|
+
say "Setting up single database configuration...", :green
|
58
|
+
|
59
|
+
# Create a migration that loads the schema
|
60
|
+
migration_template(
|
61
|
+
"migrations/install_rails_pulse_tables.rb",
|
62
|
+
"db/migrate/install_rails_pulse_tables.rb"
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def display_separate_database_message
|
67
|
+
say <<~MESSAGE
|
68
|
+
|
69
|
+
Rails Pulse installation complete! (Separate Database Setup)
|
70
|
+
|
71
|
+
Next steps:
|
72
|
+
1. Add Rails Pulse database configuration to config/database.yml:
|
73
|
+
|
74
|
+
#{Rails.env}:
|
75
|
+
rails_pulse:
|
76
|
+
<<: *default
|
77
|
+
database: storage/#{Rails.env}_rails_pulse.sqlite3
|
78
|
+
migrations_paths: db/rails_pulse_migrate
|
79
|
+
|
80
|
+
2. Run: rails db:prepare (creates database and loads schema)
|
81
|
+
3. Restart your Rails server
|
82
|
+
|
83
|
+
Future schema changes will come as regular migrations in db/rails_pulse_migrate/
|
84
|
+
|
85
|
+
MESSAGE
|
86
|
+
end
|
87
|
+
|
88
|
+
def display_single_database_message
|
89
|
+
say <<~MESSAGE
|
90
|
+
|
91
|
+
Rails Pulse installation complete! (Single Database Setup)
|
92
|
+
|
93
|
+
Next steps:
|
94
|
+
1. Run: rails db:migrate (creates Rails Pulse tables in your main database)
|
95
|
+
2. Delete: db/rails_pulse_schema.rb (no longer needed)
|
96
|
+
3. Restart your Rails server
|
97
|
+
|
98
|
+
Future schema changes will come as regular migrations in db/migrate/
|
99
|
+
|
100
|
+
Note: The installation migration was created from db/rails_pulse_schema.rb
|
101
|
+
and includes all current Rails Pulse tables and columns.
|
102
|
+
|
103
|
+
MESSAGE
|
104
|
+
end
|
15
105
|
end
|
16
106
|
end
|
17
107
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Rails Pulse Database Schema
|
2
|
+
# This file contains the complete schema for Rails Pulse tables
|
3
|
+
# Load with: rails db:schema:load:rails_pulse or db:prepare
|
4
|
+
|
5
|
+
RailsPulse::Schema = lambda do |connection|
|
6
|
+
# Skip if tables already exist to prevent conflicts
|
7
|
+
return if connection.table_exists?(:rails_pulse_routes)
|
8
|
+
|
9
|
+
connection.create_table :rails_pulse_routes do |t|
|
10
|
+
t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
|
11
|
+
t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
connection.add_index :rails_pulse_routes, [ :method, :path ], unique: true, name: "index_rails_pulse_routes_on_method_and_path"
|
16
|
+
|
17
|
+
connection.create_table :rails_pulse_queries do |t|
|
18
|
+
t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
|
19
|
+
t.timestamps
|
20
|
+
end
|
21
|
+
|
22
|
+
connection.add_index :rails_pulse_queries, :normalized_sql, unique: true, name: "index_rails_pulse_queries_on_normalized_sql", length: 191
|
23
|
+
|
24
|
+
connection.create_table :rails_pulse_requests do |t|
|
25
|
+
t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
|
26
|
+
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
|
27
|
+
t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
|
28
|
+
t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
|
29
|
+
t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
|
30
|
+
t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
|
31
|
+
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
32
|
+
t.timestamps
|
33
|
+
end
|
34
|
+
|
35
|
+
connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
|
36
|
+
connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
|
37
|
+
connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
|
38
|
+
|
39
|
+
connection.create_table :rails_pulse_operations do |t|
|
40
|
+
t.references :request, null: false, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
|
41
|
+
t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
|
42
|
+
t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
|
43
|
+
t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
|
44
|
+
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
|
45
|
+
t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
|
46
|
+
t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
|
47
|
+
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
48
|
+
t.timestamps
|
49
|
+
end
|
50
|
+
|
51
|
+
connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
|
52
|
+
connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
|
53
|
+
connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
|
54
|
+
connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
|
55
|
+
connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
|
56
|
+
end
|
57
|
+
|
58
|
+
if defined?(RailsPulse::ApplicationRecord)
|
59
|
+
RailsPulse::Schema.call(RailsPulse::ApplicationRecord.connection)
|
60
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Generated from Rails Pulse schema - automatically loads current schema definition
|
2
|
+
class InstallRailsPulseTables < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
3
|
+
def change
|
4
|
+
# Load and execute the Rails Pulse schema directly
|
5
|
+
# This ensures the migration is always in sync with the schema file
|
6
|
+
schema_file = File.join(::Rails.root.to_s, "db/rails_pulse_schema.rb")
|
7
|
+
|
8
|
+
if File.exist?(schema_file)
|
9
|
+
say "Loading Rails Pulse schema from db/rails_pulse_schema.rb"
|
10
|
+
|
11
|
+
# Load the schema file to define RailsPulse::Schema
|
12
|
+
load schema_file
|
13
|
+
|
14
|
+
# Execute the schema in the context of this migration
|
15
|
+
RailsPulse::Schema.call(connection)
|
16
|
+
|
17
|
+
say "Rails Pulse tables created successfully"
|
18
|
+
else
|
19
|
+
raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Upgrade Rails Pulse tables with new features
|
2
|
+
class UpgradeRailsPulseTables < ActiveRecord::Migration[<%= @migration_version %>]
|
3
|
+
def change
|
4
|
+
<% @missing_columns.each do |table_name, columns| %>
|
5
|
+
<% columns.each do |column_name, definition| %>
|
6
|
+
# Add <%= column_name %> column to <%= table_name %>
|
7
|
+
add_column :<%= table_name %>, :<%= column_name %>, :<%= definition[:type] %><% if definition[:comment] %>, comment: "<%= definition[:comment] %>"<% end %>
|
8
|
+
<% end %>
|
9
|
+
<% end %>
|
10
|
+
end
|
11
|
+
|
12
|
+
def down
|
13
|
+
<% @missing_columns.each do |table_name, columns| %>
|
14
|
+
<% columns.each do |column_name, _definition| %>
|
15
|
+
remove_column :<%= table_name %>, :<%= column_name %>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
end
|
19
|
+
end
|
@@ -71,18 +71,6 @@ RailsPulse.configure do |config|
|
|
71
71
|
config.ignored_requests = []
|
72
72
|
config.ignored_queries = []
|
73
73
|
|
74
|
-
# ====================================================================================================
|
75
|
-
# CACHING
|
76
|
-
# ====================================================================================================
|
77
|
-
# Configure metric card caching to improve performance of the Rails Pulse dashboard.
|
78
|
-
# Caching reduces database load for expensive metric calculations.
|
79
|
-
|
80
|
-
# Enable/disable metric card caching
|
81
|
-
config.component_cache_enabled = true
|
82
|
-
|
83
|
-
# How long to cache metric card results
|
84
|
-
config.component_cache_duration = 1.hour
|
85
|
-
|
86
74
|
# ====================================================================================================
|
87
75
|
# DATABASE CONFIGURATION
|
88
76
|
# ====================================================================================================
|