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
@@ -7,60 +7,52 @@ module RailsPulse
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def to_metric_card
|
10
|
-
|
11
|
-
|
12
|
-
else
|
13
|
-
RailsPulse::Request.all
|
14
|
-
end
|
10
|
+
last_7_days = 7.days.ago.beginning_of_day
|
11
|
+
previous_7_days = 14.days.ago.beginning_of_day
|
15
12
|
|
16
|
-
|
13
|
+
# Single query to get all P95 metrics with conditional aggregation
|
14
|
+
base_query = RailsPulse::Summary.where(
|
15
|
+
summarizable_type: "RailsPulse::Route",
|
16
|
+
period_type: "day",
|
17
|
+
period_start: 2.weeks.ago.beginning_of_day..Time.current
|
18
|
+
)
|
19
|
+
base_query = base_query.where(summarizable_id: @route.id) if @route
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
0
|
24
|
-
end
|
21
|
+
metrics = base_query.select(
|
22
|
+
"AVG(p95_duration) AS overall_p95",
|
23
|
+
"AVG(CASE WHEN period_start >= '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN p95_duration ELSE NULL END) AS current_p95",
|
24
|
+
"AVG(CASE WHEN period_start >= '#{previous_7_days.strftime('%Y-%m-%d %H:%M:%S')}' AND period_start < '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN p95_duration ELSE NULL END) AS previous_p95"
|
25
|
+
).take
|
25
26
|
|
26
|
-
# Calculate
|
27
|
-
|
28
|
-
|
27
|
+
# Calculate metrics from single query result
|
28
|
+
p95_response_time = (metrics.overall_p95 || 0).round(0)
|
29
|
+
current_period_p95 = metrics.current_p95 || 0
|
30
|
+
previous_period_p95 = metrics.previous_p95 || 0
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
current_period.select("duration").order("duration").limit(1).offset((current_count * 0.95).floor).pluck(:duration).first || 0
|
34
|
-
else
|
35
|
-
0
|
36
|
-
end
|
32
|
+
percentage = previous_period_p95.zero? ? 0 : ((previous_period_p95 - current_period_p95) / previous_period_p95 * 100).abs.round(1)
|
33
|
+
trend_icon = percentage < 0.1 ? "move-right" : current_period_p95 < previous_period_p95 ? "trending-down" : "trending-up"
|
34
|
+
trend_amount = previous_period_p95.zero? ? "0%" : "#{percentage}%"
|
37
35
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
else
|
43
|
-
0
|
44
|
-
end
|
36
|
+
# Sparkline data by day with zero-filled days over the last 14 days
|
37
|
+
grouped_daily = base_query
|
38
|
+
.group_by_day(:period_start, time_zone: "UTC")
|
39
|
+
.average(:p95_duration)
|
45
40
|
|
46
|
-
|
47
|
-
|
48
|
-
trend_amount = previous_period_95th.zero? ? "0%" : "#{percentage}%"
|
41
|
+
start_day = 2.weeks.ago.beginning_of_day.to_date
|
42
|
+
end_day = Time.current.to_date
|
49
43
|
|
50
|
-
sparkline_data =
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
hash[formatted_date] = {
|
57
|
-
value: value
|
58
|
-
}
|
59
|
-
end
|
44
|
+
sparkline_data = {}
|
45
|
+
(start_day..end_day).each do |day|
|
46
|
+
avg = grouped_daily[day]&.round(0) || 0
|
47
|
+
label = day.strftime("%b %-d")
|
48
|
+
sparkline_data[label] = { value: avg }
|
49
|
+
end
|
60
50
|
|
61
51
|
{
|
52
|
+
id: "percentile_response_times",
|
53
|
+
context: "routes",
|
62
54
|
title: "95th Percentile Response Time",
|
63
|
-
summary: "#{
|
55
|
+
summary: "#{p95_response_time} ms",
|
64
56
|
line_chart_data: sparkline_data,
|
65
57
|
trend_icon: trend_icon,
|
66
58
|
trend_amount: trend_amount,
|
@@ -7,44 +7,54 @@ module RailsPulse
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def to_metric_card
|
10
|
-
|
11
|
-
|
12
|
-
else
|
13
|
-
RailsPulse::Request.all
|
14
|
-
end
|
10
|
+
last_7_days = 7.days.ago.beginning_of_day
|
11
|
+
previous_7_days = 14.days.ago.beginning_of_day
|
15
12
|
|
16
|
-
|
13
|
+
# Single query to get all count metrics with conditional aggregation
|
14
|
+
base_query = RailsPulse::Summary.where(
|
15
|
+
summarizable_type: "RailsPulse::Route",
|
16
|
+
period_type: "day",
|
17
|
+
period_start: 2.weeks.ago.beginning_of_day..Time.current
|
18
|
+
)
|
19
|
+
base_query = base_query.where(summarizable_id: @route.id) if @route
|
17
20
|
|
18
|
-
|
19
|
-
|
21
|
+
metrics = base_query.select(
|
22
|
+
"SUM(count) AS total_count",
|
23
|
+
"SUM(CASE WHEN period_start >= '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN count ELSE 0 END) AS current_count",
|
24
|
+
"SUM(CASE WHEN period_start >= '#{previous_7_days.strftime('%Y-%m-%d %H:%M:%S')}' AND period_start < '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN count ELSE 0 END) AS previous_count"
|
25
|
+
).take
|
20
26
|
|
21
|
-
# Calculate
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
previous_period_count = requests.where("occurred_at >= ? AND occurred_at < ?", previous_7_days, last_7_days).count
|
27
|
+
# Calculate metrics from single query result
|
28
|
+
total_request_count = metrics.total_count || 0
|
29
|
+
current_period_count = metrics.current_count || 0
|
30
|
+
previous_period_count = metrics.previous_count || 0
|
26
31
|
|
27
32
|
percentage = previous_period_count.zero? ? 0 : ((previous_period_count - current_period_count) / previous_period_count.to_f * 100).abs.round(1)
|
28
33
|
trend_icon = percentage < 0.1 ? "move-right" : current_period_count < previous_period_count ? "trending-down" : "trending-up"
|
29
34
|
trend_amount = previous_period_count.zero? ? "0%" : "#{percentage}%"
|
30
35
|
|
31
|
-
|
32
|
-
|
33
|
-
.
|
34
|
-
.
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
36
|
+
# Sparkline data by day with zero-filled days over the last 14 days
|
37
|
+
grouped_daily = base_query
|
38
|
+
.group_by_day(:period_start, time_zone: "UTC")
|
39
|
+
.sum(:count)
|
40
|
+
|
41
|
+
start_day = 2.weeks.ago.beginning_of_day.to_date
|
42
|
+
end_day = Time.current.to_date
|
43
|
+
|
44
|
+
sparkline_data = {}
|
45
|
+
(start_day..end_day).each do |day|
|
46
|
+
total = grouped_daily[day] || 0
|
47
|
+
label = day.strftime("%b %-d")
|
48
|
+
sparkline_data[label] = { value: total }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Calculate average requests per minute over 2-week period
|
52
|
+
total_minutes = 2.weeks / 1.minute
|
45
53
|
average_requests_per_minute = total_request_count / total_minutes
|
46
54
|
|
47
55
|
{
|
56
|
+
id: "request_count_totals",
|
57
|
+
context: "routes",
|
48
58
|
title: "Request Count Total",
|
49
59
|
summary: "#{average_requests_per_minute.round(2)} / min",
|
50
60
|
line_chart_data: sparkline_data,
|
@@ -2,112 +2,35 @@ module RailsPulse
|
|
2
2
|
module Routes
|
3
3
|
module Charts
|
4
4
|
class AverageResponseTimes
|
5
|
-
def initialize(ransack_query:,
|
5
|
+
def initialize(ransack_query:, period_type: nil, route: nil, start_time: nil, end_time: nil, start_duration: nil)
|
6
6
|
@ransack_query = ransack_query
|
7
|
-
@
|
7
|
+
@period_type = period_type
|
8
8
|
@route = route
|
9
|
+
@start_time = start_time
|
10
|
+
@end_time = end_time
|
11
|
+
@start_duration = start_duration
|
9
12
|
end
|
10
13
|
|
11
14
|
def to_rails_chart
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
.average("rails_pulse_requests.duration")
|
30
|
-
end
|
31
|
-
|
32
|
-
# Create full time range and fill in missing periods
|
33
|
-
fill_missing_periods(actual_data)
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def fill_missing_periods(actual_data)
|
39
|
-
# Extract actual time range from ransack query conditions
|
40
|
-
start_time, end_time = extract_time_range_from_ransack
|
41
|
-
|
42
|
-
# Create time range based on grouping type
|
43
|
-
case @group_by
|
44
|
-
when :group_by_hour
|
45
|
-
time_range = generate_hour_range(start_time, end_time)
|
46
|
-
else # :group_by_day
|
47
|
-
time_range = generate_day_range(start_time, end_time)
|
48
|
-
end
|
49
|
-
|
50
|
-
# Fill in all periods with zero values for missing periods
|
51
|
-
time_range.each_with_object({}) do |period, result|
|
52
|
-
occurred_at = period.is_a?(String) ? Time.parse(period) : period
|
53
|
-
occurred_at = (occurred_at.is_a?(Time) || occurred_at.is_a?(Date)) ? occurred_at : Time.current
|
54
|
-
|
55
|
-
normalized_occurred_at =
|
56
|
-
case @group_by
|
57
|
-
when :group_by_hour
|
58
|
-
occurred_at&.beginning_of_hour || occurred_at
|
59
|
-
when :group_by_day
|
60
|
-
occurred_at&.beginning_of_day || occurred_at
|
61
|
-
else
|
62
|
-
occurred_at
|
63
|
-
end
|
64
|
-
|
65
|
-
# Use actual data if available, otherwise default to 0
|
66
|
-
average_duration = actual_data[period] || 0
|
67
|
-
result[normalized_occurred_at.to_i] = {
|
68
|
-
value: average_duration.to_f
|
69
|
-
}
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def generate_day_range(start_time, end_time)
|
74
|
-
(start_time.to_date..end_time.to_date).map(&:beginning_of_day)
|
75
|
-
end
|
76
|
-
|
77
|
-
def generate_hour_range(start_time, end_time)
|
78
|
-
current = start_time
|
79
|
-
hours = []
|
80
|
-
while current <= end_time
|
81
|
-
hours << current
|
82
|
-
current += 1.hour
|
83
|
-
end
|
84
|
-
hours
|
85
|
-
end
|
86
|
-
|
87
|
-
def extract_time_range_from_ransack
|
88
|
-
# Extract time range from ransack conditions
|
89
|
-
conditions = @ransack_query.conditions
|
90
|
-
|
91
|
-
if @route
|
92
|
-
# For specific route queries, look for occurred_at conditions
|
93
|
-
start_condition = conditions.find { |c| c.a.first == "occurred_at" && c.p == "gteq" }
|
94
|
-
end_condition = conditions.find { |c| c.a.first == "occurred_at" && c.p == "lt" }
|
95
|
-
else
|
96
|
-
# For general route queries, look for requests_occurred_at conditions
|
97
|
-
start_condition = conditions.find { |c| c.a.first == "requests_occurred_at" && c.p == "gteq" }
|
98
|
-
end_condition = conditions.find { |c| c.a.first == "requests_occurred_at" && c.p == "lt" }
|
99
|
-
end
|
100
|
-
|
101
|
-
start_time = start_condition&.v || 2.weeks.ago
|
102
|
-
end_time = end_condition&.v || Time.current
|
103
|
-
|
104
|
-
# Normalize time boundaries based on grouping
|
105
|
-
case @group_by
|
106
|
-
when :group_by_hour
|
107
|
-
[ start_time.beginning_of_hour, end_time.beginning_of_hour ]
|
108
|
-
else
|
109
|
-
[ start_time.beginning_of_day, end_time.beginning_of_day ]
|
15
|
+
summaries = @ransack_query.result(distinct: false).where(
|
16
|
+
summarizable_type: "RailsPulse::Route",
|
17
|
+
period_type: @period_type
|
18
|
+
)
|
19
|
+
|
20
|
+
summaries = summaries.where(summarizable_id: @route.id) if @route
|
21
|
+
summaries = summaries
|
22
|
+
.group(:period_start)
|
23
|
+
.having("AVG(avg_duration) > ?", @start_duration || 0)
|
24
|
+
.average(:avg_duration)
|
25
|
+
.transform_keys(&:to_i)
|
26
|
+
|
27
|
+
# Pad missing data points with zeros
|
28
|
+
step = @period_type == :hour ? 1.hour : 1.day
|
29
|
+
data = {}
|
30
|
+
(@start_time.to_i..@end_time.to_i).step(step) do |timestamp|
|
31
|
+
data[timestamp.to_i] = summaries[timestamp.to_i].to_f.round(2)
|
110
32
|
end
|
33
|
+
data
|
111
34
|
end
|
112
35
|
end
|
113
36
|
end
|
@@ -2,60 +2,77 @@ module RailsPulse
|
|
2
2
|
module Routes
|
3
3
|
module Tables
|
4
4
|
class Index
|
5
|
-
def initialize(ransack_query:, start_time:, params:)
|
5
|
+
def initialize(ransack_query:, period_type: nil, start_time:, params:)
|
6
6
|
@ransack_query = ransack_query
|
7
|
+
@period_type = period_type
|
7
8
|
@start_time = start_time
|
8
9
|
@params = params
|
9
10
|
end
|
10
11
|
|
11
12
|
def to_table
|
12
|
-
#
|
13
|
-
|
13
|
+
# Check if we have explicit ransack sorts
|
14
|
+
has_sorts = @ransack_query.sorts.any?
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
base_query = @ransack_query.result(distinct: false)
|
17
|
+
.joins("INNER JOIN rails_pulse_routes ON rails_pulse_routes.id = rails_pulse_summaries.summarizable_id")
|
18
|
+
.where(
|
19
|
+
summarizable_type: "RailsPulse::Route",
|
20
|
+
period_type: @period_type
|
21
|
+
)
|
20
22
|
|
21
|
-
|
23
|
+
base_query = base_query.where(summarizable_id: @route.id) if @route
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
.group(
|
25
|
+
# Apply grouping and aggregation
|
26
|
+
grouped_query = base_query
|
27
|
+
.group(
|
28
|
+
"rails_pulse_summaries.summarizable_id",
|
29
|
+
"rails_pulse_summaries.summarizable_type",
|
30
|
+
"rails_pulse_routes.id",
|
31
|
+
"rails_pulse_routes.path",
|
32
|
+
"rails_pulse_routes.method"
|
33
|
+
)
|
26
34
|
.select(
|
27
|
-
"
|
28
|
-
"
|
29
|
-
"
|
30
|
-
"
|
31
|
-
"
|
32
|
-
"
|
33
|
-
"
|
34
|
-
"
|
35
|
+
"rails_pulse_summaries.summarizable_id",
|
36
|
+
"rails_pulse_summaries.summarizable_type",
|
37
|
+
"rails_pulse_routes.id as route_id",
|
38
|
+
"rails_pulse_routes.path",
|
39
|
+
"rails_pulse_routes.method as route_method",
|
40
|
+
"AVG(rails_pulse_summaries.avg_duration) as avg_duration",
|
41
|
+
"MAX(rails_pulse_summaries.max_duration) as max_duration",
|
42
|
+
"SUM(rails_pulse_summaries.count) as count",
|
43
|
+
"SUM(rails_pulse_summaries.error_count) as error_count",
|
44
|
+
"SUM(rails_pulse_summaries.success_count) as success_count"
|
35
45
|
)
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
46
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
47
|
+
# Apply sorting based on ransack sorts or use default
|
48
|
+
if has_sorts
|
49
|
+
# Apply custom sorting based on ransack parameters
|
50
|
+
sort = @ransack_query.sorts.first
|
51
|
+
direction = sort.dir == "desc" ? :desc : :asc
|
44
52
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
case sort.name
|
54
|
+
when "avg_duration_sort"
|
55
|
+
grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").send(direction))
|
56
|
+
when "max_duration_sort"
|
57
|
+
grouped_query = grouped_query.order(Arel.sql("MAX(rails_pulse_summaries.max_duration)").send(direction))
|
58
|
+
when "count_sort"
|
59
|
+
grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count)").send(direction))
|
60
|
+
when "requests_per_minute"
|
61
|
+
grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count) / 60.0").send(direction))
|
62
|
+
when "error_rate_percentage"
|
63
|
+
grouped_query = grouped_query.order(Arel.sql("(SUM(rails_pulse_summaries.error_count) * 100.0) / SUM(rails_pulse_summaries.count)").send(direction))
|
64
|
+
when "route_path"
|
65
|
+
grouped_query = grouped_query.order(Arel.sql("rails_pulse_routes.path").send(direction))
|
66
|
+
else
|
67
|
+
# Unknown sort field, fallback to default
|
68
|
+
grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").desc)
|
69
|
+
end
|
70
|
+
else
|
71
|
+
# Apply default sort when no explicit sort is provided (matches controller default_table_sort)
|
72
|
+
grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").desc)
|
73
|
+
end
|
50
74
|
|
51
|
-
|
52
|
-
CASE
|
53
|
-
WHEN COALESCE(AVG(rails_pulse_requests.duration), 0) >= #{critical} THEN 3
|
54
|
-
WHEN COALESCE(AVG(rails_pulse_requests.duration), 0) >= #{very_slow} THEN 2
|
55
|
-
WHEN COALESCE(AVG(rails_pulse_requests.duration), 0) >= #{slow} THEN 1
|
56
|
-
ELSE 0
|
57
|
-
END
|
58
|
-
SQL
|
75
|
+
grouped_query
|
59
76
|
end
|
60
77
|
end
|
61
78
|
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
class Summary < RailsPulse::ApplicationRecord
|
3
|
+
self.table_name = "rails_pulse_summaries"
|
4
|
+
|
5
|
+
PERIOD_TYPES = %w[hour day week month].freeze
|
6
|
+
|
7
|
+
# Polymorphic association
|
8
|
+
belongs_to :summarizable, polymorphic: true, optional: true # Optional for Request summaries
|
9
|
+
|
10
|
+
# Convenience associations for easier querying
|
11
|
+
belongs_to :route, -> { where(rails_pulse_summaries: { summarizable_type: "RailsPulse::Route" }) },
|
12
|
+
foreign_key: "summarizable_id", class_name: "RailsPulse::Route", optional: true
|
13
|
+
belongs_to :query, -> { where(rails_pulse_summaries: { summarizable_type: "RailsPulse::Query" }) },
|
14
|
+
foreign_key: "summarizable_id", class_name: "RailsPulse::Query", optional: true
|
15
|
+
|
16
|
+
# Validations
|
17
|
+
validates :period_type, inclusion: { in: PERIOD_TYPES }
|
18
|
+
validates :period_start, presence: true
|
19
|
+
validates :period_end, presence: true
|
20
|
+
|
21
|
+
# Scopes
|
22
|
+
scope :for_period_type, ->(type) { where(period_type: type) }
|
23
|
+
scope :for_date_range, ->(start_date, end_date) {
|
24
|
+
where(period_start: start_date..end_date)
|
25
|
+
}
|
26
|
+
scope :for_requests, -> { where(summarizable_type: "RailsPulse::Request") }
|
27
|
+
scope :for_routes, -> { where(summarizable_type: "RailsPulse::Route") }
|
28
|
+
scope :for_queries, -> { where(summarizable_type: "RailsPulse::Query") }
|
29
|
+
scope :recent, -> { order(period_start: :desc) }
|
30
|
+
|
31
|
+
# Special scope for overall request summaries
|
32
|
+
scope :overall_requests, -> {
|
33
|
+
where(summarizable_type: "RailsPulse::Request", summarizable_id: 0)
|
34
|
+
}
|
35
|
+
|
36
|
+
# Ransack configuration
|
37
|
+
def self.ransackable_attributes(auth_object = nil)
|
38
|
+
%w[
|
39
|
+
period_start period_end avg_duration max_duration count error_count
|
40
|
+
requests_per_minute error_rate_percentage route_path_cont
|
41
|
+
execution_count total_time_consumed normalized_sql
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.ransackable_associations(auth_object = nil)
|
46
|
+
%w[route query]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Custom ransackers for calculated fields (designed to work with GROUP BY queries)
|
50
|
+
ransacker :count do
|
51
|
+
Arel.sql("SUM(rails_pulse_summaries.count)") # Use SUM for proper grouping
|
52
|
+
end
|
53
|
+
|
54
|
+
ransacker :requests_per_minute do
|
55
|
+
Arel.sql("SUM(rails_pulse_summaries.count) / 60.0") # Use SUM for consistency
|
56
|
+
end
|
57
|
+
|
58
|
+
ransacker :error_rate_percentage do
|
59
|
+
Arel.sql("(SUM(rails_pulse_summaries.error_count) * 100.0) / SUM(rails_pulse_summaries.count)") # Use SUM for both
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Ransacker for route path sorting (when joined with routes table)
|
64
|
+
ransacker :route_path do
|
65
|
+
Arel.sql("rails_pulse_routes.path")
|
66
|
+
end
|
67
|
+
|
68
|
+
# Ransacker for route path filtering using subquery (works without JOIN)
|
69
|
+
ransacker :route_path_cont do |parent|
|
70
|
+
Arel.sql(<<-SQL)
|
71
|
+
rails_pulse_summaries.summarizable_id IN (
|
72
|
+
SELECT id FROM rails_pulse_routes
|
73
|
+
WHERE rails_pulse_routes.path LIKE '%' || ? || '%'
|
74
|
+
)
|
75
|
+
SQL
|
76
|
+
end
|
77
|
+
|
78
|
+
# Sorting-specific ransackers for GROUP BY compatibility (used only in ORDER BY)
|
79
|
+
# These use different names to avoid conflicts with filtering
|
80
|
+
ransacker :avg_duration_sort do
|
81
|
+
Arel.sql("AVG(rails_pulse_summaries.avg_duration)")
|
82
|
+
end
|
83
|
+
|
84
|
+
ransacker :max_duration_sort do
|
85
|
+
Arel.sql("MAX(rails_pulse_summaries.max_duration)")
|
86
|
+
end
|
87
|
+
|
88
|
+
ransacker :count_sort do
|
89
|
+
Arel.sql("SUM(rails_pulse_summaries.count)")
|
90
|
+
end
|
91
|
+
|
92
|
+
ransacker :error_count_sort do
|
93
|
+
Arel.sql("SUM(rails_pulse_summaries.error_count)")
|
94
|
+
end
|
95
|
+
|
96
|
+
ransacker :success_count_sort do
|
97
|
+
Arel.sql("SUM(rails_pulse_summaries.success_count)")
|
98
|
+
end
|
99
|
+
|
100
|
+
ransacker :total_time_consumed_sort do
|
101
|
+
Arel.sql("SUM(rails_pulse_summaries.count * rails_pulse_summaries.avg_duration)")
|
102
|
+
end
|
103
|
+
|
104
|
+
# Alias execution_count_sort to count_sort for queries table compatibility
|
105
|
+
ransacker :execution_count_sort do
|
106
|
+
Arel.sql("SUM(rails_pulse_summaries.count)")
|
107
|
+
end
|
108
|
+
|
109
|
+
# Ransackers for queries table calculated fields
|
110
|
+
ransacker :execution_count do
|
111
|
+
Arel.sql("SUM(rails_pulse_summaries.count)") # Total executions
|
112
|
+
end
|
113
|
+
|
114
|
+
ransacker :total_time_consumed do
|
115
|
+
Arel.sql("SUM(rails_pulse_summaries.count * rails_pulse_summaries.avg_duration)") # Total time consumed
|
116
|
+
end
|
117
|
+
|
118
|
+
# Ransacker for query SQL sorting (when joined with queries table)
|
119
|
+
ransacker :normalized_sql do
|
120
|
+
Arel.sql("rails_pulse_queries.normalized_sql")
|
121
|
+
end
|
122
|
+
|
123
|
+
class << self
|
124
|
+
def calculate_period_end(period_type, start_time)
|
125
|
+
case period_type
|
126
|
+
when "hour" then start_time.end_of_hour
|
127
|
+
when "day" then start_time.end_of_day
|
128
|
+
when "week" then start_time.end_of_week
|
129
|
+
when "month" then start_time.end_of_month
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def normalize_period_start(period_type, time)
|
134
|
+
case period_type
|
135
|
+
when "hour" then time.beginning_of_hour
|
136
|
+
when "day" then time.beginning_of_day
|
137
|
+
when "week" then time.beginning_of_week
|
138
|
+
when "month" then time.beginning_of_month
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|