rails_pulse 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +74 -178
- data/Rakefile +75 -173
- data/app/assets/stylesheets/rails_pulse/application.css +0 -12
- 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 +1 -1
- data/app/controllers/rails_pulse/application_controller.rb +8 -4
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -0
- data/app/controllers/rails_pulse/queries_controller.rb +65 -50
- data/app/controllers/rails_pulse/requests_controller.rb +24 -12
- data/app/controllers/rails_pulse/routes_controller.rb +59 -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 +6 -2
- data/app/helpers/rails_pulse/status_helper.rb +10 -4
- data/app/javascript/rails_pulse/controllers/index_controller.js +117 -33
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +17 -27
- 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 +20 -9
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +19 -7
- data/app/models/rails_pulse/operation.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +47 -23
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +33 -26
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +34 -45
- 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 +1 -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 -23
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +38 -45
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +34 -47
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +30 -25
- 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/summary_service.rb +199 -0
- data/app/views/layouts/rails_pulse/application.html.erb +4 -4
- data/app/views/rails_pulse/components/_empty_state.html.erb +11 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +10 -24
- data/app/views/rails_pulse/dashboard/index.html.erb +54 -36
- data/app/views/rails_pulse/queries/_show_table.html.erb +1 -1
- data/app/views/rails_pulse/queries/_table.html.erb +10 -12
- data/app/views/rails_pulse/queries/index.html.erb +41 -34
- data/app/views/rails_pulse/queries/show.html.erb +38 -31
- data/app/views/rails_pulse/requests/_operations.html.erb +32 -26
- data/app/views/rails_pulse/requests/_table.html.erb +1 -3
- data/app/views/rails_pulse/requests/index.html.erb +42 -34
- data/app/views/rails_pulse/routes/_table.html.erb +13 -13
- data/app/views/rails_pulse/routes/index.html.erb +43 -35
- data/app/views/rails_pulse/routes/show.html.erb +42 -35
- data/config/initializers/rails_pulse.rb +0 -12
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +54 -0
- data/db/rails_pulse_schema.rb +121 -0
- data/lib/generators/rails_pulse/install_generator.rb +41 -4
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +60 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +0 -12
- data/lib/rails_pulse/configuration.rb +6 -12
- data/lib/rails_pulse/engine.rb +0 -1
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/tasks/rails_pulse.rake +58 -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 +1 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +3 -3
- data/public/rails-pulse-assets/search.svg +43 -0
- metadata +28 -12
- data/app/controllers/rails_pulse/caches_controller.rb +0 -115
- data/app/helpers/rails_pulse/cached_component_helper.rb +0 -73
- 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,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)
|
@@ -47,8 +58,8 @@ module RailsPulse
|
|
47
58
|
|
48
59
|
# Remove extra whitespace and truncate
|
49
60
|
cleaned_sql = sql.gsub(/\s+/, " ").strip
|
50
|
-
if cleaned_sql.length >
|
51
|
-
"#{cleaned_sql[0..
|
61
|
+
if cleaned_sql.length > 80
|
62
|
+
"#{cleaned_sql[0..79]}..."
|
52
63
|
else
|
53
64
|
cleaned_sql
|
54
65
|
end
|
@@ -3,16 +3,28 @@ module RailsPulse
|
|
3
3
|
module Tables
|
4
4
|
class SlowRoutes
|
5
5
|
include RailsPulse::FormattingHelper
|
6
|
+
|
6
7
|
def to_table_data
|
7
8
|
# Get data for this week
|
8
9
|
this_week_start = 1.week.ago.beginning_of_week
|
9
10
|
this_week_end = Time.current.end_of_week
|
10
11
|
|
11
|
-
# Fetch route data for this week
|
12
|
-
route_data = RailsPulse::
|
13
|
-
.
|
14
|
-
.
|
15
|
-
|
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
|
+
)
|
16
28
|
.order("avg_duration DESC")
|
17
29
|
.limit(5)
|
18
30
|
|
@@ -20,8 +32,8 @@ module RailsPulse
|
|
20
32
|
data_rows = route_data.map do |record|
|
21
33
|
{
|
22
34
|
route_path: record.path,
|
23
|
-
route_id: record.
|
24
|
-
route_link: "/rails_pulse/routes/#{record.
|
35
|
+
route_id: record.route_id,
|
36
|
+
route_link: "/rails_pulse/routes/#{record.route_id}",
|
25
37
|
average_time: record.avg_duration.to_f.round(0),
|
26
38
|
request_count: record.request_count,
|
27
39
|
last_request: time_ago_in_words(record.last_seen)
|
@@ -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
|
-
|
39
|
+
# Separate query for sparkline data - manually calculate weighted averages by week
|
40
|
+
sparkline_data = {}
|
41
|
+
base_query.each do |summary|
|
42
|
+
week_start = summary.period_start.beginning_of_week
|
43
|
+
formatted_date = week_start.strftime("%b %-d")
|
44
|
+
|
45
|
+
if sparkline_data[formatted_date]
|
46
|
+
sparkline_data[formatted_date][:total_weighted] += (summary.avg_duration || 0) * (summary.count || 0)
|
47
|
+
sparkline_data[formatted_date][:total_count] += (summary.count || 0)
|
48
|
+
else
|
49
|
+
sparkline_data[formatted_date] = {
|
50
|
+
total_weighted: (summary.avg_duration || 0) * (summary.count || 0),
|
51
|
+
total_count: (summary.count || 0)
|
37
52
|
}
|
38
53
|
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Convert to final format
|
57
|
+
sparkline_data = sparkline_data.transform_values do |data|
|
58
|
+
weighted_avg = data[:total_count] > 0 ? (data[:total_weighted] / data[:total_count]).round(0) : 0
|
59
|
+
{ value: weighted_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,51 @@ 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
|
-
}
|
36
|
+
# Separate query for sparkline data - group by week using Rails
|
37
|
+
sparkline_data = base_query
|
38
|
+
.group_by_week(:period_start, time_zone: "UTC")
|
39
|
+
.sum(:count)
|
40
|
+
.each_with_object({}) do |(week_start, total_count), hash|
|
41
|
+
formatted_date = week_start.strftime("%b %-d")
|
42
|
+
value = total_count || 0
|
43
|
+
hash[formatted_date] = { value: value }
|
37
44
|
end
|
38
45
|
|
39
|
-
# Calculate average
|
40
|
-
|
41
|
-
|
42
|
-
total_minutes = min_time && max_time && min_time != max_time ? (max_time - min_time) / 60.0 : 1
|
43
|
-
average_operations_per_minute = total_request_count / total_minutes
|
46
|
+
# Calculate average executions per minute over 2-week period
|
47
|
+
total_minutes = 2.weeks / 1.minute
|
48
|
+
average_executions_per_minute = total_execution_count / total_minutes
|
44
49
|
|
45
50
|
{
|
51
|
+
id: "execution_rate",
|
52
|
+
context: "queries",
|
46
53
|
title: "Execution Rate",
|
47
|
-
summary: "#{
|
54
|
+
summary: "#{average_executions_per_minute.round(2)} / min",
|
48
55
|
line_chart_data: sparkline_data,
|
49
56
|
trend_icon: trend_icon,
|
50
57
|
trend_amount: trend_amount,
|
@@ -7,58 +7,47 @@ 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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
20
|
+
|
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}%"
|
35
|
+
|
36
|
+
# Separate query for sparkline data - group by week using Rails
|
37
|
+
sparkline_data = base_query
|
38
|
+
.group_by_week(:period_start, time_zone: "UTC")
|
39
|
+
.average(:p95_duration)
|
40
|
+
.each_with_object({}) do |(week_start, avg_p95), hash|
|
41
|
+
formatted_date = week_start.strftime("%b %-d")
|
42
|
+
value = (avg_p95 || 0).round(0)
|
43
|
+
hash[formatted_date] = { value: value }
|
57
44
|
end
|
58
45
|
|
59
46
|
{
|
47
|
+
id: "percentile_query_times",
|
48
|
+
context: "queries",
|
60
49
|
title: "95th Percentile Query Time",
|
61
|
-
summary: "#{
|
50
|
+
summary: "#{p95_query_time} ms",
|
62
51
|
line_chart_data: sparkline_data,
|
63
52
|
trend_icon: trend_icon,
|
64
53
|
trend_amount: trend_amount,
|