rails_pulse 0.4.2 → 0.5.0.pre.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/CHANGELOG.md +58 -1
- data/README.md +15 -4
- data/app/assets/stylesheets/rails_pulse/components/chart.css +11 -0
- data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +19 -1
- data/app/assets/stylesheets/rails_pulse/components/row.css +3 -17
- data/app/controllers/concerns/chart_table_concern.rb +28 -69
- data/app/controllers/concerns/deployment_markers_concern.rb +3 -1
- data/app/controllers/concerns/metric_card_concern.rb +6 -7
- data/app/controllers/rails_pulse/api/v1/base_controller.rb +133 -0
- data/app/controllers/rails_pulse/api/v1/capabilities_controller.rb +27 -0
- data/app/controllers/rails_pulse/api/v1/coverage_controller.rb +158 -0
- data/app/controllers/rails_pulse/api/v1/deployments_controller.rb +40 -0
- data/app/controllers/rails_pulse/api/v1/exceptions_controller.rb +77 -0
- data/app/controllers/rails_pulse/api/v1/insights_controller.rb +62 -0
- data/app/controllers/rails_pulse/api/v1/job_runs_controller.rb +34 -0
- data/app/controllers/rails_pulse/api/v1/jobs_controller.rb +172 -0
- data/app/controllers/rails_pulse/api/v1/queries_controller.rb +123 -0
- data/app/controllers/rails_pulse/api/v1/requests_controller.rb +74 -0
- data/app/controllers/rails_pulse/api/v1/routes_controller.rb +103 -0
- data/app/controllers/rails_pulse/application_controller.rb +1 -1
- data/app/controllers/rails_pulse/dashboard_controller.rb +25 -21
- data/app/controllers/rails_pulse/deployments_controller.rb +23 -13
- data/app/controllers/rails_pulse/exceptions_controller.rb +10 -12
- data/app/controllers/rails_pulse/jobs_controller.rb +1 -2
- data/app/controllers/rails_pulse/queries_controller.rb +6 -5
- data/app/controllers/rails_pulse/requests_controller.rb +15 -14
- data/app/controllers/rails_pulse/routes_controller.rb +4 -8
- data/app/helpers/rails_pulse/application_helper.rb +15 -0
- data/app/helpers/rails_pulse/chart_helper.rb +9 -2
- data/app/helpers/rails_pulse/formatting_helper.rb +11 -12
- data/app/javascript/rails_pulse/controllers/chart_controller.js +80 -43
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +4 -2
- data/app/jobs/rails_pulse/cloud_health_job.rb +11 -0
- data/app/jobs/rails_pulse/cloud_sync_job.rb +11 -0
- data/app/jobs/rails_pulse/summary_job.rb +13 -0
- data/app/models/concerns/rails_pulse/has_metadata.rb +15 -0
- data/app/models/concerns/rails_pulse/taggable.rb +5 -12
- data/app/models/rails_pulse/cards/base.rb +20 -11
- data/app/models/rails_pulse/charts/base.rb +5 -18
- data/app/models/rails_pulse/charts/percentile_chart_base.rb +3 -5
- data/app/models/rails_pulse/cleanup_run.rb +32 -0
- data/app/models/rails_pulse/cloud/buffered_batch.rb +55 -0
- data/app/models/rails_pulse/cloud/installation.rb +55 -0
- data/app/models/rails_pulse/dashboard/charts/response_time_percentiles.rb +4 -5
- data/app/models/rails_pulse/dashboard/charts/throughput_and_errors.rb +4 -5
- data/app/models/rails_pulse/dashboard/concerns/attention_classification.rb +104 -0
- data/app/models/rails_pulse/dashboard/concerns/time_range_helper.rb +25 -2
- data/app/models/rails_pulse/dashboard/health_summary.rb +38 -13
- data/app/models/rails_pulse/dashboard/needs_attention.rb +31 -98
- data/app/models/rails_pulse/dashboard/storage_pressure.rb +39 -63
- data/app/models/rails_pulse/dashboard/storage_status.rb +130 -39
- data/app/models/rails_pulse/deployment.rb +2 -7
- data/app/models/rails_pulse/event.rb +41 -0
- data/app/models/rails_pulse/exception_occurrence.rb +1 -1
- data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +3 -6
- data/app/models/rails_pulse/exceptions/cards/open_groups.rb +1 -4
- data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +3 -6
- data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +4 -7
- data/app/models/rails_pulse/job_run.rb +1 -1
- data/app/models/rails_pulse/jobs/cards/failure_rate.rb +8 -14
- data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -14
- data/app/models/rails_pulse/jobs/cards/total_runs.rb +5 -11
- data/app/models/rails_pulse/jobs/charts/duration.rb +1 -2
- data/app/models/rails_pulse/jobs/charts/execution_volume.rb +1 -1
- data/app/models/rails_pulse/jobs/charts/failure_rate.rb +1 -2
- data/app/models/rails_pulse/like_pattern.rb +35 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +6 -12
- data/app/models/rails_pulse/queries/cards/database_load.rb +0 -9
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +5 -11
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +6 -12
- data/app/models/rails_pulse/queries/charts/database_load.rb +5 -8
- data/app/models/rails_pulse/queries/charts/execution_volume.rb +1 -1
- data/app/models/rails_pulse/request.rb +1 -1
- data/app/models/rails_pulse/routes/cards/error_rates.rb +5 -15
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +5 -17
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +5 -17
- data/app/models/rails_pulse/routes/charts/error_rate.rb +1 -1
- data/app/models/rails_pulse/routes/charts/request_volume.rb +1 -1
- data/app/models/rails_pulse/summary.rb +5 -0
- data/app/models/rails_pulse/tables/base.rb +5 -6
- data/app/models/rails_pulse/time_range.rb +309 -0
- data/app/models/rails_pulse/time_window.rb +15 -0
- data/app/models/rails_pulse/writer_heartbeat.rb +90 -0
- data/app/serializers/rails_pulse/api/v1/deployment_serializer.rb +23 -0
- data/app/serializers/rails_pulse/api/v1/exception_group_serializer.rb +23 -0
- data/app/serializers/rails_pulse/api/v1/exception_occurrence_serializer.rb +22 -0
- data/app/serializers/rails_pulse/api/v1/job_run_serializer.rb +28 -0
- data/app/serializers/rails_pulse/api/v1/job_serializer.rb +26 -0
- data/app/serializers/rails_pulse/api/v1/query_serializer.rb +25 -0
- data/app/serializers/rails_pulse/api/v1/request_serializer.rb +22 -0
- data/app/serializers/rails_pulse/api/v1/route_serializer.rb +19 -0
- data/app/services/rails_pulse/cloud/batch.rb +83 -0
- data/app/services/rails_pulse/cloud/client.rb +68 -0
- data/app/services/rails_pulse/cloud/combined_metrics.rb +49 -0
- data/app/services/rails_pulse/cloud/health_item.rb +77 -0
- data/app/services/rails_pulse/cloud/path_prefix.rb +50 -0
- data/app/services/rails_pulse/cloud/preview.rb +83 -0
- data/app/services/rails_pulse/cloud/query_shape.rb +195 -0
- data/app/services/rails_pulse/cloud/record_items.rb +60 -0
- data/app/services/rails_pulse/cloud/route_pattern.rb +111 -0
- data/app/services/rails_pulse/cloud/summary_items.rb +188 -0
- data/app/services/rails_pulse/cloud/sync.rb +275 -0
- data/app/services/rails_pulse/config_recommendations.rb +132 -0
- data/app/services/rails_pulse/deployment_comparison.rb +177 -0
- data/app/services/rails_pulse/exception_capture_service.rb +1 -1
- data/app/services/rails_pulse/operations/change_point.rb +1 -1
- data/app/services/rails_pulse/operations/comparison.rb +1 -1
- data/app/services/rails_pulse/period_insights.rb +131 -0
- data/app/services/rails_pulse/sql_query_normalizer.rb +222 -122
- data/app/services/rails_pulse/summary_service/from_child_periods.rb +161 -0
- data/app/services/rails_pulse/summary_service/from_raw_rows.rb +115 -0
- data/app/services/rails_pulse/summary_service/metrics.rb +38 -0
- data/app/services/rails_pulse/summary_service.rb +121 -133
- data/app/services/rails_pulse/tag_filter_service.rb +5 -5
- data/app/views/layouts/rails_pulse/_time_range_selector.html.erb +4 -1
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
- data/app/views/rails_pulse/components/_table_head.html.erb +2 -2
- data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -1
- data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +2 -2
- data/app/views/rails_pulse/dashboard/_health_badge.html.erb +29 -20
- data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +13 -3
- data/app/views/rails_pulse/dashboard/index.html.erb +16 -2
- data/app/views/rails_pulse/jobs/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/jobs/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/queries/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/show.html.erb +1 -1
- data/app/views/rails_pulse/requests/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/routes/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/show.html.erb +1 -1
- data/app/views/rails_pulse/shared/_aggregation_zone_badge.html.erb +4 -0
- data/app/views/rails_pulse/storage/show.html.erb +39 -2
- data/config/routes.rb +23 -0
- data/db/rails_pulse_cloud_schema.rb +58 -0
- data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +1 -1
- data/db/rails_pulse_migrate/20260921000001_create_rails_pulse_events.rb +21 -0
- data/db/rails_pulse_schema.rb +20 -2
- data/exe/rails-pulse +7 -0
- data/lib/generators/rails_pulse/base_methods.rb +19 -0
- data/lib/generators/rails_pulse/install_cloud_generator.rb +84 -0
- data/lib/generators/rails_pulse/templates/db/rails_pulse_cloud_schema.rb +58 -0
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +20 -2
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_cloud_tables.rb +30 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +71 -19
- data/lib/generators/rails_pulse/upgrade_generator.rb +9 -16
- data/lib/rails_pulse/cleanup_service.rb +37 -20
- data/lib/rails_pulse/cli/agent_files/agents.md +89 -0
- data/lib/rails_pulse/cli/agent_files/claude_skill.md +182 -0
- data/lib/rails_pulse/cli/base_command.rb +37 -0
- data/lib/rails_pulse/cli/client.rb +64 -0
- data/lib/rails_pulse/cli/config.rb +87 -0
- data/lib/rails_pulse/cli/configure.rb +75 -0
- data/lib/rails_pulse/cli/coverage.rb +91 -0
- data/lib/rails_pulse/cli/deployments.rb +54 -0
- data/lib/rails_pulse/cli/exceptions.rb +116 -0
- data/lib/rails_pulse/cli/formatter.rb +31 -0
- data/lib/rails_pulse/cli/insights.rb +70 -0
- data/lib/rails_pulse/cli/install.rb +145 -0
- data/lib/rails_pulse/cli/job_runs.rb +53 -0
- data/lib/rails_pulse/cli/jobs.rb +71 -0
- data/lib/rails_pulse/cli/main.rb +76 -0
- data/lib/rails_pulse/cli/mcp.rb +47 -0
- data/lib/rails_pulse/cli/queries.rb +67 -0
- data/lib/rails_pulse/cli/requests.rb +57 -0
- data/lib/rails_pulse/cli/routes.rb +73 -0
- data/lib/rails_pulse/configuration.rb +100 -2
- data/lib/rails_pulse/current.rb +11 -0
- data/lib/rails_pulse/engine.rb +12 -3
- data/lib/rails_pulse/installers/config_updater.rb +9 -2
- data/lib/rails_pulse/job_run_collector.rb +15 -15
- data/lib/rails_pulse/mcp/server.rb +78 -0
- data/lib/rails_pulse/mcp/tools/coverage.rb +96 -0
- data/lib/rails_pulse/mcp/tools/deployments.rb +117 -0
- data/lib/rails_pulse/mcp/tools/endpoint.rb +167 -0
- data/lib/rails_pulse/mcp/tools/errors.rb +104 -0
- data/lib/rails_pulse/mcp/tools/exception_detail.rb +118 -0
- data/lib/rails_pulse/mcp/tools/exceptions.rb +128 -0
- data/lib/rails_pulse/mcp/tools/helpers.rb +162 -0
- data/lib/rails_pulse/mcp/tools/insights.rb +99 -0
- data/lib/rails_pulse/mcp/tools/jobs.rb +155 -0
- data/lib/rails_pulse/mcp/tools/queries.rb +142 -0
- data/lib/rails_pulse/mcp/tools/routes.rb +108 -0
- data/lib/rails_pulse/mcp/tools/slow_requests.rb +118 -0
- data/lib/rails_pulse/middleware/request_collector.rb +9 -9
- data/lib/rails_pulse/scoped_inflector.rb +35 -0
- data/lib/rails_pulse/statistics.rb +27 -0
- data/lib/rails_pulse/subscribers/exception_subscriber.rb +2 -2
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +15 -29
- data/lib/rails_pulse/tasks/status_reporter.rb +201 -0
- data/lib/rails_pulse/tracker.rb +76 -14
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +0 -4
- data/lib/tasks/rails_pulse.rake +6 -0
- data/lib/tasks/rails_pulse_tasks.rake +7 -0
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +13 -13
- metadata +123 -24
- data/app/controllers/concerns/page_timings.rb +0 -10
- data/app/controllers/concerns/response_range_concern.rb +0 -45
- data/app/controllers/concerns/time_range_concern.rb +0 -152
- data/app/controllers/concerns/zoom_range_concern.rb +0 -86
|
@@ -2,13 +2,12 @@ module RailsPulse
|
|
|
2
2
|
module Dashboard
|
|
3
3
|
module Charts
|
|
4
4
|
class ThroughputAndErrors
|
|
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", window: 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
|
-
@
|
|
11
|
-
@end_time = end_time
|
|
10
|
+
@window = window
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def to_chart_data
|
|
@@ -125,10 +124,10 @@ module RailsPulse
|
|
|
125
124
|
|
|
126
125
|
private
|
|
127
126
|
|
|
128
|
-
# nil unless
|
|
127
|
+
# nil unless a window was given, so the default
|
|
129
128
|
# "recent" view keeps using the trailing-@period-days fallback below.
|
|
130
129
|
def time_window
|
|
131
|
-
@
|
|
130
|
+
@window
|
|
132
131
|
end
|
|
133
132
|
|
|
134
133
|
def default_hour_range
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Dashboard
|
|
3
|
+
module Concerns
|
|
4
|
+
# How a route, query or job earns a place in a needs-attention list,
|
|
5
|
+
# shared by the dashboard panel (NeedsAttention, over the selected
|
|
6
|
+
# window) and the API's PeriodInsights (over one summary period) so the
|
|
7
|
+
# two never disagree about what is critical.
|
|
8
|
+
#
|
|
9
|
+
# Each classify method returns nil for something healthy, otherwise
|
|
10
|
+
# [severity, reason, metric, metric_sub, sort_score]. The includer sets
|
|
11
|
+
# @route_thresholds, @query_thresholds and @job_thresholds.
|
|
12
|
+
module AttentionClassification
|
|
13
|
+
include ThresholdConstants
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# `span` finishes the error-rate reason: "this week", "this period".
|
|
18
|
+
def classify_route(p95, total, errors, error_rate, span)
|
|
19
|
+
if p95 >= @route_thresholds[:critical] || error_rate >= CRITICAL_ERROR_RATE
|
|
20
|
+
if error_rate >= CRITICAL_ERROR_RATE
|
|
21
|
+
[ :critical,
|
|
22
|
+
"#{error_rate}% error rate · #{total} requests #{span}",
|
|
23
|
+
"#{errors} errors",
|
|
24
|
+
"P95 #{p95.round(0).to_i}ms",
|
|
25
|
+
errors * total.to_f ]
|
|
26
|
+
else
|
|
27
|
+
[ :critical,
|
|
28
|
+
"#{p95.round(0).to_i}ms P95 · exceeds #{@route_thresholds[:critical]}ms threshold",
|
|
29
|
+
"#{p95.round(0).to_i}ms P95",
|
|
30
|
+
"#{total} requests",
|
|
31
|
+
p95 ]
|
|
32
|
+
end
|
|
33
|
+
elsif p95 >= @route_thresholds[:slow] || error_rate >= WARNING_ERROR_RATE
|
|
34
|
+
if error_rate >= WARNING_ERROR_RATE && p95 < @route_thresholds[:slow]
|
|
35
|
+
[ :warning,
|
|
36
|
+
"#{error_rate}% error rate · #{total} requests #{span}",
|
|
37
|
+
"#{errors} errors",
|
|
38
|
+
"P95 #{p95.round(0).to_i}ms",
|
|
39
|
+
errors * total.to_f ]
|
|
40
|
+
else
|
|
41
|
+
[ :warning,
|
|
42
|
+
"#{p95.round(0).to_i}ms P95 · #{error_rate > 0 ? "#{error_rate}% error rate" : "above slow threshold"}",
|
|
43
|
+
"#{p95.round(0).to_i}ms P95",
|
|
44
|
+
"#{total} requests",
|
|
45
|
+
p95 ]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def classify_query(p95, count)
|
|
51
|
+
if p95 >= @query_thresholds[:critical]
|
|
52
|
+
severity = :critical
|
|
53
|
+
reason = "#{p95.round(0).to_i}ms P95 · exceeds #{@query_thresholds[:critical]}ms threshold"
|
|
54
|
+
elsif p95 >= @query_thresholds[:slow]
|
|
55
|
+
severity = :warning
|
|
56
|
+
reason = "#{p95.round(0).to_i}ms P95 · above #{@query_thresholds[:slow]}ms slow threshold"
|
|
57
|
+
else
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[ severity, reason, "#{p95.round(0).to_i}ms P95", "#{count} execution#{count == 1 ? "" : "s"}", p95 ]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def classify_job(failure_rate, p95, runs, failures, queue)
|
|
65
|
+
if failure_rate >= CRITICAL_JOB_FAILURE_RATE || p95 >= @job_thresholds[:critical]
|
|
66
|
+
if failure_rate >= CRITICAL_JOB_FAILURE_RATE
|
|
67
|
+
[ :critical,
|
|
68
|
+
"#{queue} queue · #{failure_rate}% failure rate",
|
|
69
|
+
"#{failures} / #{runs} failed",
|
|
70
|
+
"P95 #{p95.round(0).to_i}ms",
|
|
71
|
+
failures * runs.to_f ]
|
|
72
|
+
else
|
|
73
|
+
[ :critical,
|
|
74
|
+
"P95 #{p95.round(0).to_i}ms · exceeds #{@job_thresholds[:critical]}ms threshold",
|
|
75
|
+
"#{p95.round(0).to_i}ms P95",
|
|
76
|
+
"#{runs} runs",
|
|
77
|
+
p95 ]
|
|
78
|
+
end
|
|
79
|
+
elsif failure_rate >= WARNING_JOB_FAILURE_RATE || p95 >= @job_thresholds[:slow]
|
|
80
|
+
if failure_rate >= WARNING_JOB_FAILURE_RATE
|
|
81
|
+
[ :warning,
|
|
82
|
+
"#{queue} queue · #{failure_rate}% failure rate",
|
|
83
|
+
"#{failures} / #{runs} failed",
|
|
84
|
+
"P95 #{p95.round(0).to_i}ms",
|
|
85
|
+
failures * runs.to_f ]
|
|
86
|
+
else
|
|
87
|
+
[ :warning,
|
|
88
|
+
"P95 #{p95.round(0).to_i}ms · above slow threshold",
|
|
89
|
+
"#{p95.round(0).to_i}ms P95",
|
|
90
|
+
"#{runs} runs",
|
|
91
|
+
p95 ]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def truncate_sql(sql)
|
|
97
|
+
return "" if sql.blank?
|
|
98
|
+
cleaned = sql.gsub(/\s+/, " ").strip
|
|
99
|
+
cleaned.length > 80 ? "#{cleaned[0..79]}..." : cleaned
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -5,11 +5,34 @@ module RailsPulse
|
|
|
5
5
|
private
|
|
6
6
|
|
|
7
7
|
def period_range
|
|
8
|
-
|
|
9
|
-
return [ window.start_time, window.end_time ] if window
|
|
8
|
+
return [ @window.start_time, @window.end_time ] if @window
|
|
10
9
|
|
|
11
10
|
[ @period.days.ago.beginning_of_day, Time.current ]
|
|
12
11
|
end
|
|
12
|
+
|
|
13
|
+
# Summary rows that cover the period exactly once. The same traffic is
|
|
14
|
+
# summarized per hour, day, week and month, so summing across period
|
|
15
|
+
# types would count it once per granularity. A window of 25 hours or
|
|
16
|
+
# less reads hourly rows. A longer one reads daily rows up to the start
|
|
17
|
+
# of today, or to its own end if that comes first, and hourly rows
|
|
18
|
+
# after today's start, because a day is summarized only once it has
|
|
19
|
+
# ended.
|
|
20
|
+
def period_summaries
|
|
21
|
+
start, finish = period_range
|
|
22
|
+
return RailsPulse::Summary.where(period_type: "hour", period_start: start..finish) if hourly_period?(start, finish)
|
|
23
|
+
|
|
24
|
+
today = Time.zone.now.beginning_of_day
|
|
25
|
+
RailsPulse::Summary.where(period_type: "day", period_start: start...[ today, finish ].min)
|
|
26
|
+
.or(RailsPulse::Summary.where(period_type: "hour", period_start: today..finish))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The dashboard passes the period type TimeRange resolved, so these
|
|
30
|
+
# panels read the same granularity as the cards and charts beside them.
|
|
31
|
+
def hourly_period?(start, finish)
|
|
32
|
+
return @period_type == "hour" if @period_type
|
|
33
|
+
|
|
34
|
+
finish - start <= 25.hours
|
|
35
|
+
end
|
|
13
36
|
end
|
|
14
37
|
end
|
|
15
38
|
end
|
|
@@ -4,12 +4,13 @@ module RailsPulse
|
|
|
4
4
|
include Concerns::ThresholdConstants
|
|
5
5
|
include Concerns::TimeRangeHelper
|
|
6
6
|
|
|
7
|
-
def initialize(disabled_tags: [], show_non_tagged: true, period: 7,
|
|
7
|
+
def initialize(disabled_tags: [], show_non_tagged: true, period: 7, period_type: nil, window: nil, storage_pressure: nil)
|
|
8
8
|
@disabled_tags = disabled_tags
|
|
9
9
|
@show_non_tagged = show_non_tagged
|
|
10
10
|
@period = period
|
|
11
|
-
@
|
|
12
|
-
@
|
|
11
|
+
@period_type = period_type
|
|
12
|
+
@window = window
|
|
13
|
+
@storage_pressure = storage_pressure
|
|
13
14
|
@route_thresholds = RailsPulse.configuration.route_thresholds
|
|
14
15
|
@query_thresholds = RailsPulse.configuration.query_thresholds
|
|
15
16
|
@job_thresholds = RailsPulse.configuration.job_thresholds
|
|
@@ -22,6 +23,7 @@ module RailsPulse
|
|
|
22
23
|
queries: query_counts,
|
|
23
24
|
jobs: job_counts,
|
|
24
25
|
exceptions: exception_counts,
|
|
26
|
+
tracking: tracking_counts,
|
|
25
27
|
storage: storage_counts
|
|
26
28
|
}
|
|
27
29
|
end
|
|
@@ -29,11 +31,10 @@ module RailsPulse
|
|
|
29
31
|
private
|
|
30
32
|
|
|
31
33
|
def route_counts
|
|
32
|
-
start, finish = period_range
|
|
33
|
-
|
|
34
34
|
data = RailsPulse::Summary
|
|
35
35
|
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
36
|
-
.where(summarizable_type: "RailsPulse::Route"
|
|
36
|
+
.where(summarizable_type: "RailsPulse::Route")
|
|
37
|
+
.merge(period_summaries)
|
|
37
38
|
.group("summarizable_id")
|
|
38
39
|
.select(
|
|
39
40
|
"summarizable_id",
|
|
@@ -62,11 +63,10 @@ module RailsPulse
|
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
def query_counts
|
|
65
|
-
start, finish = period_range
|
|
66
|
-
|
|
67
66
|
data = RailsPulse::Summary
|
|
68
67
|
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
69
|
-
.where(summarizable_type: "RailsPulse::Query"
|
|
68
|
+
.where(summarizable_type: "RailsPulse::Query")
|
|
69
|
+
.merge(period_summaries)
|
|
70
70
|
.group("summarizable_id")
|
|
71
71
|
.select(
|
|
72
72
|
"summarizable_id",
|
|
@@ -90,7 +90,34 @@ module RailsPulse
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def storage_counts
|
|
93
|
-
StoragePressure.new.storage_counts
|
|
93
|
+
(@storage_pressure || StoragePressure.new).storage_counts
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A process that dropped and has since gone away still counts as
|
|
97
|
+
# critical for that hour. Nil until any writer has reported.
|
|
98
|
+
def tracking_counts
|
|
99
|
+
return nil unless RailsPulse::Event.table_available?
|
|
100
|
+
|
|
101
|
+
live = RailsPulse::WriterHeartbeat.live_processes
|
|
102
|
+
dropped_by_process = RailsPulse::WriterHeartbeat.dropped_by_process(window: 1.hour).select { |_, n| n.positive? }
|
|
103
|
+
return nil if live.empty? && dropped_by_process.empty?
|
|
104
|
+
|
|
105
|
+
critical = dropped_by_process.size
|
|
106
|
+
healthy = slow = 0
|
|
107
|
+
live.each do |process|
|
|
108
|
+
next if dropped_by_process.key?(process.process_label)
|
|
109
|
+
|
|
110
|
+
# queue_size <= 0 means unreadable metadata, not an empty queue
|
|
111
|
+
if process.queue_size <= 0
|
|
112
|
+
critical += 1
|
|
113
|
+
elsif process.queue_depth * 2 >= process.queue_size
|
|
114
|
+
slow += 1
|
|
115
|
+
else
|
|
116
|
+
healthy += 1
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
{ healthy: healthy, slow: slow, critical: critical }
|
|
94
121
|
end
|
|
95
122
|
|
|
96
123
|
def job_counts
|
|
@@ -125,12 +152,10 @@ module RailsPulse
|
|
|
125
152
|
return nil unless RailsPulse.configuration.track_exceptions
|
|
126
153
|
return nil unless exception_summaries_available?
|
|
127
154
|
|
|
128
|
-
start, finish = period_range
|
|
129
|
-
|
|
130
155
|
counts = RailsPulse::Summary
|
|
131
156
|
.for_exceptions
|
|
132
157
|
.where.not(summarizable_id: 0)
|
|
133
|
-
.
|
|
158
|
+
.merge(period_summaries)
|
|
134
159
|
.group(:summarizable_id)
|
|
135
160
|
.sum(:count)
|
|
136
161
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
module Dashboard
|
|
3
3
|
class NeedsAttention
|
|
4
|
-
include Concerns::
|
|
4
|
+
include Concerns::AttentionClassification
|
|
5
5
|
include Concerns::TimeRangeHelper
|
|
6
6
|
|
|
7
|
-
def initialize(disabled_tags: [], show_non_tagged: true, period: 7,
|
|
7
|
+
def initialize(disabled_tags: [], show_non_tagged: true, period: 7, period_type: nil, window: nil, storage_pressure: nil)
|
|
8
8
|
@disabled_tags = disabled_tags
|
|
9
9
|
@show_non_tagged = show_non_tagged
|
|
10
10
|
@period = period
|
|
11
|
-
@
|
|
12
|
-
@
|
|
11
|
+
@period_type = period_type
|
|
12
|
+
@window = window
|
|
13
|
+
@storage_pressure = storage_pressure
|
|
13
14
|
@route_thresholds = RailsPulse.configuration.route_thresholds
|
|
14
15
|
@query_thresholds = RailsPulse.configuration.query_thresholds
|
|
15
16
|
@job_thresholds = RailsPulse.configuration.job_thresholds
|
|
@@ -41,7 +42,18 @@ module RailsPulse
|
|
|
41
42
|
private
|
|
42
43
|
|
|
43
44
|
def storage_pressure_items
|
|
44
|
-
StoragePressure.new.pressure_items
|
|
45
|
+
(@storage_pressure || StoragePressure.new).pressure_items
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The dashboard window is user-selected, so the error-rate reason names
|
|
49
|
+
# the window's real length rather than assuming a week.
|
|
50
|
+
def span_phrase
|
|
51
|
+
start, finish = period_range
|
|
52
|
+
hours = ((finish - start) / 1.hour).round
|
|
53
|
+
return "in the last #{hours} hour#{"s" unless hours == 1}" if hours <= 25
|
|
54
|
+
|
|
55
|
+
days = (hours / 24.0).round
|
|
56
|
+
"in the last #{days} day#{"s" unless days == 1}"
|
|
45
57
|
end
|
|
46
58
|
|
|
47
59
|
def url_helpers
|
|
@@ -49,12 +61,11 @@ module RailsPulse
|
|
|
49
61
|
end
|
|
50
62
|
|
|
51
63
|
def route_items
|
|
52
|
-
start, finish = period_range
|
|
53
|
-
|
|
54
64
|
route_data = RailsPulse::Summary
|
|
55
65
|
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
56
66
|
.joins("INNER JOIN rails_pulse_routes ON rails_pulse_routes.id = rails_pulse_summaries.summarizable_id")
|
|
57
|
-
.where(summarizable_type: "RailsPulse::Route"
|
|
67
|
+
.where(summarizable_type: "RailsPulse::Route")
|
|
68
|
+
.merge(period_summaries)
|
|
58
69
|
.group("rails_pulse_summaries.summarizable_id, rails_pulse_routes.path")
|
|
59
70
|
.select(
|
|
60
71
|
"rails_pulse_summaries.summarizable_id as route_id",
|
|
@@ -71,7 +82,7 @@ module RailsPulse
|
|
|
71
82
|
errors = record.total_errors.to_i
|
|
72
83
|
error_rate = total > 0 ? (errors * 100.0 / total).round(1) : 0.0
|
|
73
84
|
|
|
74
|
-
severity, reason, metric, metric_sub, sort_score = classify_route(p95, total, errors, error_rate)
|
|
85
|
+
severity, reason, metric, metric_sub, sort_score = classify_route(p95, total, errors, error_rate, span_phrase)
|
|
75
86
|
next unless severity
|
|
76
87
|
|
|
77
88
|
items << {
|
|
@@ -89,45 +100,12 @@ module RailsPulse
|
|
|
89
100
|
items
|
|
90
101
|
end
|
|
91
102
|
|
|
92
|
-
def classify_route(p95, total, errors, error_rate)
|
|
93
|
-
if p95 >= @route_thresholds[:critical] || error_rate >= CRITICAL_ERROR_RATE
|
|
94
|
-
if error_rate >= CRITICAL_ERROR_RATE
|
|
95
|
-
[ :critical,
|
|
96
|
-
"#{error_rate}% error rate · #{total} requests this week",
|
|
97
|
-
"#{errors} errors",
|
|
98
|
-
"P95 #{p95.round(0).to_i}ms",
|
|
99
|
-
errors * total.to_f ]
|
|
100
|
-
else
|
|
101
|
-
[ :critical,
|
|
102
|
-
"#{p95.round(0).to_i}ms P95 · exceeds #{@route_thresholds[:critical]}ms threshold",
|
|
103
|
-
"#{p95.round(0).to_i}ms P95",
|
|
104
|
-
"#{total} requests",
|
|
105
|
-
p95 ]
|
|
106
|
-
end
|
|
107
|
-
elsif p95 >= @route_thresholds[:slow] || error_rate >= WARNING_ERROR_RATE
|
|
108
|
-
if error_rate >= WARNING_ERROR_RATE && p95 < @route_thresholds[:slow]
|
|
109
|
-
[ :warning,
|
|
110
|
-
"#{error_rate}% error rate · #{total} requests this week",
|
|
111
|
-
"#{errors} errors",
|
|
112
|
-
"P95 #{p95.round(0).to_i}ms",
|
|
113
|
-
errors * total.to_f ]
|
|
114
|
-
else
|
|
115
|
-
[ :warning,
|
|
116
|
-
"#{p95.round(0).to_i}ms P95 · #{error_rate > 0 ? "#{error_rate}% error rate" : "above slow threshold"}",
|
|
117
|
-
"#{p95.round(0).to_i}ms P95",
|
|
118
|
-
"#{total} requests",
|
|
119
|
-
p95 ]
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
103
|
def query_items
|
|
125
|
-
start, finish = period_range
|
|
126
|
-
|
|
127
104
|
query_data = RailsPulse::Summary
|
|
128
105
|
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
129
106
|
.joins("INNER JOIN rails_pulse_queries ON rails_pulse_queries.id = rails_pulse_summaries.summarizable_id")
|
|
130
|
-
.where(summarizable_type: "RailsPulse::Query"
|
|
107
|
+
.where(summarizable_type: "RailsPulse::Query")
|
|
108
|
+
.merge(period_summaries)
|
|
131
109
|
.group("rails_pulse_summaries.summarizable_id, rails_pulse_queries.normalized_sql")
|
|
132
110
|
.select(
|
|
133
111
|
"rails_pulse_summaries.summarizable_id as query_id",
|
|
@@ -143,26 +121,19 @@ module RailsPulse
|
|
|
143
121
|
p95 = record.p95_duration.to_f
|
|
144
122
|
count = record.total_count.to_i
|
|
145
123
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
reason = "#{p95.round(0).to_i}ms P95 · exceeds #{@query_thresholds[:critical]}ms threshold"
|
|
149
|
-
elsif p95 >= @query_thresholds[:slow]
|
|
150
|
-
severity = :warning
|
|
151
|
-
reason = "#{p95.round(0).to_i}ms P95 · above #{@query_thresholds[:slow]}ms slow threshold"
|
|
152
|
-
else
|
|
153
|
-
next
|
|
154
|
-
end
|
|
124
|
+
severity, reason, metric, metric_sub, sort_score = classify_query(p95, count)
|
|
125
|
+
next unless severity
|
|
155
126
|
|
|
156
127
|
classified_ids << record.query_id
|
|
157
128
|
items << {
|
|
158
129
|
type: "QUERY",
|
|
159
130
|
name: truncate_sql(record.normalized_sql),
|
|
160
131
|
reason: reason,
|
|
161
|
-
metric:
|
|
162
|
-
metric_sub:
|
|
132
|
+
metric: metric,
|
|
133
|
+
metric_sub: metric_sub,
|
|
163
134
|
link: url_helpers.query_path(record.query_id),
|
|
164
135
|
severity: severity,
|
|
165
|
-
sort_score:
|
|
136
|
+
sort_score: sort_score,
|
|
166
137
|
monospace: true
|
|
167
138
|
}
|
|
168
139
|
end
|
|
@@ -200,7 +171,9 @@ module RailsPulse
|
|
|
200
171
|
failure_rate = job.failure_rate
|
|
201
172
|
p95 = job.p95_duration.to_f
|
|
202
173
|
|
|
203
|
-
severity, reason, metric, metric_sub, sort_score = classify_job(
|
|
174
|
+
severity, reason, metric, metric_sub, sort_score = classify_job(
|
|
175
|
+
failure_rate, p95, job.runs_count, job.failures_count, job.queue_name.presence || "default"
|
|
176
|
+
)
|
|
204
177
|
next unless severity
|
|
205
178
|
|
|
206
179
|
items << {
|
|
@@ -218,38 +191,6 @@ module RailsPulse
|
|
|
218
191
|
items
|
|
219
192
|
end
|
|
220
193
|
|
|
221
|
-
def classify_job(job, failure_rate, p95)
|
|
222
|
-
if failure_rate >= CRITICAL_JOB_FAILURE_RATE || p95 >= @job_thresholds[:critical]
|
|
223
|
-
if failure_rate >= CRITICAL_JOB_FAILURE_RATE
|
|
224
|
-
[ :critical,
|
|
225
|
-
"#{job.queue_name.presence || "default"} queue · #{failure_rate}% failure rate",
|
|
226
|
-
"#{job.failures_count} / #{job.runs_count} failed",
|
|
227
|
-
"P95 #{p95.round(0).to_i}ms",
|
|
228
|
-
job.failures_count * job.runs_count.to_f ]
|
|
229
|
-
else
|
|
230
|
-
[ :critical,
|
|
231
|
-
"P95 #{p95.round(0).to_i}ms · exceeds #{@job_thresholds[:critical]}ms threshold",
|
|
232
|
-
"#{p95.round(0).to_i}ms P95",
|
|
233
|
-
"#{job.runs_count} runs",
|
|
234
|
-
p95 ]
|
|
235
|
-
end
|
|
236
|
-
elsif failure_rate >= WARNING_JOB_FAILURE_RATE || p95 >= @job_thresholds[:slow]
|
|
237
|
-
if failure_rate >= WARNING_JOB_FAILURE_RATE
|
|
238
|
-
[ :warning,
|
|
239
|
-
"#{job.queue_name.presence || "default"} queue · #{failure_rate}% failure rate",
|
|
240
|
-
"#{job.failures_count} / #{job.runs_count} failed",
|
|
241
|
-
"P95 #{p95.round(0).to_i}ms",
|
|
242
|
-
job.failures_count * job.runs_count.to_f ]
|
|
243
|
-
else
|
|
244
|
-
[ :warning,
|
|
245
|
-
"P95 #{p95.round(0).to_i}ms · above slow threshold",
|
|
246
|
-
"#{p95.round(0).to_i}ms P95",
|
|
247
|
-
"#{job.runs_count} runs",
|
|
248
|
-
p95 ]
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
|
|
253
194
|
# Exception groups that fired often enough over the period to be worth
|
|
254
195
|
# looking at. Read from summaries, not from ExceptionGroup#occurrence_count
|
|
255
196
|
# — that is a lifetime counter, so a group that was noisy last year and is
|
|
@@ -262,12 +203,10 @@ module RailsPulse
|
|
|
262
203
|
return [] unless RailsPulse.configuration.track_exceptions
|
|
263
204
|
return [] unless exception_tables_available?
|
|
264
205
|
|
|
265
|
-
start, finish = period_range
|
|
266
|
-
|
|
267
206
|
counts = RailsPulse::Summary
|
|
268
207
|
.for_exceptions
|
|
269
208
|
.where.not(summarizable_id: 0)
|
|
270
|
-
.
|
|
209
|
+
.merge(period_summaries)
|
|
271
210
|
.group(:summarizable_id)
|
|
272
211
|
.sum(:count)
|
|
273
212
|
|
|
@@ -320,12 +259,6 @@ module RailsPulse
|
|
|
320
259
|
rescue ActiveRecord::ActiveRecordError
|
|
321
260
|
false
|
|
322
261
|
end
|
|
323
|
-
|
|
324
|
-
def truncate_sql(sql)
|
|
325
|
-
return "" if sql.blank?
|
|
326
|
-
cleaned = sql.gsub(/\s+/, " ").strip
|
|
327
|
-
cleaned.length > 80 ? "#{cleaned[0..79]}..." : cleaned
|
|
328
|
-
end
|
|
329
262
|
end
|
|
330
263
|
end
|
|
331
264
|
end
|
|
@@ -8,12 +8,15 @@ module RailsPulse
|
|
|
8
8
|
@config = RailsPulse.configuration
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# Memoized: computing the items costs several aggregate queries (one a
|
|
12
|
+
# raw-request count), and one dashboard render asks for them repeatedly.
|
|
11
13
|
def pressure_items
|
|
12
|
-
summary_staleness_items +
|
|
14
|
+
@pressure_items ||= summary_staleness_items + sub_hour_retention_items + writer_drop_items
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
# Dropped requests get their own Tracking badge, not this one
|
|
15
18
|
def storage_counts
|
|
16
|
-
items = pressure_items
|
|
19
|
+
items = pressure_items.reject { |i| i[:type] == "TRACKING" }
|
|
17
20
|
critical = items.any? { |i| i[:severity] == :critical } ? 1 : 0
|
|
18
21
|
slow = (critical.zero? && items.any? { |i| i[:severity] == :warning }) ? 1 : 0
|
|
19
22
|
healthy = (critical.zero? && slow.zero?) ? 1 : 0
|
|
@@ -36,53 +39,17 @@ module RailsPulse
|
|
|
36
39
|
if age >= STALE_CRITICAL_THRESHOLD
|
|
37
40
|
hours = (age / 1.hour).round
|
|
38
41
|
[ stale_item(:critical, "Summary job is #{hours}h behind",
|
|
39
|
-
"#{hours}h stale", "Last run: #{latest_end.strftime("%Y-%m-%d %H:%M
|
|
42
|
+
"#{hours}h stale", "Last run: #{latest_end.strftime("%Y-%m-%d %H:%M")} (#{RailsPulse::TimeRange.aggregation_zone_label})") ]
|
|
40
43
|
elsif age >= STALE_WARNING_THRESHOLD
|
|
41
44
|
hours = (age / 1.hour).round
|
|
42
45
|
[ stale_item(:warning, "Summary job is #{hours}h behind",
|
|
43
|
-
"#{hours}h stale", "Last run: #{latest_end.strftime("%Y-%m-%d %H:%M
|
|
46
|
+
"#{hours}h stale", "Last run: #{latest_end.strftime("%Y-%m-%d %H:%M")} (#{RailsPulse::TimeRange.aggregation_zone_label})") ]
|
|
44
47
|
else
|
|
45
48
|
[]
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
|
|
49
|
-
# Signal B —
|
|
50
|
-
def stuck_records_items
|
|
51
|
-
return [] unless @config.archiving_enabled
|
|
52
|
-
return [] unless @config.full_retention_period
|
|
53
|
-
|
|
54
|
-
oldest_start = oldest_overall_request_summary_start
|
|
55
|
-
return [] if oldest_start.nil?
|
|
56
|
-
|
|
57
|
-
stuck_count = RailsPulse::Request
|
|
58
|
-
.where("occurred_at < ?", @config.full_retention_period.ago)
|
|
59
|
-
.where("occurred_at < ?", oldest_start)
|
|
60
|
-
.count
|
|
61
|
-
|
|
62
|
-
return [] if stuck_count.zero?
|
|
63
|
-
|
|
64
|
-
[ {
|
|
65
|
-
type: "STORAGE",
|
|
66
|
-
name: "Storage pressure",
|
|
67
|
-
reason: "#{stuck_count} #{"request".pluralize(stuck_count)} past the retention window cannot be cleaned up — summarize first",
|
|
68
|
-
metric: "#{stuck_count} stuck",
|
|
69
|
-
metric_sub: "older than #{humanize_duration(@config.full_retention_period)}",
|
|
70
|
-
link: "#",
|
|
71
|
-
severity: :critical,
|
|
72
|
-
sort_score: stuck_count.to_f,
|
|
73
|
-
popover_title: "Cleanup is blocked on unsummarised records",
|
|
74
|
-
popover_body: "#{stuck_count} #{"request".pluralize(stuck_count)} are older than your retention period " \
|
|
75
|
-
"(#{humanize_duration(@config.full_retention_period)}) but predate any hourly summary record. " \
|
|
76
|
-
"CleanupService will not delete records from a time period until that period has been summarised — " \
|
|
77
|
-
"otherwise those data points would be permanently lost from dashboard charts.<br><br>" \
|
|
78
|
-
"<strong>How to fix:</strong> Run <code>RailsPulse::SummaryJob</code> to summarise the historical periods. " \
|
|
79
|
-
"If the job is not yet scheduled, run the backfill task to summarise all historical data:<br><br>" \
|
|
80
|
-
"<code>rails rails_pulse:backfill_summaries</code><br><br>" \
|
|
81
|
-
"Once the affected periods are summarised, the next cleanup run will delete these records automatically."
|
|
82
|
-
} ]
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Signal C — Retention period shorter than 1-hour cleanup minimum
|
|
52
|
+
# Signal B — Retention period shorter than 1-hour cleanup minimum
|
|
86
53
|
def sub_hour_retention_items
|
|
87
54
|
return [] unless @config.full_retention_period
|
|
88
55
|
return [] unless @config.full_retention_period < 1.hour
|
|
@@ -107,6 +74,35 @@ module RailsPulse
|
|
|
107
74
|
} ]
|
|
108
75
|
end
|
|
109
76
|
|
|
77
|
+
# Signal C — the background writer is discarding requests
|
|
78
|
+
def writer_drop_items
|
|
79
|
+
return [] unless RailsPulse::Event.table_available?
|
|
80
|
+
|
|
81
|
+
summary = RailsPulse::WriterHeartbeat.summary
|
|
82
|
+
dropped = summary[:dropped].to_i
|
|
83
|
+
return [] if dropped.zero?
|
|
84
|
+
|
|
85
|
+
[ {
|
|
86
|
+
type: "TRACKING",
|
|
87
|
+
name: "Writer queue dropping requests",
|
|
88
|
+
reason: "#{dropped} #{"request".pluralize(dropped)} dropped in the last hour — charts are missing samples",
|
|
89
|
+
metric: "#{dropped} dropped",
|
|
90
|
+
metric_sub: "last hour, #{summary[:processes]} live #{"writer".pluralize(summary[:processes])}",
|
|
91
|
+
link: "#",
|
|
92
|
+
severity: :critical,
|
|
93
|
+
sort_score: dropped.to_f,
|
|
94
|
+
popover_title: "The tracking queue is overflowing",
|
|
95
|
+
popover_body: "Each process queues tracked requests for one background writer. When the queue (#{summary[:queue_size]} requests, " \
|
|
96
|
+
"<code>config.async_queue_size</code>) is full the newest request is dropped rather than slowing the app, so the " \
|
|
97
|
+
"dashboard undercounts traffic while this lasts.<br><br>" \
|
|
98
|
+
"<strong>How to fix:</strong> raise <code>config.async_queue_size</code> in the Rails Pulse initializer, or find out " \
|
|
99
|
+
"why the writer cannot keep up: database latency, a saturated connection pool, or a burst far above normal traffic. " \
|
|
100
|
+
"<code>rails rails_pulse:status</code> reports the same numbers from the shell."
|
|
101
|
+
} ]
|
|
102
|
+
rescue ActiveRecord::ActiveRecordError
|
|
103
|
+
[]
|
|
104
|
+
end
|
|
105
|
+
|
|
110
106
|
def stale_item(severity, reason, metric, metric_sub)
|
|
111
107
|
{
|
|
112
108
|
type: "STORAGE",
|
|
@@ -118,8 +114,8 @@ module RailsPulse
|
|
|
118
114
|
severity: severity,
|
|
119
115
|
sort_score: severity == :critical ? Float::INFINITY : 1.0,
|
|
120
116
|
popover_title: "Summary job is not running",
|
|
121
|
-
popover_body: "Rails Pulse summarises raw request data into hourly aggregates
|
|
122
|
-
"
|
|
117
|
+
popover_body: "Rails Pulse summarises raw request data into hourly aggregates for the dashboard's charts. " \
|
|
118
|
+
"Count-based cleanup only trims records from periods that have been summarised, so until the job catches up tables can grow past <code>max_table_records</code>.<br><br>" \
|
|
123
119
|
"<strong>How to fix:</strong> Schedule <code>RailsPulse::SummaryJob</code> to run every hour in your job scheduler " \
|
|
124
120
|
"(Sidekiq-Cron, GoodJob, Solid Queue, etc.). Once it has run, cleanup will resume automatically on its next execution.<br><br>" \
|
|
125
121
|
"To backfill any historical gaps, run: <code>rails rails_pulse:backfill_summaries</code>"
|
|
@@ -131,26 +127,6 @@ module RailsPulse
|
|
|
131
127
|
.where(summarizable_type: "RailsPulse::Request", summarizable_id: 0, period_type: "hour")
|
|
132
128
|
.maximum(:period_end)
|
|
133
129
|
end
|
|
134
|
-
|
|
135
|
-
def oldest_overall_request_summary_start
|
|
136
|
-
RailsPulse::Summary
|
|
137
|
-
.where(summarizable_type: "RailsPulse::Request", summarizable_id: 0, period_type: "hour")
|
|
138
|
-
.minimum(:period_start)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def humanize_duration(duration)
|
|
142
|
-
total_seconds = duration.to_i
|
|
143
|
-
if total_seconds >= 86_400
|
|
144
|
-
days = total_seconds / 86_400
|
|
145
|
-
"#{days} #{"day".pluralize(days)}"
|
|
146
|
-
elsif total_seconds >= 3_600
|
|
147
|
-
hours = total_seconds / 3_600
|
|
148
|
-
"#{hours} #{"hour".pluralize(hours)}"
|
|
149
|
-
else
|
|
150
|
-
minutes = total_seconds / 60
|
|
151
|
-
"#{minutes} #{"minute".pluralize(minutes)}"
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
130
|
end
|
|
155
131
|
end
|
|
156
132
|
end
|