rails_pulse 0.4.0.pre.4 → 0.4.0
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/CHANGELOG.md +64 -87
- data/README.md +83 -111
- data/app/controllers/concerns/session_filters_concern.rb +4 -3
- data/app/controllers/concerns/time_range_concern.rb +29 -19
- data/app/controllers/rails_pulse/application_controller.rb +9 -5
- data/app/controllers/rails_pulse/dashboard_controller.rb +20 -11
- data/app/controllers/rails_pulse/routes_controller.rb +46 -0
- data/app/helpers/rails_pulse/application_helper.rb +4 -0
- data/app/javascript/rails_pulse/controllers/chart_controller.js +27 -0
- data/app/models/rails_pulse/cards/base.rb +61 -25
- data/app/models/rails_pulse/dashboard/charts/response_time_percentiles.rb +38 -15
- data/app/models/rails_pulse/dashboard/charts/throughput_and_errors.rb +36 -13
- data/app/models/rails_pulse/dashboard/concerns/time_range_helper.rb +3 -0
- data/app/models/rails_pulse/dashboard/health_summary.rb +3 -1
- data/app/models/rails_pulse/dashboard/needs_attention.rb +3 -1
- data/app/models/rails_pulse/dashboard/storage_status.rb +38 -68
- data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +1 -5
- data/app/models/rails_pulse/exceptions/cards/open_groups.rb +4 -4
- data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +3 -4
- data/app/models/rails_pulse/jobs/cards/failure_rate.rb +7 -20
- data/app/models/rails_pulse/jobs/cards/p95_duration.rb +5 -8
- data/app/models/rails_pulse/jobs/cards/total_runs.rb +4 -4
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +6 -9
- data/app/models/rails_pulse/queries/cards/database_load.rb +7 -10
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +4 -6
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +5 -8
- data/app/models/rails_pulse/route.rb +7 -1
- data/app/models/rails_pulse/routes/cards/error_rates.rb +5 -11
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +3 -1
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +4 -6
- data/app/models/rails_pulse/summary.rb +13 -18
- data/app/models/rails_pulse/time_range_preference.rb +28 -0
- data/app/models/rails_pulse/time_window.rb +61 -0
- data/app/services/rails_pulse/summary_service.rb +112 -162
- data/app/services/rails_pulse/tag_filter_service.rb +10 -1
- data/app/views/layouts/rails_pulse/_time_range_selector.html.erb +2 -2
- data/app/views/rails_pulse/dashboard/index.html.erb +6 -3
- data/app/views/rails_pulse/routes/_archived_summary_table.html.erb +46 -0
- data/app/views/rails_pulse/routes/_archived_table_pagination.html.erb +28 -0
- data/app/views/rails_pulse/routes/show.html.erb +14 -2
- data/lib/generators/rails_pulse/install_generator.rb +5 -2
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +7 -2
- data/lib/rails_pulse/cleanup_service.rb +7 -7
- data/lib/rails_pulse/configuration.rb +9 -1
- data/lib/rails_pulse/engine.rb +14 -42
- data/lib/rails_pulse/middleware/request_collector.rb +2 -17
- data/lib/rails_pulse/standalone.rb +12 -0
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +45 -3
- data/lib/rails_pulse/tracker.rb +241 -25
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +3 -16
- data/lib/rails_pulse_server.ru +37 -23
- data/public/rails-pulse-assets/rails-pulse.js +1 -1
- metadata +7 -9
- data/app/assets/images/rails_pulse/dashboard.png +0 -0
- data/app/assets/images/rails_pulse/query-show.png +0 -0
- data/app/assets/images/rails_pulse/request-show.png +0 -0
- data/app/assets/images/rails_pulse/request.png +0 -0
- data/app/assets/images/rails_pulse/route-show.png +0 -0
- data/lib/rails_pulse/extensions/active_record.rb +0 -124
|
@@ -98,11 +98,7 @@ module RailsPulse
|
|
|
98
98
|
def grouped_counts(scope)
|
|
99
99
|
scoped = scope.where(period_type: @period_type, period_start: current_window_start..now)
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
scoped.group_by_hour(:period_start).sum("rails_pulse_summaries.count")
|
|
103
|
-
else
|
|
104
|
-
scoped.group_by_date(:period_start).sum("rails_pulse_summaries.count")
|
|
105
|
-
end
|
|
101
|
+
bucket_by_period(scoped) { |relation| relation.sum("rails_pulse_summaries.count") }
|
|
106
102
|
end
|
|
107
103
|
|
|
108
104
|
def period_stat(exceptions, requests)
|
|
@@ -25,10 +25,10 @@ module RailsPulse
|
|
|
25
25
|
.where(first_seen_at: current_window_start..now)
|
|
26
26
|
.count
|
|
27
27
|
|
|
28
|
-
grouped =
|
|
29
|
-
.where(first_seen_at: current_window_start..now)
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
grouped = bucket_by_period(
|
|
29
|
+
RailsPulse::ExceptionGroup.where(first_seen_at: current_window_start..now),
|
|
30
|
+
column: :first_seen_at
|
|
31
|
+
) { |relation| relation.count }
|
|
32
32
|
|
|
33
33
|
{
|
|
34
34
|
id: "exceptions_open_groups",
|
|
@@ -36,10 +36,9 @@ module RailsPulse
|
|
|
36
36
|
# what it means on a throughput card.
|
|
37
37
|
trend_icon, trend_amount = trend_for(current, previous) if show_trend?
|
|
38
38
|
|
|
39
|
-
grouped = base_query
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
.sum("rails_pulse_summaries.count")
|
|
39
|
+
grouped = bucket_by_period(base_query.where(period_start: current_window_start..now)) do |relation|
|
|
40
|
+
relation.sum("rails_pulse_summaries.count")
|
|
41
|
+
end
|
|
43
42
|
|
|
44
43
|
{
|
|
45
44
|
id: "exceptions_total_occurrences",
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Jobs
|
|
3
3
|
module Cards
|
|
4
4
|
class FailureRate < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(job: nil, disabled_tags: [], show_non_tagged: true, period: 14, period_type: "day")
|
|
5
|
+
def initialize(job: nil, disabled_tags: [], show_non_tagged: true, period: 14, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@job = job
|
|
7
7
|
@disabled_tags = disabled_tags
|
|
8
8
|
@show_non_tagged = show_non_tagged
|
|
9
9
|
@period = period
|
|
10
10
|
@period_type = period_type
|
|
11
|
+
@start_time = start_time
|
|
12
|
+
@end_time = end_time
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_metric_card
|
|
@@ -52,13 +54,8 @@ module RailsPulse
|
|
|
52
54
|
)
|
|
53
55
|
sparkline_query = sparkline_query.where(summarizable_id: @job.id) if @job
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
grouped_counts = sparkline_query.group_by_hour(:period_start).sum("rails_pulse_summaries.count")
|
|
58
|
-
else
|
|
59
|
-
grouped_errors = sparkline_query.group_by_date(:period_start).sum("rails_pulse_summaries.error_count")
|
|
60
|
-
grouped_counts = sparkline_query.group_by_date(:period_start).sum("rails_pulse_summaries.count")
|
|
61
|
-
end
|
|
57
|
+
grouped_errors = bucket_by_period(sparkline_query) { |relation| relation.sum("rails_pulse_summaries.error_count") }
|
|
58
|
+
grouped_counts = bucket_by_period(sparkline_query) { |relation| relation.sum("rails_pulse_summaries.count") }
|
|
62
59
|
|
|
63
60
|
sparkline_data = sparkline_from_failure_rates(grouped_errors, grouped_counts)
|
|
64
61
|
|
|
@@ -88,25 +85,15 @@ module RailsPulse
|
|
|
88
85
|
|
|
89
86
|
def sparkline_from_failure_rates(errors_by_period, counts_by_period)
|
|
90
87
|
if @period_type == "hour"
|
|
91
|
-
|
|
92
|
-
end_time = now.beginning_of_hour
|
|
93
|
-
result = {}
|
|
94
|
-
|
|
95
|
-
current_time = start_time
|
|
96
|
-
while current_time <= end_time
|
|
88
|
+
sparkline_hours.each_with_object({}) do |current_time, result|
|
|
97
89
|
errors = errors_by_period[current_time].to_f
|
|
98
90
|
total = counts_by_period[current_time].to_f
|
|
99
91
|
rate = total.zero? ? 0.0 : (errors / total * 100).round(1)
|
|
100
92
|
# Use timestamp in milliseconds as key to preserve uniqueness across days
|
|
101
93
|
result[current_time.to_i * 1000] = { value: rate }
|
|
102
|
-
current_time += 1.hour
|
|
103
94
|
end
|
|
104
|
-
result
|
|
105
95
|
else
|
|
106
|
-
|
|
107
|
-
end_date = now.to_date
|
|
108
|
-
|
|
109
|
-
(start_date..end_date).each_with_object({}) do |day, hash|
|
|
96
|
+
sparkline_dates.each_with_object({}) do |day, hash|
|
|
110
97
|
errors = errors_by_period[day].to_f
|
|
111
98
|
total = counts_by_period[day].to_f
|
|
112
99
|
rate = total.zero? ? 0.0 : (errors / total * 100).round(1)
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Jobs
|
|
3
3
|
module Cards
|
|
4
4
|
class P95Duration < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(job: nil, disabled_tags: [], show_non_tagged: true, period: 14, period_type: "day")
|
|
5
|
+
def initialize(job: nil, disabled_tags: [], show_non_tagged: true, period: 14, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@job = job
|
|
7
7
|
@disabled_tags = disabled_tags
|
|
8
8
|
@show_non_tagged = show_non_tagged
|
|
9
9
|
@period = period
|
|
10
10
|
@period_type = period_type
|
|
11
|
+
@start_time = start_time
|
|
12
|
+
@end_time = end_time
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_metric_card
|
|
@@ -48,13 +50,8 @@ module RailsPulse
|
|
|
48
50
|
trend_icon, trend_amount = trend_for(current_p95, previous_p95)
|
|
49
51
|
end
|
|
50
52
|
|
|
51
|
-
grouped_weighted = base_query
|
|
52
|
-
|
|
53
|
-
.sum(Arel.sql("rails_pulse_summaries.p95_duration * rails_pulse_summaries.count"))
|
|
54
|
-
|
|
55
|
-
grouped_counts = base_query
|
|
56
|
-
.group_by_date(:period_start)
|
|
57
|
-
.sum("rails_pulse_summaries.count")
|
|
53
|
+
grouped_weighted = bucket_by_period(base_query) { |relation| relation.sum(Arel.sql("rails_pulse_summaries.p95_duration * rails_pulse_summaries.count")) }
|
|
54
|
+
grouped_counts = bucket_by_period(base_query) { |relation| relation.sum("rails_pulse_summaries.count") }
|
|
58
55
|
|
|
59
56
|
sparkline_data = sparkline_from_averages(grouped_weighted, grouped_counts)
|
|
60
57
|
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Jobs
|
|
3
3
|
module Cards
|
|
4
4
|
class TotalRuns < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(job: nil, disabled_tags: [], show_non_tagged: true, period: 14, period_type: "day")
|
|
5
|
+
def initialize(job: nil, disabled_tags: [], show_non_tagged: true, period: 14, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@job = job
|
|
7
7
|
@disabled_tags = disabled_tags
|
|
8
8
|
@show_non_tagged = show_non_tagged
|
|
9
9
|
@period = period
|
|
10
10
|
@period_type = period_type
|
|
11
|
+
@start_time = start_time
|
|
12
|
+
@end_time = end_time
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_metric_card
|
|
@@ -32,9 +34,7 @@ module RailsPulse
|
|
|
32
34
|
|
|
33
35
|
trend_icon, trend_amount = trend_for(current_runs, previous_runs) if show_trend?
|
|
34
36
|
|
|
35
|
-
grouped_runs = base_query
|
|
36
|
-
.group_by_date(:period_start)
|
|
37
|
-
.sum("rails_pulse_summaries.count")
|
|
37
|
+
grouped_runs = bucket_by_period(base_query) { |relation| relation.sum("rails_pulse_summaries.count") }
|
|
38
38
|
|
|
39
39
|
{
|
|
40
40
|
id: "jobs_total_runs",
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class AverageQueryTimes < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(query: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day")
|
|
5
|
+
def initialize(query: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@query = query
|
|
7
7
|
@disabled_tags = disabled_tags
|
|
8
8
|
@show_non_tagged = show_non_tagged
|
|
9
9
|
@period = period
|
|
10
10
|
@period_type = period_type
|
|
11
|
+
@start_time = start_time
|
|
12
|
+
@end_time = end_time
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_metric_card
|
|
@@ -32,16 +34,11 @@ module RailsPulse
|
|
|
32
34
|
trend_icon, trend_amount = trend_for(current_period_avg, previous_period_avg) if show_trend?
|
|
33
35
|
|
|
34
36
|
# Sparkline data with zero-filled periods
|
|
35
|
-
grouped_weighted = base_query
|
|
36
|
-
|
|
37
|
-
.sum(Arel.sql("avg_duration * count"))
|
|
38
|
-
|
|
39
|
-
grouped_counts = base_query
|
|
40
|
-
.group_by_date(:period_start)
|
|
41
|
-
.sum(:count)
|
|
37
|
+
grouped_weighted = bucket_by_period(base_query) { |relation| relation.sum(Arel.sql("avg_duration * count")) }
|
|
38
|
+
grouped_counts = bucket_by_period(base_query) { |relation| relation.sum(:count) }
|
|
42
39
|
|
|
43
40
|
# Calculate weighted averages for each period
|
|
44
|
-
averages_by_period = grouped_weighted.
|
|
41
|
+
averages_by_period = grouped_weighted.transform_values.with_index do |(weighted, day), _|
|
|
45
42
|
count = grouped_counts[day] || 0
|
|
46
43
|
count > 0 ? (weighted.to_f / count).round(0) : 0
|
|
47
44
|
end
|
|
@@ -2,11 +2,13 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class DatabaseLoad < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day")
|
|
5
|
+
def initialize(disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@disabled_tags = disabled_tags
|
|
7
7
|
@show_non_tagged = show_non_tagged
|
|
8
8
|
@period = period
|
|
9
9
|
@period_type = period_type
|
|
10
|
+
@start_time = start_time
|
|
11
|
+
@end_time = end_time
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def to_metric_card
|
|
@@ -63,8 +65,8 @@ module RailsPulse
|
|
|
63
65
|
|
|
64
66
|
# Sparkline data - group by hour or day depending on period_type
|
|
65
67
|
if period_type_hours?
|
|
66
|
-
start_time =
|
|
67
|
-
end_time =
|
|
68
|
+
start_time = sparkline_hours.first
|
|
69
|
+
end_time = sparkline_hours.last
|
|
68
70
|
|
|
69
71
|
# Create separate queries for sparkline data using only the current period
|
|
70
72
|
sparkline_query_summaries = RailsPulse::Summary
|
|
@@ -91,8 +93,7 @@ module RailsPulse
|
|
|
91
93
|
.transform_values { |summaries| summaries.sum { |s| s.total_duration || 0 } }
|
|
92
94
|
|
|
93
95
|
sparkline_data = {}
|
|
94
|
-
|
|
95
|
-
while current_time <= end_time
|
|
96
|
+
sparkline_hours.each do |current_time|
|
|
96
97
|
query_time = hourly_query_time[current_time] || 0
|
|
97
98
|
request_time = hourly_request_time[current_time] || 0
|
|
98
99
|
percentage = request_time > 0 ? (query_time.to_f / request_time * 100).round(1) : 0
|
|
@@ -111,12 +112,8 @@ module RailsPulse
|
|
|
111
112
|
value: percentage,
|
|
112
113
|
itemStyle: { color: color }
|
|
113
114
|
}
|
|
114
|
-
current_time += 1.hour
|
|
115
115
|
end
|
|
116
116
|
else
|
|
117
|
-
start_day = current_window_start.to_date
|
|
118
|
-
end_day = now.to_date
|
|
119
|
-
|
|
120
117
|
# Group by day
|
|
121
118
|
daily_query_time = query_summaries.group_by { |s| s.period_start.to_date }
|
|
122
119
|
.transform_values { |summaries| summaries.sum { |s| s.total_duration || 0 } }
|
|
@@ -125,7 +122,7 @@ module RailsPulse
|
|
|
125
122
|
.transform_values { |summaries| summaries.sum { |s| s.total_duration || 0 } }
|
|
126
123
|
|
|
127
124
|
sparkline_data = {}
|
|
128
|
-
|
|
125
|
+
sparkline_dates.each do |day|
|
|
129
126
|
query_time = daily_query_time[day] || 0
|
|
130
127
|
request_time = daily_request_time[day] || 0
|
|
131
128
|
percentage = request_time > 0 ? (query_time.to_f / request_time * 100).round(1) : 0
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class ExecutionRate < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(query: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day")
|
|
5
|
+
def initialize(query: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@query = query
|
|
7
7
|
@disabled_tags = disabled_tags
|
|
8
8
|
@show_non_tagged = show_non_tagged
|
|
9
9
|
@period = period
|
|
10
10
|
@period_type = period_type
|
|
11
|
+
@start_time = start_time
|
|
12
|
+
@end_time = end_time
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_metric_card
|
|
@@ -38,11 +40,7 @@ module RailsPulse
|
|
|
38
40
|
)
|
|
39
41
|
sparkline_query = sparkline_query.where(summarizable_id: @query.id) if @query
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
grouped_data = sparkline_query.group_by_hour(:period_start).sum(:count)
|
|
43
|
-
else
|
|
44
|
-
grouped_data = sparkline_query.group_by_date(:period_start).sum(:count)
|
|
45
|
-
end
|
|
43
|
+
grouped_data = bucket_by_period(sparkline_query) { |relation| relation.sum(:count) }
|
|
46
44
|
|
|
47
45
|
# Use base class sparkline generation (handles hour vs day automatically)
|
|
48
46
|
sparkline_data = sparkline_from(grouped_data)
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class PercentileQueryTimes < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(query: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day")
|
|
5
|
+
def initialize(query: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@query = query
|
|
7
7
|
@disabled_tags = disabled_tags
|
|
8
8
|
@show_non_tagged = show_non_tagged
|
|
9
9
|
@period = period
|
|
10
10
|
@period_type = period_type
|
|
11
|
+
@start_time = start_time
|
|
12
|
+
@end_time = end_time
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_metric_card
|
|
@@ -44,13 +46,8 @@ module RailsPulse
|
|
|
44
46
|
)
|
|
45
47
|
sparkline_query = sparkline_query.where(summarizable_id: @query.id) if @query
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
period_counts = sparkline_query.group_by_hour(:period_start).sum(:count)
|
|
50
|
-
else
|
|
51
|
-
weighted_sums = sparkline_query.group_by_date(:period_start).sum("p95_duration * count")
|
|
52
|
-
period_counts = sparkline_query.group_by_date(:period_start).sum(:count)
|
|
53
|
-
end
|
|
49
|
+
weighted_sums = bucket_by_period(sparkline_query) { |relation| relation.sum("p95_duration * count") }
|
|
50
|
+
period_counts = bucket_by_period(sparkline_query) { |relation| relation.sum(:count) }
|
|
54
51
|
|
|
55
52
|
# Calculate weighted averages for each period
|
|
56
53
|
averages_by_period = weighted_sums.each_with_object({}) do |(period, weighted), hash|
|
|
@@ -70,11 +70,17 @@ module RailsPulse
|
|
|
70
70
|
# True when at least one stored route still has no action but the live
|
|
71
71
|
# host router would assign one — the usual sign that schema migrated
|
|
72
72
|
# without `rails rails_pulse:migrate_routes`.
|
|
73
|
+
# Runs on every dashboard page, so the common case (every route already
|
|
74
|
+
# has an action) must be one indexed query; router recognition only
|
|
75
|
+
# happens when there is something to recognise.
|
|
73
76
|
def self.needs_action_backfill?
|
|
74
77
|
return false unless column_names.include?("controller_action")
|
|
75
78
|
return false unless column_names.include?("http_methods")
|
|
76
79
|
|
|
77
|
-
where(controller_action: [ nil, "" ])
|
|
80
|
+
blank_action = where(controller_action: [ nil, "" ])
|
|
81
|
+
return false unless blank_action.exists?
|
|
82
|
+
|
|
83
|
+
blank_action.limit(25).any? do |route|
|
|
78
84
|
method = route.http_methods_list.first
|
|
79
85
|
next false if method.blank?
|
|
80
86
|
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Cards
|
|
4
4
|
class ErrorRates < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(route: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day")
|
|
5
|
+
def initialize(route: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@route = route
|
|
7
7
|
@disabled_tags = disabled_tags
|
|
8
8
|
@show_non_tagged = show_non_tagged
|
|
9
9
|
@period = period
|
|
10
10
|
@period_type = period_type
|
|
11
|
+
@start_time = start_time
|
|
12
|
+
@end_time = end_time
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_metric_card
|
|
@@ -76,22 +78,14 @@ module RailsPulse
|
|
|
76
78
|
|
|
77
79
|
def sparkline_from_error_rates(errors_by_period, client_errors_by_period, counts_by_period)
|
|
78
80
|
if @period_type == "hour"
|
|
79
|
-
|
|
80
|
-
end_time = now.beginning_of_hour
|
|
81
|
-
result = {}
|
|
82
|
-
current_time = start_time
|
|
83
|
-
while current_time <= end_time
|
|
81
|
+
sparkline_hours.each_with_object({}) do |current_time, result|
|
|
84
82
|
errors = errors_by_period[current_time].to_f + client_errors_by_period[current_time].to_f
|
|
85
83
|
total = counts_by_period[current_time].to_f
|
|
86
84
|
rate = total.zero? ? 0.0 : (errors / total * 100).round(2)
|
|
87
85
|
result[current_time.to_i * 1000] = { value: rate }
|
|
88
|
-
current_time += 1.hour
|
|
89
86
|
end
|
|
90
|
-
result
|
|
91
87
|
else
|
|
92
|
-
|
|
93
|
-
end_date = now.to_date
|
|
94
|
-
(start_date..end_date).each_with_object({}) do |day, hash|
|
|
88
|
+
sparkline_dates.each_with_object({}) do |day, hash|
|
|
95
89
|
errors = errors_by_period[day].to_f + client_errors_by_period[day].to_f
|
|
96
90
|
total = counts_by_period[day].to_f
|
|
97
91
|
rate = total.zero? ? 0.0 : (errors / total * 100).round(2)
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Cards
|
|
4
4
|
class PercentileResponseTimes < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(route: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day")
|
|
5
|
+
def initialize(route: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@route = route
|
|
7
7
|
@disabled_tags = disabled_tags
|
|
8
8
|
@show_non_tagged = show_non_tagged
|
|
9
9
|
@period = period
|
|
10
10
|
@period_type = period_type
|
|
11
|
+
@start_time = start_time
|
|
12
|
+
@end_time = end_time
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_metric_card
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Cards
|
|
4
4
|
class RequestCountTotals < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(route: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day")
|
|
5
|
+
def initialize(route: nil, disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day", start_time: nil, end_time: nil)
|
|
6
6
|
@route = route
|
|
7
7
|
@disabled_tags = disabled_tags
|
|
8
8
|
@show_non_tagged = show_non_tagged
|
|
9
9
|
@period = period
|
|
10
10
|
@period_type = period_type
|
|
11
|
+
@start_time = start_time
|
|
12
|
+
@end_time = end_time
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_metric_card
|
|
@@ -42,11 +44,7 @@ module RailsPulse
|
|
|
42
44
|
)
|
|
43
45
|
sparkline_query = sparkline_query.where(summarizable_id: @route.id) if @route
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
grouped_data = sparkline_query.group_by_hour(:period_start).sum(:count)
|
|
47
|
-
else
|
|
48
|
-
grouped_data = sparkline_query.group_by_date(:period_start).sum(:count)
|
|
49
|
-
end
|
|
47
|
+
grouped_data = bucket_by_period(sparkline_query) { |relation| relation.sum(:count) }
|
|
50
48
|
|
|
51
49
|
# Use base class sparkline generation (handles hour vs day automatically)
|
|
52
50
|
sparkline_data = sparkline_from(grouped_data)
|
|
@@ -55,26 +55,21 @@ module RailsPulse
|
|
|
55
55
|
# Return early if no filters are applied
|
|
56
56
|
return all if actual_disabled_tags.empty? && show_non_tagged
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
# One subquery per summarizable kind. This scope runs on every card and
|
|
59
|
+
# chart query, so the alternative (plucking every matching route, query
|
|
60
|
+
# and job id and inlining the lists) cost three extra statements per
|
|
61
|
+
# call and grew each statement with the size of the tables.
|
|
62
|
+
table = arel_table
|
|
63
|
+
keep_for = lambda do |type, model|
|
|
64
|
+
matching = TagFilterService.scope(model, actual_disabled_tags, show_non_tagged).select(:id).arel
|
|
65
|
+
table[:summarizable_type].eq(type).and(table[:summarizable_id].in(matching))
|
|
66
|
+
end
|
|
64
67
|
|
|
65
|
-
# Apply filters: include only summaries for filtered routes/queries/jobs
|
|
66
|
-
# Use -1 as an impossible ID when no items match the filter
|
|
67
68
|
where(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
" (summarizable_type = 'RailsPulse::Request') OR " \
|
|
73
|
-
" (summarizable_type = 'RailsPulse::ExceptionGroup')" \
|
|
74
|
-
")",
|
|
75
|
-
route_ids,
|
|
76
|
-
query_ids,
|
|
77
|
-
job_ids
|
|
69
|
+
keep_for.call("RailsPulse::Route", Route)
|
|
70
|
+
.or(keep_for.call("RailsPulse::Query", Query))
|
|
71
|
+
.or(keep_for.call("RailsPulse::Job", Job))
|
|
72
|
+
.or(table[:summarizable_type].in([ "RailsPulse::Request", "RailsPulse::ExceptionGroup" ]))
|
|
78
73
|
)
|
|
79
74
|
}
|
|
80
75
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
# Normalizes the time range preference stored in the session so every
|
|
3
|
+
# reader sees the same shape regardless of which session store wrote it.
|
|
4
|
+
#
|
|
5
|
+
# The cookie store round-trips through JSON and stringifies hash keys, but a
|
|
6
|
+
# Marshal-backed store (activerecord-session_store's default) hands back the
|
|
7
|
+
# symbol keys the writer used. Readers that only knew one shape raised
|
|
8
|
+
# NoMethodError on the other and locked the dashboard into 500s for the rest
|
|
9
|
+
# of the browser session.
|
|
10
|
+
module TimeRangePreference
|
|
11
|
+
# A preset name as a String, a custom range as a string-keyed Hash, or nil
|
|
12
|
+
# for anything unrecognized so callers fall back to their defaults.
|
|
13
|
+
def self.normalize(value)
|
|
14
|
+
case value
|
|
15
|
+
when Hash
|
|
16
|
+
value.to_h.transform_keys(&:to_s)
|
|
17
|
+
when String, Symbol
|
|
18
|
+
value.to_s
|
|
19
|
+
else
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.custom?(preference)
|
|
25
|
+
preference.is_a?(Hash) && preference["type"] == "custom"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
# A resolved start/end range plus the bucketing helpers charts and cards
|
|
3
|
+
# need: day count, calendar dates/hour-starts inside it, and the
|
|
4
|
+
# equal-length prior window for comparisons.
|
|
5
|
+
class TimeWindow
|
|
6
|
+
attr_reader :start_time, :end_time
|
|
7
|
+
|
|
8
|
+
# nil (not raised) when either bound is missing — callers already have
|
|
9
|
+
# a "no range selected" fallback to use instead.
|
|
10
|
+
def self.build(start_time, end_time)
|
|
11
|
+
return nil if start_time.nil? || end_time.nil?
|
|
12
|
+
|
|
13
|
+
new(start_time, end_time)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(start_time, end_time)
|
|
17
|
+
@start_time = coerce(start_time)
|
|
18
|
+
@end_time = coerce(end_time)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Whole days spanned, rounded rather than truncated (a range a few
|
|
22
|
+
# seconds short of N days still counts as N). Always at least 1.
|
|
23
|
+
def days
|
|
24
|
+
[ ((end_time - start_time) / 1.day).round, 1 ].max
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Every full calendar day inside the window, ascending. Skips a leading
|
|
28
|
+
# day whose midnight falls before the window start.
|
|
29
|
+
def dates
|
|
30
|
+
first_date = start_time.beginning_of_day == start_time ? start_time.to_date : (start_time.to_date + 1)
|
|
31
|
+
return [] if first_date > end_time.to_date
|
|
32
|
+
|
|
33
|
+
(first_date..end_time.to_date).to_a
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Every hour-start inside the window, ascending.
|
|
37
|
+
def hour_starts
|
|
38
|
+
starts = []
|
|
39
|
+
current = start_time.beginning_of_hour
|
|
40
|
+
current += 1.hour if current < start_time
|
|
41
|
+
while current <= end_time
|
|
42
|
+
starts << current
|
|
43
|
+
current += 1.hour
|
|
44
|
+
end
|
|
45
|
+
starts
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The window of the same length immediately preceding this one, for
|
|
49
|
+
# period-over-period comparisons. `unit` is "day" or "hour".
|
|
50
|
+
def previous(unit)
|
|
51
|
+
span = unit == "hour" ? (end_time - start_time) : days.days
|
|
52
|
+
self.class.new(start_time - span, start_time)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def coerce(value)
|
|
58
|
+
value.is_a?(Numeric) ? Time.zone.at(value) : value.in_time_zone
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|