rails_pulse 0.4.2 → 0.5.0.pre.1
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 +33 -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 +130 -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 +24 -0
- data/app/controllers/rails_pulse/api/v1/exceptions_controller.rb +76 -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 +114 -0
- data/app/controllers/rails_pulse/api/v1/requests_controller.rb +58 -0
- data/app/controllers/rails_pulse/api/v1/routes_controller.rb +98 -0
- data/app/controllers/rails_pulse/application_controller.rb +1 -1
- data/app/controllers/rails_pulse/dashboard_controller.rb +20 -20
- 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/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 +2 -6
- data/app/models/rails_pulse/charts/percentile_chart_base.rb +3 -4
- 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/time_range_helper.rb +1 -2
- data/app/models/rails_pulse/dashboard/health_summary.rb +30 -3
- data/app/models/rails_pulse/dashboard/needs_attention.rb +2 -3
- data/app/models/rails_pulse/dashboard/storage_pressure.rb +34 -4
- data/app/models/rails_pulse/dashboard/storage_status.rb +36 -0
- data/app/models/rails_pulse/deployment.rb +2 -7
- data/app/models/rails_pulse/event.rb +40 -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 -5
- 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 -1
- data/app/models/rails_pulse/jobs/charts/execution_volume.rb +1 -1
- data/app/models/rails_pulse/jobs/charts/failure_rate.rb +1 -1
- data/app/models/rails_pulse/like_pattern.rb +29 -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 -6
- 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/writer_heartbeat.rb +86 -0
- data/app/serializers/rails_pulse/api/v1/deployment_serializer.rb +20 -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 +21 -0
- data/app/serializers/rails_pulse/api/v1/route_serializer.rb +19 -0
- data/app/services/rails_pulse/exception_capture_service.rb +1 -1
- data/app/services/rails_pulse/operations/comparison.rb +1 -1
- 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/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 +37 -0
- data/config/routes.rb +19 -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 +1 -0
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +20 -2
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +36 -13
- data/lib/rails_pulse/cleanup_service.rb +26 -20
- data/lib/rails_pulse/cli/agent_files/agents.md +87 -0
- data/lib/rails_pulse/cli/agent_files/claude_skill.md +165 -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 +40 -0
- data/lib/rails_pulse/cli/exceptions.rb +111 -0
- data/lib/rails_pulse/cli/formatter.rb +31 -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 +72 -0
- data/lib/rails_pulse/cli/mcp.rb +47 -0
- data/lib/rails_pulse/cli/queries.rb +62 -0
- data/lib/rails_pulse/cli/requests.rb +49 -0
- data/lib/rails_pulse/cli/routes.rb +66 -0
- data/lib/rails_pulse/configuration.rb +23 -1
- data/lib/rails_pulse/current.rb +11 -0
- data/lib/rails_pulse/engine.rb +12 -3
- data/lib/rails_pulse/job_run_collector.rb +15 -15
- data/lib/rails_pulse/mcp/server.rb +76 -0
- data/lib/rails_pulse/mcp/tools/coverage.rb +96 -0
- data/lib/rails_pulse/mcp/tools/deployments.rb +80 -0
- data/lib/rails_pulse/mcp/tools/endpoint.rb +167 -0
- data/lib/rails_pulse/mcp/tools/errors.rb +95 -0
- data/lib/rails_pulse/mcp/tools/exception_detail.rb +118 -0
- data/lib/rails_pulse/mcp/tools/exceptions.rb +114 -0
- data/lib/rails_pulse/mcp/tools/helpers.rb +162 -0
- data/lib/rails_pulse/mcp/tools/jobs.rb +155 -0
- data/lib/rails_pulse/mcp/tools/queries.rb +136 -0
- data/lib/rails_pulse/mcp/tools/routes.rb +97 -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 +26 -0
- data/lib/rails_pulse/tracker.rb +75 -14
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +0 -4
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +13 -13
- metadata +96 -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,14 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Cards
|
|
4
4
|
class ErrorRates < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(route: nil,
|
|
6
|
-
|
|
7
|
-
@disabled_tags = disabled_tags
|
|
8
|
-
@show_non_tagged = show_non_tagged
|
|
9
|
-
@period = period
|
|
10
|
-
@period_type = period_type
|
|
11
|
-
@start_time = start_time
|
|
12
|
-
@end_time = end_time
|
|
5
|
+
def initialize(route: nil, **kwargs)
|
|
6
|
+
super(subject: route, **kwargs)
|
|
13
7
|
end
|
|
14
8
|
|
|
15
9
|
def to_metric_card
|
|
@@ -20,8 +14,8 @@ module RailsPulse
|
|
|
20
14
|
"SUM(error_count) AS total_errors",
|
|
21
15
|
"SUM(status_4xx) AS total_4xx",
|
|
22
16
|
"SUM(count) AS total_requests",
|
|
23
|
-
"SUM(CASE WHEN period_start >=
|
|
24
|
-
"SUM(CASE WHEN period_start >=
|
|
17
|
+
window_sql("SUM(CASE WHEN period_start >= :current_start THEN error_count + status_4xx ELSE 0 END) AS current_total_errors"),
|
|
18
|
+
window_sql("SUM(CASE WHEN period_start >= :range_start AND period_start < :current_start THEN error_count + status_4xx ELSE 0 END) AS previous_total_errors")
|
|
25
19
|
).take
|
|
26
20
|
|
|
27
21
|
total_errors = metrics.total_errors || 0
|
|
@@ -35,7 +29,7 @@ module RailsPulse
|
|
|
35
29
|
server_rate = has_data ? (total_errors.to_f / total_requests * 100).round(2) : 0
|
|
36
30
|
client_rate = has_data ? (total_4xx.to_f / total_requests * 100).round(2) : 0
|
|
37
31
|
|
|
38
|
-
baseline = baseline_trend_for(@
|
|
32
|
+
baseline = baseline_trend_for(@subject, metric: :error_rate)
|
|
39
33
|
|
|
40
34
|
if baseline
|
|
41
35
|
trend_icon, trend_amount, trend_caption = baseline
|
|
@@ -72,10 +66,6 @@ module RailsPulse
|
|
|
72
66
|
|
|
73
67
|
private
|
|
74
68
|
|
|
75
|
-
def subject_id
|
|
76
|
-
@route&.id
|
|
77
|
-
end
|
|
78
|
-
|
|
79
69
|
def sparkline_from_error_rates(errors_by_period, client_errors_by_period, counts_by_period)
|
|
80
70
|
if @period_type == "hour"
|
|
81
71
|
sparkline_hours.each_with_object({}) do |current_time, result|
|
|
@@ -2,14 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Cards
|
|
4
4
|
class PercentileResponseTimes < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(route: nil,
|
|
6
|
-
|
|
7
|
-
@disabled_tags = disabled_tags
|
|
8
|
-
@show_non_tagged = show_non_tagged
|
|
9
|
-
@period = period
|
|
10
|
-
@period_type = period_type
|
|
11
|
-
@start_time = start_time
|
|
12
|
-
@end_time = end_time
|
|
5
|
+
def initialize(route: nil, **kwargs)
|
|
6
|
+
super(subject: route, **kwargs)
|
|
13
7
|
end
|
|
14
8
|
|
|
15
9
|
def to_metric_card
|
|
@@ -18,8 +12,8 @@ module RailsPulse
|
|
|
18
12
|
|
|
19
13
|
metrics = base_query.select(
|
|
20
14
|
"SUM(p95_duration * count) / NULLIF(SUM(count), 0) AS overall_p95",
|
|
21
|
-
"SUM(CASE WHEN period_start >=
|
|
22
|
-
"SUM(CASE WHEN period_start >=
|
|
15
|
+
window_sql("SUM(CASE WHEN period_start >= :current_start THEN p95_duration * count ELSE 0 END) / NULLIF(SUM(CASE WHEN period_start >= :current_start THEN count ELSE 0 END), 0) AS current_p95"),
|
|
16
|
+
window_sql("SUM(CASE WHEN period_start >= :range_start AND period_start < :current_start THEN p95_duration * count ELSE 0 END) / NULLIF(SUM(CASE WHEN period_start >= :range_start AND period_start < :current_start THEN count ELSE 0 END), 0) AS previous_p95"),
|
|
23
17
|
"SUM(count) AS total_count"
|
|
24
18
|
).take
|
|
25
19
|
|
|
@@ -32,7 +26,7 @@ module RailsPulse
|
|
|
32
26
|
|
|
33
27
|
# Prefer a comparison against this route's own history; fall back to
|
|
34
28
|
# period-over-period on the index, where there is no single subject.
|
|
35
|
-
baseline = baseline_trend_for(@
|
|
29
|
+
baseline = baseline_trend_for(@subject, metric: :p95)
|
|
36
30
|
|
|
37
31
|
if baseline
|
|
38
32
|
trend_icon, trend_amount, trend_caption = baseline
|
|
@@ -69,12 +63,6 @@ module RailsPulse
|
|
|
69
63
|
help_text: "The 95th percentile response time — 95% of requests are faster than this. Weighted by request volume across all routes. A rising P95 indicates increasing slowness affecting your users."
|
|
70
64
|
}
|
|
71
65
|
end
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
def subject_id
|
|
76
|
-
@route&.id
|
|
77
|
-
end
|
|
78
66
|
end
|
|
79
67
|
end
|
|
80
68
|
end
|
|
@@ -2,14 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Cards
|
|
4
4
|
class RequestCountTotals < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(route: nil,
|
|
6
|
-
|
|
7
|
-
@disabled_tags = disabled_tags
|
|
8
|
-
@show_non_tagged = show_non_tagged
|
|
9
|
-
@period = period
|
|
10
|
-
@period_type = period_type
|
|
11
|
-
@start_time = start_time
|
|
12
|
-
@end_time = end_time
|
|
5
|
+
def initialize(route: nil, **kwargs)
|
|
6
|
+
super(subject: route, **kwargs)
|
|
13
7
|
end
|
|
14
8
|
|
|
15
9
|
def to_metric_card
|
|
@@ -18,8 +12,8 @@ module RailsPulse
|
|
|
18
12
|
|
|
19
13
|
metrics = base_query.select(
|
|
20
14
|
"SUM(count) AS total_count",
|
|
21
|
-
"SUM(CASE WHEN period_start >=
|
|
22
|
-
"SUM(CASE WHEN period_start >=
|
|
15
|
+
window_sql("SUM(CASE WHEN period_start >= :current_start THEN count ELSE 0 END) AS current_count"),
|
|
16
|
+
window_sql("SUM(CASE WHEN period_start >= :range_start AND period_start < :current_start THEN count ELSE 0 END) AS previous_count")
|
|
23
17
|
).take
|
|
24
18
|
|
|
25
19
|
# Calculate metrics from single query result
|
|
@@ -42,7 +36,7 @@ module RailsPulse
|
|
|
42
36
|
period_type: @period_type,
|
|
43
37
|
period_start: current_window_start..now
|
|
44
38
|
)
|
|
45
|
-
sparkline_query = sparkline_query.where(summarizable_id: @
|
|
39
|
+
sparkline_query = sparkline_query.where(summarizable_id: @subject.id) if @subject
|
|
46
40
|
|
|
47
41
|
grouped_data = bucket_by_period(sparkline_query) { |relation| relation.sum(:count) }
|
|
48
42
|
|
|
@@ -83,12 +77,6 @@ module RailsPulse
|
|
|
83
77
|
help_text: "Total HTTP requests served over the last 14 days, expressed as an average rate. Use this to understand traffic patterns and capacity planning needs."
|
|
84
78
|
}
|
|
85
79
|
end
|
|
86
|
-
|
|
87
|
-
private
|
|
88
|
-
|
|
89
|
-
def subject_id
|
|
90
|
-
@route&.id
|
|
91
|
-
end
|
|
92
80
|
end
|
|
93
81
|
end
|
|
94
82
|
end
|
|
@@ -30,7 +30,7 @@ module RailsPulse
|
|
|
30
30
|
# Pad missing data with defaults
|
|
31
31
|
default_value = { error_rate: nil, client_error_rate: nil }
|
|
32
32
|
daily_data = {}
|
|
33
|
-
(@start_time.to_i..@end_time.to_i).step(time_step) do |timestamp|
|
|
33
|
+
(@window.start_time.to_i..@window.end_time.to_i).step(time_step) do |timestamp|
|
|
34
34
|
daily_data[timestamp] = raw_data[timestamp] || default_value
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -22,7 +22,7 @@ module RailsPulse
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Pad missing data with zeros
|
|
25
|
-
daily_data = pad_data_with_zeros(raw_data, @start_time, @end_time, time_step)
|
|
25
|
+
daily_data = pad_data_with_zeros(raw_data, @window.start_time, @window.end_time, time_step)
|
|
26
26
|
|
|
27
27
|
# Build series data as [timestamp_ms, value] pairs for ECharts time axis
|
|
28
28
|
series = [ {
|
|
@@ -171,7 +171,11 @@ module RailsPulse
|
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
class << self
|
|
174
|
+
# A Date is read as midnight in Time.zone, the aggregation zone, so
|
|
175
|
+
# boundaries are always times: Date#end_of_week is the last day itself,
|
|
176
|
+
# and a period ending there would drop that whole day.
|
|
174
177
|
def calculate_period_end(period_type, start_time)
|
|
178
|
+
start_time = start_time.in_time_zone if start_time.is_a?(Date)
|
|
175
179
|
case period_type
|
|
176
180
|
when "hour" then start_time.end_of_hour
|
|
177
181
|
when "day" then start_time.end_of_day
|
|
@@ -181,6 +185,7 @@ module RailsPulse
|
|
|
181
185
|
end
|
|
182
186
|
|
|
183
187
|
def normalize_period_start(period_type, time)
|
|
188
|
+
time = time.in_time_zone if time.is_a?(Date)
|
|
184
189
|
case period_type
|
|
185
190
|
when "hour" then time.beginning_of_hour
|
|
186
191
|
when "day" then time.beginning_of_day
|
|
@@ -4,10 +4,9 @@ module RailsPulse
|
|
|
4
4
|
WEIGHTED_P95 = "SUM(rails_pulse_summaries.p95_duration * rails_pulse_summaries.count) / NULLIF(SUM(rails_pulse_summaries.count), 0)"
|
|
5
5
|
WEIGHTED_P99 = "SUM(rails_pulse_summaries.p99_duration * rails_pulse_summaries.count) / NULLIF(SUM(rails_pulse_summaries.count), 0)"
|
|
6
6
|
|
|
7
|
-
def initialize(ransack_query:, period_type: nil,
|
|
7
|
+
def initialize(ransack_query:, period_type: nil, params:, disabled_tags: [], show_non_tagged: true)
|
|
8
8
|
@ransack_query = ransack_query
|
|
9
9
|
@period_type = period_type
|
|
10
|
-
@start_time = start_time
|
|
11
10
|
@params = params
|
|
12
11
|
@disabled_tags = disabled_tags
|
|
13
12
|
@show_non_tagged = show_non_tagged
|
|
@@ -41,11 +40,11 @@ module RailsPulse
|
|
|
41
40
|
def apply_tag_filters(query)
|
|
42
41
|
actual_disabled_tags = @disabled_tags.reject { |tag| tag == "non_tagged" }
|
|
43
42
|
|
|
44
|
-
# Explicit ESCAPE: SQLite has no default escape character, so without
|
|
45
|
-
# it an underscore in a tag is never matched literally there.
|
|
46
43
|
actual_disabled_tags.each do |tag|
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
query = query.where.not(
|
|
45
|
+
"#{model_table}.tags LIKE ? #{RailsPulse::LikePattern::CLAUSE}",
|
|
46
|
+
RailsPulse::LikePattern.containing(tag)
|
|
47
|
+
)
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
unless @show_non_tagged
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
# Resolves the dashboard's time range, chart-zoom/table window, and
|
|
3
|
+
# duration-threshold filter from request params and session state, in one
|
|
4
|
+
# pass. Returns a single frozen Result so every caller — controllers,
|
|
5
|
+
# views, cards, charts — reads the same object instead of each re-deriving
|
|
6
|
+
# its own answer.
|
|
7
|
+
#
|
|
8
|
+
# Priority order for the chart/page window (highest first):
|
|
9
|
+
# 1. Page-specific preset from the dropdown (params[:q][:period_start_range])
|
|
10
|
+
# 2. Page-specific custom range from the picker (params[:q][:custom_date_range])
|
|
11
|
+
# 3. Chart zoom / drill-down params (params[:q][:occurred_at_gteq/_lt])
|
|
12
|
+
# 4. Time range selector preference (session[:time_range_preference])
|
|
13
|
+
# 5. Global filters (session[:global_filters]["start_time"/"end_time"])
|
|
14
|
+
# 6. Default (default_key)
|
|
15
|
+
#
|
|
16
|
+
# Every input is parsed in Time.zone, matching how Summary rows are
|
|
17
|
+
# bucketed — parsing a custom-range string in any other zone would
|
|
18
|
+
# misinterpret the intended instant whenever that zone differs from
|
|
19
|
+
# Time.zone, silently resolving to the wrong window.
|
|
20
|
+
class TimeRange
|
|
21
|
+
Result = Struct.new(
|
|
22
|
+
:window, :table_window, :period_type, :selected_time_range,
|
|
23
|
+
:zoom_start, :zoom_end, :start_duration, :selected_response_range,
|
|
24
|
+
keyword_init: true
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
PRESET_START = {
|
|
28
|
+
"last_24_hours" => -> { 1.day.ago },
|
|
29
|
+
"last_7_days" => -> { 1.week.ago },
|
|
30
|
+
"last_14_days" => -> { 2.weeks.ago },
|
|
31
|
+
"last_30_days" => -> { 1.month.ago }
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# Human-readable label for the zone daily/weekly/monthly summaries are
|
|
35
|
+
# bucketed in and custom ranges are rounded in (see class comment above)
|
|
36
|
+
# — the host app's config.time_zone, not the browser's zone. "UTC" or,
|
|
37
|
+
# e.g., "Eastern Time (US & Canada) (UTC-04:00)".
|
|
38
|
+
#
|
|
39
|
+
# This reads the Time.zone of the current dashboard request and assumes
|
|
40
|
+
# it is the zone SummaryJob ran under. That holds when both use
|
|
41
|
+
# config.time_zone (decision 0020). A host that overrides Time.zone per
|
|
42
|
+
# request for the dashboard as well would see a label that names the
|
|
43
|
+
# display zone while the daily buckets were still cut in the job's zone.
|
|
44
|
+
def self.aggregation_zone_label
|
|
45
|
+
return "UTC" if utc_zone?
|
|
46
|
+
"#{Time.zone.name} (UTC#{Time.zone.now.formatted_offset})"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Compact form of aggregation_zone_label for chart headings and tooltip
|
|
50
|
+
# suffixes, where the full name would crowd out the data: "UTC", the
|
|
51
|
+
# zone's abbreviation when it has an alphabetic one ("EDT"), otherwise
|
|
52
|
+
# the offset ("UTC+05:45").
|
|
53
|
+
def self.aggregation_zone_short_label
|
|
54
|
+
return "UTC" if utc_zone?
|
|
55
|
+
abbreviation = Time.zone.now.zone
|
|
56
|
+
abbreviation.match?(/\A[A-Z]{2,5}\z/) ? abbreviation : "UTC#{Time.zone.now.formatted_offset}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# IANA identifier for the same zone, for the chart JS (Intl.DateTimeFormat)
|
|
60
|
+
# to render daily labels in the zone summaries were actually bucketed in
|
|
61
|
+
# instead of the browser's zone.
|
|
62
|
+
def self.aggregation_zone_iana
|
|
63
|
+
Time.zone.tzinfo.name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.utc_zone?
|
|
67
|
+
Time.zone.utc_offset.zero? && Time.zone.now.zone == "UTC"
|
|
68
|
+
end
|
|
69
|
+
private_class_method :utc_zone?
|
|
70
|
+
|
|
71
|
+
def self.resolve(params:, session:, default_key: :last_24_hours, duration_range_type: :route)
|
|
72
|
+
new(params: params, session: session, default_key: default_key, duration_range_type: duration_range_type).resolve
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def initialize(params:, session:, default_key:, duration_range_type:)
|
|
76
|
+
@params = params
|
|
77
|
+
@session = session
|
|
78
|
+
@default_key = default_key
|
|
79
|
+
@duration_range_type = duration_range_type
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def resolve
|
|
83
|
+
start_time, end_time, selected_time_range = resolve_window
|
|
84
|
+
start_time, end_time, time_diff_hours = normalize(start_time, end_time)
|
|
85
|
+
period_type = time_diff_hours <= 25 ? "hour" : "day"
|
|
86
|
+
|
|
87
|
+
zoom_start, zoom_end, table_start, table_end = resolve_zoom(start_time.to_i, end_time.to_i)
|
|
88
|
+
start_duration, selected_response_range = resolve_duration
|
|
89
|
+
|
|
90
|
+
Result.new(
|
|
91
|
+
window: RailsPulse::TimeWindow.new(start_time, end_time),
|
|
92
|
+
table_window: RailsPulse::TimeWindow.new(table_start, table_end),
|
|
93
|
+
period_type: period_type,
|
|
94
|
+
selected_time_range: selected_time_range.to_s,
|
|
95
|
+
zoom_start: zoom_start,
|
|
96
|
+
zoom_end: zoom_end,
|
|
97
|
+
start_duration: start_duration,
|
|
98
|
+
selected_response_range: selected_response_range
|
|
99
|
+
).freeze
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
attr_reader :params, :session, :default_key, :duration_range_type
|
|
105
|
+
|
|
106
|
+
# -- Window resolution (priorities 1-6) ----------------------------------
|
|
107
|
+
|
|
108
|
+
def resolve_window
|
|
109
|
+
from_page_preset || from_page_custom_range || from_chart_zoom ||
|
|
110
|
+
from_session_custom || from_session_preset || from_global_filters ||
|
|
111
|
+
default_window
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def default_window
|
|
115
|
+
[ preset_start(default_key), Time.zone.now, default_key ]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def preset_start(key)
|
|
119
|
+
(PRESET_START[key.to_s] || PRESET_START["last_24_hours"]).call
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Priority 1: page-specific preset from the dropdown.
|
|
123
|
+
def from_page_preset
|
|
124
|
+
range = ransack_params[:period_start_range]
|
|
125
|
+
return nil unless range.present? && range.to_sym != :custom
|
|
126
|
+
|
|
127
|
+
[ preset_start(range), Time.zone.now, range ]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Priority 2: page-specific custom datetime range from the picker (only
|
|
131
|
+
# when period_start_range is explicitly "custom").
|
|
132
|
+
def from_page_custom_range
|
|
133
|
+
return nil unless ransack_params[:period_start_range].present? &&
|
|
134
|
+
ransack_params[:period_start_range].to_sym == :custom &&
|
|
135
|
+
ransack_params[:custom_date_range].present? &&
|
|
136
|
+
ransack_params[:custom_date_range].include?(" to ")
|
|
137
|
+
|
|
138
|
+
dates = ransack_params[:custom_date_range].split(" to ")
|
|
139
|
+
custom_start = parse_time_param(dates[0].strip)
|
|
140
|
+
custom_end = parse_time_param(dates[1].strip)
|
|
141
|
+
return nil unless custom_start && custom_end
|
|
142
|
+
|
|
143
|
+
[ custom_start, custom_end, :custom ]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Priority 3: custom time range from chart zoom.
|
|
147
|
+
def from_chart_zoom
|
|
148
|
+
return nil unless ransack_params[:occurred_at_gteq].present? && ransack_params[:occurred_at_lt].present?
|
|
149
|
+
|
|
150
|
+
zoom_start = parse_time_param(ransack_params[:occurred_at_gteq])
|
|
151
|
+
zoom_end = parse_time_param(ransack_params[:occurred_at_lt])
|
|
152
|
+
return nil unless zoom_start && zoom_end
|
|
153
|
+
|
|
154
|
+
[ zoom_start, zoom_end, :custom ]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Priority 4a: custom range from the time range selector (session).
|
|
158
|
+
def from_session_custom
|
|
159
|
+
preference = session_time_range_preference
|
|
160
|
+
return nil unless RailsPulse::TimeRangePreference.custom?(preference)
|
|
161
|
+
|
|
162
|
+
start_time = parse_time_param(preference["start_time"]) || preset_start(default_key)
|
|
163
|
+
end_time = parse_time_param(preference["end_time"]) || Time.zone.now
|
|
164
|
+
[ start_time, end_time, :custom ]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Priority 4b: preset from the time range selector (session).
|
|
168
|
+
def from_session_preset
|
|
169
|
+
preference = session_time_range_preference
|
|
170
|
+
return nil unless preference.is_a?(String) && preference.present?
|
|
171
|
+
|
|
172
|
+
[ preset_start(preference), Time.zone.now, preference.to_sym ]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Priority 5: global filters (session).
|
|
176
|
+
def from_global_filters
|
|
177
|
+
filters = session_global_filters
|
|
178
|
+
return nil unless filters["start_time"].present? || filters["end_time"].present?
|
|
179
|
+
|
|
180
|
+
start_time = parse_time_param(filters["start_time"]) || preset_start(default_key)
|
|
181
|
+
end_time = parse_time_param(filters["end_time"]) || Time.zone.now
|
|
182
|
+
[ start_time, end_time, :custom ]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def ransack_params
|
|
186
|
+
q = params[:q]
|
|
187
|
+
q.respond_to?(:permit) ? q : ActionController::Parameters.new({})
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def session_time_range_preference
|
|
191
|
+
RailsPulse::TimeRangePreference.normalize(session[:time_range_preference])
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def session_global_filters
|
|
195
|
+
filters = session[:global_filters]
|
|
196
|
+
filters.is_a?(Hash) ? filters : {}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Returns nil for anything that does not parse, so a hand-edited or stale
|
|
200
|
+
# value falls back to the default range instead of raising.
|
|
201
|
+
def parse_time_param(param)
|
|
202
|
+
case param
|
|
203
|
+
when Time, DateTime
|
|
204
|
+
param.in_time_zone
|
|
205
|
+
when String
|
|
206
|
+
return nil if param.blank?
|
|
207
|
+
|
|
208
|
+
Time.zone.parse(param)
|
|
209
|
+
when Numeric
|
|
210
|
+
Time.zone.at(param)
|
|
211
|
+
else
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
rescue ArgumentError, TypeError, RangeError
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Rounds to hour or day boundaries in Time.zone (summaries are always
|
|
219
|
+
# bucketed there) and returns the span in hours (float) alongside.
|
|
220
|
+
def normalize(start_time, end_time)
|
|
221
|
+
start_time = start_time.in_time_zone
|
|
222
|
+
end_time = end_time.in_time_zone
|
|
223
|
+
time_diff_hours = (end_time.to_i - start_time.to_i) / 3600.0
|
|
224
|
+
|
|
225
|
+
if time_diff_hours <= 25
|
|
226
|
+
[ start_time.beginning_of_hour, end_time.end_of_hour, time_diff_hours ]
|
|
227
|
+
else
|
|
228
|
+
[ start_time.beginning_of_day, end_time.end_of_day, time_diff_hours ]
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# -- Zoom / table window --------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def resolve_zoom(main_start, main_end)
|
|
235
|
+
selected_column_time = params[:selected_column_time]
|
|
236
|
+
zoom_start = params.delete(:zoom_start_time)
|
|
237
|
+
zoom_end = params.delete(:zoom_end_time)
|
|
238
|
+
|
|
239
|
+
if selected_column_time
|
|
240
|
+
table_start, table_end = normalize_column_time(selected_column_time.to_i, main_start, main_end)
|
|
241
|
+
# Column selection has highest precedence for the table only; the
|
|
242
|
+
# chart keeps showing the full range, so zoom_start/zoom_end are
|
|
243
|
+
# returned as extracted (not normalized) rather than derived here.
|
|
244
|
+
return [ zoom_start, zoom_end, table_start, table_end ]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
zoom_start, zoom_end = normalize_zoom_times(zoom_start.to_i, zoom_end.to_i) if zoom_start && zoom_end
|
|
248
|
+
|
|
249
|
+
table_start = zoom_start || main_start
|
|
250
|
+
table_end = zoom_end || main_end
|
|
251
|
+
|
|
252
|
+
[ zoom_start, zoom_end, table_start, table_end ]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# column_time_ms is JS milliseconds; period boundary is based on the
|
|
256
|
+
# overall page range (main_start/main_end), same as the chart.
|
|
257
|
+
def normalize_column_time(column_time_ms, main_start, main_end)
|
|
258
|
+
column_time_seconds = column_time_ms / 1000
|
|
259
|
+
time_diff_hours = (main_end - main_start) / 3600.0
|
|
260
|
+
column_time_obj = Time.zone&.at(column_time_seconds) || Time.at(column_time_seconds)
|
|
261
|
+
|
|
262
|
+
if time_diff_hours <= 25
|
|
263
|
+
[ column_time_obj.beginning_of_hour.to_i, column_time_obj.end_of_hour.to_i ]
|
|
264
|
+
else
|
|
265
|
+
[ column_time_obj.beginning_of_day.to_i, column_time_obj.end_of_day.to_i ]
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# start_ms/end_ms are JS milliseconds; period boundary is based on the
|
|
270
|
+
# zoomed span itself.
|
|
271
|
+
def normalize_zoom_times(start_ms, end_ms)
|
|
272
|
+
start_seconds = start_ms / 1000
|
|
273
|
+
end_seconds = end_ms / 1000
|
|
274
|
+
time_diff_hours = (end_seconds - start_seconds) / 3600.0
|
|
275
|
+
start_obj = Time.zone&.at(start_seconds) || Time.at(start_seconds)
|
|
276
|
+
end_obj = Time.zone&.at(end_seconds) || Time.at(end_seconds)
|
|
277
|
+
|
|
278
|
+
if time_diff_hours <= 25
|
|
279
|
+
[ start_obj.beginning_of_hour.to_i, end_obj.end_of_hour.to_i ]
|
|
280
|
+
else
|
|
281
|
+
[ start_obj.beginning_of_day.to_i, end_obj.end_of_day.to_i ]
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# -- Duration threshold -----------------------------------------------------
|
|
286
|
+
|
|
287
|
+
def resolve_duration
|
|
288
|
+
thresholds = RailsPulse.configuration.public_send("#{duration_range_type}_thresholds")
|
|
289
|
+
duration_param = ransack_params[:avg_duration] || ransack_params[:duration] || ransack_params[:duration_gteq]
|
|
290
|
+
|
|
291
|
+
if duration_param.present?
|
|
292
|
+
[ threshold_for(thresholds, duration_param), duration_param ]
|
|
293
|
+
elsif (global_threshold = session_global_filters["performance_threshold"]).present?
|
|
294
|
+
[ threshold_for(thresholds, global_threshold), global_threshold.to_sym ]
|
|
295
|
+
else
|
|
296
|
+
[ 0, :all ]
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def threshold_for(thresholds, key)
|
|
301
|
+
case key.to_sym
|
|
302
|
+
when :slow then thresholds[:slow]
|
|
303
|
+
when :very_slow then thresholds[:very_slow]
|
|
304
|
+
when :critical then thresholds[:critical]
|
|
305
|
+
else 0
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
# The background writer's once-a-minute sample, stored as an Event of kind
|
|
3
|
+
# writer_heartbeat: `subject` is "host:pid", `value` the requests dropped
|
|
4
|
+
# since the previous sample, and `metadata` the queue depth, its configured
|
|
5
|
+
# capacity and the lifetime drop count. Stats are otherwise per process, so
|
|
6
|
+
# with eight Puma workers the dashboard could only ever see the one that
|
|
7
|
+
# happened to serve it; these rows let it, the Storage page and
|
|
8
|
+
# rails_pulse:status add every live writer up.
|
|
9
|
+
class WriterHeartbeat
|
|
10
|
+
KIND = "writer_heartbeat".freeze
|
|
11
|
+
# A writer that has not reported for this long is treated as gone.
|
|
12
|
+
LIVE_WINDOW = 3.minutes
|
|
13
|
+
RETENTION = 24.hours
|
|
14
|
+
|
|
15
|
+
Sample = Struct.new(:hostname, :pid, :queue_size, :queue_depth, :dropped_total, :sampled_at, keyword_init: true) do
|
|
16
|
+
def process_label
|
|
17
|
+
"#{hostname}:#{pid}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def events
|
|
23
|
+
RailsPulse::Event.of_kind(KIND)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def record!(hostname:, pid:, queue_size:, queue_depth:, dropped:, dropped_total:, sampled_at: Time.current)
|
|
27
|
+
RailsPulse::Event.insert_all([ {
|
|
28
|
+
kind: KIND,
|
|
29
|
+
subject: "#{hostname}:#{pid}",
|
|
30
|
+
outcome: "sampled",
|
|
31
|
+
value: dropped,
|
|
32
|
+
occurred_at: sampled_at,
|
|
33
|
+
metadata: { hostname: hostname, pid: pid, queue_size: queue_size, queue_depth: queue_depth, dropped_total: dropped_total }.to_json,
|
|
34
|
+
created_at: sampled_at,
|
|
35
|
+
updated_at: sampled_at
|
|
36
|
+
} ])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def prune!
|
|
40
|
+
events.where(occurred_at: ...RETENTION.ago).delete_all
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The latest sample from each writer that reported inside LIVE_WINDOW,
|
|
44
|
+
# newest first. A handful of rows per process, reduced in Ruby.
|
|
45
|
+
def live_processes
|
|
46
|
+
events.since(LIVE_WINDOW.ago).recent.to_a.uniq(&:subject).map { |event| sample_from(event) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# { "host:pid" => requests dropped inside `window` }
|
|
50
|
+
def dropped_by_process(window: 1.hour)
|
|
51
|
+
events.since(window.ago).group(:subject).sum(:value).transform_values(&:to_i)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# What the dashboard, Storage page and status task show:
|
|
55
|
+
# processes live writers
|
|
56
|
+
# queue_depth requests waiting across them, from each one's latest sample
|
|
57
|
+
# queue_size the largest configured capacity among them
|
|
58
|
+
# dropped requests dropped inside `window`, across every process
|
|
59
|
+
# last_sampled_at most recent heartbeat, nil when there is none
|
|
60
|
+
def summary(window: 1.hour)
|
|
61
|
+
processes = live_processes
|
|
62
|
+
{
|
|
63
|
+
processes: processes.size,
|
|
64
|
+
queue_depth: processes.sum(&:queue_depth),
|
|
65
|
+
queue_size: processes.map(&:queue_size).max || 0,
|
|
66
|
+
dropped: events.since(window.ago).sum(:value).to_i,
|
|
67
|
+
last_sampled_at: events.maximum(:occurred_at)
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def sample_from(event)
|
|
74
|
+
meta = event.metadata_hash
|
|
75
|
+
Sample.new(
|
|
76
|
+
hostname: meta["hostname"],
|
|
77
|
+
pid: meta["pid"],
|
|
78
|
+
queue_size: meta["queue_size"].to_i,
|
|
79
|
+
queue_depth: meta["queue_depth"].to_i,
|
|
80
|
+
dropped_total: meta["dropped_total"].to_i,
|
|
81
|
+
sampled_at: event.occurred_at
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class DeploymentSerializer
|
|
5
|
+
def self.serialize(deployment)
|
|
6
|
+
{
|
|
7
|
+
id: deployment.id,
|
|
8
|
+
revision: deployment.revision,
|
|
9
|
+
short_revision: deployment.short_revision,
|
|
10
|
+
started_at: deployment.started_at,
|
|
11
|
+
finished_at: deployment.finished_at,
|
|
12
|
+
duration_seconds: deployment.duration&.round(1),
|
|
13
|
+
in_progress: deployment.in_progress?,
|
|
14
|
+
metadata: deployment.metadata_hash
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class ExceptionGroupSerializer
|
|
5
|
+
def self.serialize(group)
|
|
6
|
+
{
|
|
7
|
+
id: group.id,
|
|
8
|
+
fingerprint: group.fingerprint,
|
|
9
|
+
exception_class: group.exception_class,
|
|
10
|
+
location: group.location,
|
|
11
|
+
message: group.message,
|
|
12
|
+
status: group.status,
|
|
13
|
+
occurrence_count: group.occurrence_count,
|
|
14
|
+
first_seen_at: group.first_seen_at,
|
|
15
|
+
last_seen_at: group.last_seen_at,
|
|
16
|
+
resolved_at: group.resolved_at,
|
|
17
|
+
preserve: group.preserve
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class ExceptionOccurrenceSerializer
|
|
5
|
+
def self.serialize(occurrence)
|
|
6
|
+
{
|
|
7
|
+
id: occurrence.id,
|
|
8
|
+
exception_class: occurrence.exception_class,
|
|
9
|
+
message: occurrence.message,
|
|
10
|
+
occurred_at: occurrence.occurred_at,
|
|
11
|
+
request_method: occurrence.request_method,
|
|
12
|
+
request_url: occurrence.request_url,
|
|
13
|
+
request_params: occurrence.request_params.presence,
|
|
14
|
+
environment: occurrence.environment,
|
|
15
|
+
deploy_sha: occurrence.deploy_sha,
|
|
16
|
+
backtrace: Array(occurrence.backtrace)
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|