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,41 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
class BackfillSummariesJob < ApplicationJob
|
3
|
+
queue_as :low_priority
|
4
|
+
|
5
|
+
def perform(start_date, end_date, period_types = [ "hour", "day" ])
|
6
|
+
start_date = start_date.to_datetime
|
7
|
+
end_date = end_date.to_datetime
|
8
|
+
|
9
|
+
period_types.each do |period_type|
|
10
|
+
backfill_period(period_type, start_date, end_date)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def backfill_period(period_type, start_date, end_date)
|
17
|
+
current = Summary.normalize_period_start(period_type, start_date)
|
18
|
+
period_end = Summary.calculate_period_end(period_type, end_date)
|
19
|
+
|
20
|
+
while current <= period_end
|
21
|
+
Rails.logger.info "[RailsPulse] Backfilling #{period_type} summary for #{current}"
|
22
|
+
|
23
|
+
SummaryService.new(period_type, current).perform
|
24
|
+
|
25
|
+
current = advance_period(current, period_type)
|
26
|
+
|
27
|
+
# Add small delay to avoid overwhelming the database
|
28
|
+
sleep 0.1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def advance_period(time, period_type)
|
33
|
+
case period_type
|
34
|
+
when "hour" then time + 1.hour
|
35
|
+
when "day" then time + 1.day
|
36
|
+
when "week" then time + 1.week
|
37
|
+
when "month" then time + 1.month
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
class SummaryJob < ApplicationJob
|
3
|
+
queue_as :low_priority
|
4
|
+
|
5
|
+
def perform(target_hour = nil)
|
6
|
+
target_hour ||= 1.hour.ago.beginning_of_hour
|
7
|
+
|
8
|
+
# Always run hourly summary
|
9
|
+
process_hourly_summary(target_hour)
|
10
|
+
|
11
|
+
# Check if we should run daily summary (at the start of a new day)
|
12
|
+
if target_hour.hour == 0
|
13
|
+
process_daily_summary(target_hour.to_date - 1.day)
|
14
|
+
|
15
|
+
# Check if we should run weekly summary (Monday at midnight)
|
16
|
+
if target_hour.wday == 1
|
17
|
+
process_weekly_summary((target_hour.to_date - 1.week).beginning_of_week)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check if we should run monthly summary (first day of month)
|
21
|
+
if target_hour.day == 1
|
22
|
+
process_monthly_summary((target_hour.to_date - 1.month).beginning_of_month)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
rescue => e
|
26
|
+
Rails.logger.error "[RailsPulse] Summary job failed: #{e.message}"
|
27
|
+
Rails.logger.error e.backtrace.join("\n")
|
28
|
+
raise
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def process_hourly_summary(hour)
|
34
|
+
Rails.logger.info "[RailsPulse] Processing hourly summary for #{hour}"
|
35
|
+
SummaryService.new("hour", hour).perform
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_daily_summary(date)
|
39
|
+
Rails.logger.info "[RailsPulse] Processing daily summary for #{date}"
|
40
|
+
SummaryService.new("day", date).perform
|
41
|
+
end
|
42
|
+
|
43
|
+
def process_weekly_summary(week_start)
|
44
|
+
Rails.logger.info "[RailsPulse] Processing weekly summary for week starting #{week_start}"
|
45
|
+
SummaryService.new("week", week_start).perform
|
46
|
+
end
|
47
|
+
|
48
|
+
def process_monthly_summary(month_start)
|
49
|
+
Rails.logger.info "[RailsPulse] Processing monthly summary for month starting #{month_start}"
|
50
|
+
SummaryService.new("month", month_start).perform
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -8,17 +8,38 @@ module RailsPulse
|
|
8
8
|
end_date = Time.current.to_date
|
9
9
|
date_range = (start_date..end_date)
|
10
10
|
|
11
|
-
# Get the actual data
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
.
|
11
|
+
# Get the actual data from Summary records (routes)
|
12
|
+
summaries = RailsPulse::Summary.where(
|
13
|
+
summarizable_type: "RailsPulse::Route",
|
14
|
+
period_type: "day",
|
15
|
+
period_start: start_date.beginning_of_day..end_date.end_of_day
|
16
|
+
)
|
17
|
+
|
18
|
+
# Group by day manually for cross-database compatibility
|
19
|
+
actual_data = {}
|
20
|
+
summaries.each do |summary|
|
21
|
+
date = summary.period_start.to_date
|
22
|
+
|
23
|
+
if actual_data[date]
|
24
|
+
actual_data[date][:total_weighted] += (summary.avg_duration || 0) * (summary.count || 0)
|
25
|
+
actual_data[date][:total_count] += (summary.count || 0)
|
26
|
+
else
|
27
|
+
actual_data[date] = {
|
28
|
+
total_weighted: (summary.avg_duration || 0) * (summary.count || 0),
|
29
|
+
total_count: (summary.count || 0)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convert to final values
|
35
|
+
actual_data = actual_data.transform_values do |data|
|
36
|
+
data[:total_count] > 0 ? (data[:total_weighted] / data[:total_count]).round(0) : 0
|
37
|
+
end
|
16
38
|
|
17
39
|
# Fill in all dates with zero values for missing days
|
18
40
|
date_range.each_with_object({}) do |date, result|
|
19
41
|
formatted_date = date.strftime("%b %-d")
|
20
|
-
|
21
|
-
result[formatted_date] = avg_duration&.round(0) || 0
|
42
|
+
result[formatted_date] = actual_data[date] || 0
|
22
43
|
end
|
23
44
|
end
|
24
45
|
end
|
@@ -3,32 +3,28 @@ module RailsPulse
|
|
3
3
|
module Charts
|
4
4
|
class P95ResponseTime
|
5
5
|
def to_chart_data
|
6
|
-
|
6
|
+
# Create a range of all dates in the past 2 weeks
|
7
|
+
start_date = 2.weeks.ago.beginning_of_day.to_date
|
8
|
+
end_date = Time.current.to_date
|
9
|
+
date_range = (start_date..end_date)
|
7
10
|
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
.order("request_date, duration")
|
15
|
-
.group_by { |r| r.request_date.to_date }
|
11
|
+
# Get the actual data from Summary records (queries for P95)
|
12
|
+
summaries = RailsPulse::Summary.where(
|
13
|
+
summarizable_type: "RailsPulse::Query",
|
14
|
+
period_type: "day",
|
15
|
+
period_start: start_date.beginning_of_day..end_date.end_of_day
|
16
|
+
)
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
p95_value = 0
|
23
|
-
else
|
24
|
-
# Calculate P95 from in-memory sorted array (already sorted by DB)
|
25
|
-
count = day_requests.length
|
26
|
-
p95_index = (count * 0.95).ceil - 1
|
27
|
-
p95_value = day_requests[p95_index].duration.round(0)
|
28
|
-
end
|
18
|
+
actual_data = summaries
|
19
|
+
.group_by_day(:period_start, time_zone: Time.zone)
|
20
|
+
.average(:p95_duration)
|
21
|
+
.transform_keys { |date| date.to_date }
|
22
|
+
.transform_values { |avg| avg&.round(0) || 0 }
|
29
23
|
|
24
|
+
# Fill in all dates with zero values for missing days
|
25
|
+
date_range.each_with_object({}) do |date, result|
|
30
26
|
formatted_date = date.strftime("%b %-d")
|
31
|
-
|
27
|
+
result[formatted_date] = actual_data[date] || 0
|
32
28
|
end
|
33
29
|
end
|
34
30
|
end
|
@@ -8,11 +8,22 @@ module RailsPulse
|
|
8
8
|
this_week_start = 1.week.ago.beginning_of_week
|
9
9
|
this_week_end = Time.current.end_of_week
|
10
10
|
|
11
|
-
# Fetch query data for this week
|
12
|
-
query_data = RailsPulse::
|
13
|
-
.
|
14
|
-
.
|
15
|
-
|
11
|
+
# Fetch query data from Summary records for this week
|
12
|
+
query_data = RailsPulse::Summary
|
13
|
+
.joins("INNER JOIN rails_pulse_queries ON rails_pulse_queries.id = rails_pulse_summaries.summarizable_id")
|
14
|
+
.where(
|
15
|
+
summarizable_type: "RailsPulse::Query",
|
16
|
+
period_type: "day",
|
17
|
+
period_start: this_week_start..this_week_end
|
18
|
+
)
|
19
|
+
.group("rails_pulse_summaries.summarizable_id, rails_pulse_queries.normalized_sql")
|
20
|
+
.select(
|
21
|
+
"rails_pulse_summaries.summarizable_id as query_id",
|
22
|
+
"rails_pulse_queries.normalized_sql",
|
23
|
+
"SUM(rails_pulse_summaries.avg_duration * rails_pulse_summaries.count) / SUM(rails_pulse_summaries.count) as avg_duration",
|
24
|
+
"SUM(rails_pulse_summaries.count) as request_count",
|
25
|
+
"MAX(rails_pulse_summaries.period_end) as last_seen"
|
26
|
+
)
|
16
27
|
.order("avg_duration DESC")
|
17
28
|
.limit(5)
|
18
29
|
|
@@ -20,8 +31,8 @@ module RailsPulse
|
|
20
31
|
data_rows = query_data.map do |record|
|
21
32
|
{
|
22
33
|
query_text: truncate_query(record.normalized_sql),
|
23
|
-
query_id: record.
|
24
|
-
query_link: "/rails_pulse/queries/#{record.
|
34
|
+
query_id: record.query_id,
|
35
|
+
query_link: "/rails_pulse/queries/#{record.query_id}",
|
25
36
|
average_time: record.avg_duration.to_f.round(0),
|
26
37
|
request_count: record.request_count,
|
27
38
|
last_request: time_ago_in_words(record.last_seen)
|
@@ -5,58 +5,51 @@ module RailsPulse
|
|
5
5
|
include RailsPulse::FormattingHelper
|
6
6
|
|
7
7
|
def to_table_data
|
8
|
-
# Get data for this week
|
8
|
+
# Get data for this week
|
9
9
|
this_week_start = 1.week.ago.beginning_of_week
|
10
10
|
this_week_end = Time.current.end_of_week
|
11
|
-
last_week_start = 2.weeks.ago.beginning_of_week
|
12
|
-
last_week_end = 1.week.ago.beginning_of_week
|
13
11
|
|
14
|
-
#
|
15
|
-
|
16
|
-
.
|
17
|
-
.
|
18
|
-
|
12
|
+
# Fetch route data from Summary records for this week
|
13
|
+
route_data = RailsPulse::Summary
|
14
|
+
.joins("INNER JOIN rails_pulse_routes ON rails_pulse_routes.id = rails_pulse_summaries.summarizable_id")
|
15
|
+
.where(
|
16
|
+
summarizable_type: "RailsPulse::Route",
|
17
|
+
period_type: "day",
|
18
|
+
period_start: this_week_start..this_week_end
|
19
|
+
)
|
20
|
+
.group("rails_pulse_summaries.summarizable_id, rails_pulse_routes.path")
|
21
|
+
.select(
|
22
|
+
"rails_pulse_summaries.summarizable_id as route_id",
|
23
|
+
"rails_pulse_routes.path",
|
24
|
+
"SUM(rails_pulse_summaries.avg_duration * rails_pulse_summaries.count) / SUM(rails_pulse_summaries.count) as avg_duration",
|
25
|
+
"SUM(rails_pulse_summaries.count) as request_count",
|
26
|
+
"MAX(rails_pulse_summaries.period_end) as last_seen"
|
27
|
+
)
|
19
28
|
.order("avg_duration DESC")
|
20
29
|
.limit(5)
|
21
30
|
|
22
|
-
#
|
23
|
-
|
24
|
-
.where(occurred_at: last_week_start..last_week_end)
|
25
|
-
.group("rails_pulse_routes.path")
|
26
|
-
.average("rails_pulse_requests.duration")
|
27
|
-
|
28
|
-
# Build result array matching test expectations
|
29
|
-
this_week_data.map do |record|
|
30
|
-
this_week_avg = record.avg_duration.to_f.round(0)
|
31
|
-
last_week_avg = last_week_averages[record.path]&.round(0) || 0
|
32
|
-
|
33
|
-
# Calculate percentage change
|
34
|
-
percentage_change = if last_week_avg == 0
|
35
|
-
this_week_avg > 0 ? 100.0 : 0.0
|
36
|
-
else
|
37
|
-
((this_week_avg - last_week_avg) / last_week_avg.to_f * 100).round(1)
|
38
|
-
end
|
39
|
-
|
40
|
-
# Determine trend (worse = slower response times)
|
41
|
-
trend = if last_week_avg == 0
|
42
|
-
this_week_avg > 0 ? "worse" : "stable"
|
43
|
-
elsif this_week_avg > last_week_avg
|
44
|
-
"worse" # Slower = worse
|
45
|
-
elsif this_week_avg < last_week_avg
|
46
|
-
"better" # Faster = better
|
47
|
-
else
|
48
|
-
"stable"
|
49
|
-
end
|
50
|
-
|
31
|
+
# Build data rows
|
32
|
+
data_rows = route_data.map do |record|
|
51
33
|
{
|
52
34
|
route_path: record.path,
|
53
|
-
|
54
|
-
|
55
|
-
|
35
|
+
route_id: record.route_id,
|
36
|
+
route_link: "/rails_pulse/routes/#{record.route_id}",
|
37
|
+
average_time: record.avg_duration.to_f.round(0),
|
56
38
|
request_count: record.request_count,
|
57
|
-
|
39
|
+
last_request: time_ago_in_words(record.last_seen)
|
58
40
|
}
|
59
41
|
end
|
42
|
+
|
43
|
+
# Return new structure with columns and data
|
44
|
+
{
|
45
|
+
columns: [
|
46
|
+
{ field: :route_path, label: "Route", link_to: :route_link, class: "w-auto" },
|
47
|
+
{ field: :average_time, label: "Average Time", class: "w-32" },
|
48
|
+
{ field: :request_count, label: "Requests", class: "w-24" },
|
49
|
+
{ field: :last_request, label: "Last Request", class: "w-32" }
|
50
|
+
],
|
51
|
+
data: data_rows
|
52
|
+
}
|
60
53
|
end
|
61
54
|
end
|
62
55
|
end
|
@@ -34,7 +34,7 @@ module RailsPulse
|
|
34
34
|
before_validation :associate_query
|
35
35
|
|
36
36
|
def self.ransackable_attributes(auth_object = nil)
|
37
|
-
%w[id occurred_at label duration start_time average_query_time_ms query_count operation_type]
|
37
|
+
%w[id occurred_at label duration start_time average_query_time_ms query_count operation_type query_id]
|
38
38
|
end
|
39
39
|
|
40
40
|
def self.ransackable_associations(auth_object = nil)
|
@@ -2,42 +2,66 @@ module RailsPulse
|
|
2
2
|
module Queries
|
3
3
|
module Cards
|
4
4
|
class AverageQueryTimes
|
5
|
-
def initialize(query:)
|
5
|
+
def initialize(query: nil)
|
6
6
|
@query = query
|
7
7
|
end
|
8
8
|
|
9
9
|
def to_metric_card
|
10
|
-
operations = if @query
|
11
|
-
RailsPulse::Operation.where(query: @query)
|
12
|
-
else
|
13
|
-
RailsPulse::Operation.all
|
14
|
-
end
|
15
|
-
|
16
|
-
# Calculate overall average response time
|
17
|
-
average_query_time = operations.average(:duration)&.round(0) || 0
|
18
|
-
|
19
|
-
# Calculate trend by comparing last 7 days vs previous 7 days
|
20
10
|
last_7_days = 7.days.ago.beginning_of_day
|
21
11
|
previous_7_days = 14.days.ago.beginning_of_day
|
22
|
-
current_period_avg = operations.where("occurred_at >= ?", last_7_days).average(:duration) || 0
|
23
|
-
previous_period_avg = operations.where("occurred_at >= ? AND occurred_at < ?", previous_7_days, last_7_days).average(:duration) || 0
|
24
12
|
|
25
|
-
|
26
|
-
|
13
|
+
# Single query to get all aggregated metrics with conditional sums
|
14
|
+
base_query = RailsPulse::Summary.where(
|
15
|
+
summarizable_type: "RailsPulse::Query",
|
16
|
+
period_type: "day",
|
17
|
+
period_start: 2.weeks.ago.beginning_of_day..Time.current
|
18
|
+
)
|
19
|
+
base_query = base_query.where(summarizable_id: @query.id) if @query
|
20
|
+
|
21
|
+
metrics = base_query.select(
|
22
|
+
"SUM(avg_duration * count) AS total_weighted_duration",
|
23
|
+
"SUM(count) AS total_requests",
|
24
|
+
"SUM(CASE WHEN period_start >= '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN avg_duration * count ELSE 0 END) AS current_weighted_duration",
|
25
|
+
"SUM(CASE WHEN period_start >= '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN count ELSE 0 END) AS current_requests",
|
26
|
+
"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 avg_duration * count ELSE 0 END) AS previous_weighted_duration",
|
27
|
+
"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_requests"
|
28
|
+
).take
|
29
|
+
|
30
|
+
# Calculate metrics from single query result
|
31
|
+
average_query_time = metrics.total_requests.to_i > 0 ? (metrics.total_weighted_duration / metrics.total_requests).round(0) : 0
|
32
|
+
current_period_avg = metrics.current_requests.to_i > 0 ? (metrics.current_weighted_duration / metrics.current_requests) : 0
|
33
|
+
previous_period_avg = metrics.previous_requests.to_i > 0 ? (metrics.previous_weighted_duration / metrics.previous_requests) : 0
|
34
|
+
|
35
|
+
percentage = previous_period_avg.zero? ? 0 : ((previous_period_avg - current_period_avg) / previous_period_avg * 100).abs.round(1)
|
36
|
+
trend_icon = percentage < 0.1 ? "move-right" : current_period_avg < previous_period_avg ? "trending-down" : "trending-up"
|
27
37
|
trend_amount = previous_period_avg.zero? ? "0%" : "#{percentage}%"
|
28
38
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
.
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
# Sparkline data by day with zero-filled days over the last 14 days
|
40
|
+
# Use Groupdate to get grouped sums and compute weighted averages per day
|
41
|
+
grouped_weighted = base_query
|
42
|
+
.group_by_day(:period_start, time_zone: "UTC")
|
43
|
+
.sum(Arel.sql("avg_duration * count"))
|
44
|
+
|
45
|
+
grouped_counts = base_query
|
46
|
+
.group_by_day(:period_start, time_zone: "UTC")
|
47
|
+
.sum(:count)
|
48
|
+
|
49
|
+
# Build a continuous 14-day range, fill missing days with 0
|
50
|
+
start_day = 2.weeks.ago.beginning_of_day.to_date
|
51
|
+
end_day = Time.current.to_date
|
52
|
+
|
53
|
+
sparkline_data = {}
|
54
|
+
(start_day..end_day).each do |day|
|
55
|
+
weighted_sum = grouped_weighted[day] || 0
|
56
|
+
count_sum = grouped_counts[day] || 0
|
57
|
+
avg = count_sum > 0 ? (weighted_sum.to_f / count_sum).round(0) : 0
|
58
|
+
label = day.strftime("%b %-d")
|
59
|
+
sparkline_data[label] = { value: avg }
|
60
|
+
end
|
39
61
|
|
40
62
|
{
|
63
|
+
id: "average_query_times",
|
64
|
+
context: "queries",
|
41
65
|
title: "Average Query Time",
|
42
66
|
summary: "#{average_query_time} ms",
|
43
67
|
line_chart_data: sparkline_data,
|
@@ -7,44 +7,56 @@ module RailsPulse
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def to_metric_card
|
10
|
-
operations = if @query
|
11
|
-
RailsPulse::Operation.where(query: @query)
|
12
|
-
else
|
13
|
-
RailsPulse::Operation.all
|
14
|
-
end
|
15
|
-
|
16
|
-
# Calculate total request count
|
17
|
-
total_request_count = operations.count
|
18
|
-
|
19
|
-
# Calculate trend by comparing last 7 days vs previous 7 days
|
20
10
|
last_7_days = 7.days.ago.beginning_of_day
|
21
11
|
previous_7_days = 14.days.ago.beginning_of_day
|
22
|
-
|
23
|
-
|
12
|
+
|
13
|
+
# Single query to get all count metrics with conditional aggregation
|
14
|
+
base_query = RailsPulse::Summary.where(
|
15
|
+
summarizable_type: "RailsPulse::Query",
|
16
|
+
period_type: "day",
|
17
|
+
period_start: 2.weeks.ago.beginning_of_day..Time.current
|
18
|
+
)
|
19
|
+
base_query = base_query.where(summarizable_id: @query.id) if @query
|
20
|
+
|
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
|
26
|
+
|
27
|
+
# Calculate metrics from single query result
|
28
|
+
total_execution_count = metrics.total_count || 0
|
29
|
+
current_period_count = metrics.current_count || 0
|
30
|
+
previous_period_count = metrics.previous_count || 0
|
24
31
|
|
25
32
|
percentage = previous_period_count.zero? ? 0 : ((previous_period_count - current_period_count) / previous_period_count.to_f * 100).abs.round(1)
|
26
33
|
trend_icon = percentage < 0.1 ? "move-right" : current_period_count < previous_period_count ? "trending-down" : "trending-up"
|
27
34
|
trend_amount = previous_period_count.zero? ? "0%" : "#{percentage}%"
|
28
35
|
|
29
|
-
|
30
|
-
|
31
|
-
.
|
32
|
-
.
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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 executions per minute over 2-week period
|
52
|
+
total_minutes = 2.weeks / 1.minute
|
53
|
+
average_executions_per_minute = total_execution_count / total_minutes
|
44
54
|
|
45
55
|
{
|
56
|
+
id: "execution_rate",
|
57
|
+
context: "queries",
|
46
58
|
title: "Execution Rate",
|
47
|
-
summary: "#{
|
59
|
+
summary: "#{average_executions_per_minute.round(2)} / min",
|
48
60
|
line_chart_data: sparkline_data,
|
49
61
|
trend_icon: trend_icon,
|
50
62
|
trend_amount: trend_amount,
|
@@ -7,58 +7,52 @@ module RailsPulse
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def to_metric_card
|
10
|
-
operations = if @query
|
11
|
-
RailsPulse::Operation.where(query: @query)
|
12
|
-
else
|
13
|
-
RailsPulse::Operation.all
|
14
|
-
end
|
15
|
-
|
16
|
-
# Calculate overall 95th percentile response time
|
17
|
-
count = operations.count
|
18
|
-
percentile_95th = if count > 0
|
19
|
-
operations.select("duration").order("duration").limit(1).offset((count * 0.95).floor).pluck(:duration).first || 0
|
20
|
-
else
|
21
|
-
0
|
22
|
-
end
|
23
|
-
|
24
|
-
# Calculate trend by comparing last 7 days vs previous 7 days for 95th percentile
|
25
10
|
last_7_days = 7.days.ago.beginning_of_day
|
26
11
|
previous_7_days = 14.days.ago.beginning_of_day
|
27
12
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
13
|
+
# Single query to get all P95 metrics with conditional aggregation
|
14
|
+
base_query = RailsPulse::Summary.where(
|
15
|
+
summarizable_type: "RailsPulse::Query",
|
16
|
+
period_type: "day",
|
17
|
+
period_start: 2.weeks.ago.beginning_of_day..Time.current
|
18
|
+
)
|
19
|
+
base_query = base_query.where(summarizable_id: @query.id) if @query
|
35
20
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
26
|
+
|
27
|
+
# Calculate metrics from single query result
|
28
|
+
p95_query_time = (metrics.overall_p95 || 0).round(0)
|
29
|
+
current_period_p95 = metrics.current_p95 || 0
|
30
|
+
previous_period_p95 = metrics.previous_p95 || 0
|
31
|
+
|
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}%"
|
43
35
|
|
44
|
-
|
45
|
-
|
46
|
-
|
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)
|
47
40
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
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
|
+
avg = grouped_daily[day]&.round(0) || 0
|
47
|
+
label = day.strftime("%b %-d")
|
48
|
+
sparkline_data[label] = { value: avg }
|
49
|
+
end
|
58
50
|
|
59
51
|
{
|
52
|
+
id: "percentile_query_times",
|
53
|
+
context: "queries",
|
60
54
|
title: "95th Percentile Query Time",
|
61
|
-
summary: "#{
|
55
|
+
summary: "#{p95_query_time} ms",
|
62
56
|
line_chart_data: sparkline_data,
|
63
57
|
trend_icon: trend_icon,
|
64
58
|
trend_amount: trend_amount,
|