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
|
@@ -71,6 +71,13 @@ module RailsPulse
|
|
|
71
71
|
label: "Deployments",
|
|
72
72
|
description: "Deploy markers shown on charts",
|
|
73
73
|
time_column: :started_at
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: :rails_pulse_events,
|
|
77
|
+
model: "RailsPulse::Event",
|
|
78
|
+
label: "Events",
|
|
79
|
+
description: "What Pulse noticed: writer heartbeats every minute, pruned after a day; any other kind is kept for event_retention_period",
|
|
80
|
+
time_column: :occurred_at
|
|
74
81
|
}
|
|
75
82
|
].freeze
|
|
76
83
|
|
|
@@ -137,6 +144,35 @@ module RailsPulse
|
|
|
137
144
|
}
|
|
138
145
|
end
|
|
139
146
|
|
|
147
|
+
# Built from live_processes/dropped_by_process rather than
|
|
148
|
+
# WriterHeartbeat.summary, which would recompute both internally.
|
|
149
|
+
def tracking
|
|
150
|
+
@tracking ||= begin
|
|
151
|
+
live = RailsPulse::WriterHeartbeat.live_processes
|
|
152
|
+
dropped_by_process = RailsPulse::WriterHeartbeat.dropped_by_process(window: 1.hour)
|
|
153
|
+
processes = live.map do |process|
|
|
154
|
+
{
|
|
155
|
+
label: process.process_label,
|
|
156
|
+
queue_depth: process.queue_depth,
|
|
157
|
+
queue_size: process.queue_size,
|
|
158
|
+
dropped_last_hour: dropped_by_process.fetch(process.process_label, 0),
|
|
159
|
+
last_seen_at: process.sampled_at
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
processes: processes,
|
|
165
|
+
live_count: processes.size,
|
|
166
|
+
queue_depth: live.sum(&:queue_depth),
|
|
167
|
+
queue_size: live.map(&:queue_size).max || 0,
|
|
168
|
+
dropped: dropped_by_process.values.sum,
|
|
169
|
+
last_sampled_at: RailsPulse::WriterHeartbeat.events.maximum(:occurred_at)
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
rescue ActiveRecord::ActiveRecordError
|
|
173
|
+
{ processes: [], live_count: 0, queue_depth: 0, queue_size: 0, dropped: 0, last_sampled_at: nil }
|
|
174
|
+
end
|
|
175
|
+
|
|
140
176
|
def dashboard_tables
|
|
141
177
|
capped = tables.select { |table| table[:limit] }
|
|
142
178
|
return tables.first(4) if capped.empty?
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
class Deployment < RailsPulse::ApplicationRecord
|
|
3
|
+
include HasMetadata
|
|
4
|
+
|
|
3
5
|
self.table_name = "rails_pulse_deployments"
|
|
4
6
|
|
|
5
7
|
# The create endpoint is reachable with a CI token, so every field a
|
|
@@ -52,13 +54,6 @@ module RailsPulse
|
|
|
52
54
|
finished_at - started_at
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
def metadata_hash
|
|
56
|
-
return {} if metadata.blank?
|
|
57
|
-
JSON.parse(metadata)
|
|
58
|
-
rescue JSON::ParserError
|
|
59
|
-
{}
|
|
60
|
-
end
|
|
61
|
-
|
|
62
57
|
def to_chart_marker
|
|
63
58
|
{ timestamp: started_at.to_i * 1000, revision: revision, started_at: started_at.iso8601 }
|
|
64
59
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
# What Rails Pulse noticed, as opposed to what it measured. One row per
|
|
3
|
+
# outcome or sample, tagged by `kind`; `subject` names what it is about,
|
|
4
|
+
# `value` carries its number and `metadata` the kind-specific detail as JSON.
|
|
5
|
+
#
|
|
6
|
+
# The one kind written today is writer_heartbeat (see WriterHeartbeat). The
|
|
7
|
+
# table is generic so a new kind needs no migration. Decision 0019.
|
|
8
|
+
#
|
|
9
|
+
# CleanupService deletes rows older than config.event_retention_period,
|
|
10
|
+
# except kinds listed in config.event_retention_exempt_kinds (rows a writer
|
|
11
|
+
# updates in place). Writer heartbeats are pruned after a day by the writer
|
|
12
|
+
# itself.
|
|
13
|
+
class Event < RailsPulse::ApplicationRecord
|
|
14
|
+
include HasMetadata
|
|
15
|
+
|
|
16
|
+
self.table_name = "rails_pulse_events"
|
|
17
|
+
|
|
18
|
+
validates :kind, :outcome, :occurred_at, presence: true
|
|
19
|
+
|
|
20
|
+
scope :of_kind, ->(*kinds) { where(kind: kinds.flatten.map(&:to_s)) }
|
|
21
|
+
scope :for_subject, ->(subject) { where(subject: subject) }
|
|
22
|
+
scope :since, ->(time) { where(occurred_at: time..) }
|
|
23
|
+
scope :recent, -> { order(occurred_at: :desc) }
|
|
24
|
+
|
|
25
|
+
def self.ransackable_attributes(_auth_object = nil)
|
|
26
|
+
%w[kind subject outcome value occurred_at message created_at updated_at]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.ransackable_associations(_auth_object = nil)
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# table_exists?, rescued — for an upgrader who hasn't migrated yet
|
|
34
|
+
def self.table_available?
|
|
35
|
+
table_exists?
|
|
36
|
+
rescue ActiveRecord::ActiveRecordError
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -16,10 +16,7 @@ module RailsPulse
|
|
|
16
16
|
PER = 1_000
|
|
17
17
|
|
|
18
18
|
def initialize(disabled_tags: [], show_non_tagged: true, period: 14, period_type: "day")
|
|
19
|
-
|
|
20
|
-
@show_non_tagged = show_non_tagged
|
|
21
|
-
@period = period
|
|
22
|
-
@period_type = period_type
|
|
19
|
+
super
|
|
23
20
|
end
|
|
24
21
|
|
|
25
22
|
def to_metric_card
|
|
@@ -63,8 +60,8 @@ module RailsPulse
|
|
|
63
60
|
.where(period_type: @period_type, period_start: range_start..now)
|
|
64
61
|
.select(
|
|
65
62
|
"SUM(rails_pulse_summaries.count) AS total_count",
|
|
66
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
67
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
63
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS current_count"),
|
|
64
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :range_start AND rails_pulse_summaries.period_start < :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS previous_count")
|
|
68
65
|
)
|
|
69
66
|
.take
|
|
70
67
|
|
|
@@ -9,10 +9,7 @@ module RailsPulse
|
|
|
9
9
|
# you about the other.
|
|
10
10
|
class OpenGroups < RailsPulse::Cards::Base
|
|
11
11
|
def initialize(disabled_tags: [], show_non_tagged: true, period: 14, period_type: "day")
|
|
12
|
-
|
|
13
|
-
@show_non_tagged = show_non_tagged
|
|
14
|
-
@period = period
|
|
15
|
-
@period_type = period_type
|
|
12
|
+
super
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
def to_metric_card
|
|
@@ -11,10 +11,7 @@ module RailsPulse
|
|
|
11
11
|
SUMMARIZABLE_TYPE = "RailsPulse::ExceptionGroup".freeze
|
|
12
12
|
|
|
13
13
|
def initialize(disabled_tags: [], show_non_tagged: true, period: 14, period_type: "day")
|
|
14
|
-
|
|
15
|
-
@show_non_tagged = show_non_tagged
|
|
16
|
-
@period = period
|
|
17
|
-
@period_type = period_type
|
|
14
|
+
super
|
|
18
15
|
end
|
|
19
16
|
|
|
20
17
|
def to_metric_card
|
|
@@ -24,8 +21,8 @@ module RailsPulse
|
|
|
24
21
|
|
|
25
22
|
metrics = base_query.select(
|
|
26
23
|
"SUM(rails_pulse_summaries.count) AS total_count",
|
|
27
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
28
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
24
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS current_count"),
|
|
25
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :range_start AND rails_pulse_summaries.period_start < :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS previous_count")
|
|
29
26
|
).take
|
|
30
27
|
|
|
31
28
|
total = metrics&.total_count.to_i
|
|
@@ -8,16 +8,15 @@ module RailsPulse
|
|
|
8
8
|
# silently flattens toward zero the further back you look. Summaries are
|
|
9
9
|
# retained at day granularity indefinitely.
|
|
10
10
|
class OccurrenceVolume
|
|
11
|
-
def initialize(
|
|
12
|
-
@
|
|
13
|
-
@end_time = end_time
|
|
11
|
+
def initialize(window:, period_type: "day")
|
|
12
|
+
@window = window
|
|
14
13
|
@period_type = period_type
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def to_chart_data
|
|
18
17
|
totals = RailsPulse::Summary
|
|
19
18
|
.overall_exceptions
|
|
20
|
-
.where(period_type: @period_type, period_start: @start_time..@end_time)
|
|
19
|
+
.where(period_type: @period_type, period_start: @window.start_time..@window.end_time)
|
|
21
20
|
.group(:period_start)
|
|
22
21
|
.sum(:count)
|
|
23
22
|
|
|
@@ -45,7 +44,7 @@ module RailsPulse
|
|
|
45
44
|
step = @period_type.to_s == "hour" ? 3600 : 86_400
|
|
46
45
|
|
|
47
46
|
{}.tap do |padded|
|
|
48
|
-
(@start_time.to_i..@end_time.to_i).step(step) do |timestamp|
|
|
47
|
+
(@window.start_time.to_i..@window.end_time.to_i).step(step) do |timestamp|
|
|
49
48
|
padded[timestamp] = raw[timestamp] || 0
|
|
50
49
|
end
|
|
51
50
|
end
|
|
@@ -2,14 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Jobs
|
|
3
3
|
module Cards
|
|
4
4
|
class FailureRate < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(job: 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(job: nil, period: 14, **kwargs)
|
|
6
|
+
super(subject: job, period: period, **kwargs)
|
|
13
7
|
end
|
|
14
8
|
|
|
15
9
|
def to_metric_card
|
|
@@ -20,15 +14,15 @@ module RailsPulse
|
|
|
20
14
|
period_type: @period_type,
|
|
21
15
|
period_start: range_start..now
|
|
22
16
|
)
|
|
23
|
-
base_query = base_query.where(summarizable_id: @
|
|
17
|
+
base_query = base_query.where(summarizable_id: @subject.id) if @subject
|
|
24
18
|
|
|
25
19
|
metrics = base_query.select(
|
|
26
20
|
"SUM(rails_pulse_summaries.count) AS total_count",
|
|
27
21
|
"SUM(rails_pulse_summaries.error_count) AS total_errors",
|
|
28
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
29
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
30
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
31
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
22
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS current_count"),
|
|
23
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :current_start THEN rails_pulse_summaries.error_count ELSE 0 END) AS current_errors"),
|
|
24
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :range_start AND rails_pulse_summaries.period_start < :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS previous_count"),
|
|
25
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :range_start AND rails_pulse_summaries.period_start < :current_start THEN rails_pulse_summaries.error_count ELSE 0 END) AS previous_errors")
|
|
32
26
|
).take
|
|
33
27
|
|
|
34
28
|
total_runs = metrics&.total_count.to_i
|
|
@@ -52,7 +46,7 @@ module RailsPulse
|
|
|
52
46
|
period_type: @period_type,
|
|
53
47
|
period_start: current_window_start..now
|
|
54
48
|
)
|
|
55
|
-
sparkline_query = sparkline_query.where(summarizable_id: @
|
|
49
|
+
sparkline_query = sparkline_query.where(summarizable_id: @subject.id) if @subject
|
|
56
50
|
|
|
57
51
|
grouped_errors = bucket_by_period(sparkline_query) { |relation| relation.sum("rails_pulse_summaries.error_count") }
|
|
58
52
|
grouped_counts = bucket_by_period(sparkline_query) { |relation| relation.sum("rails_pulse_summaries.count") }
|
|
@@ -2,14 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Jobs
|
|
3
3
|
module Cards
|
|
4
4
|
class P95Duration < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(job: 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(job: nil, period: 14, **kwargs)
|
|
6
|
+
super(subject: job, period: period, **kwargs)
|
|
13
7
|
end
|
|
14
8
|
|
|
15
9
|
def to_metric_card
|
|
@@ -20,15 +14,15 @@ module RailsPulse
|
|
|
20
14
|
period_type: @period_type,
|
|
21
15
|
period_start: range_start..now
|
|
22
16
|
)
|
|
23
|
-
base_query = base_query.where(summarizable_id: @
|
|
17
|
+
base_query = base_query.where(summarizable_id: @subject.id) if @subject
|
|
24
18
|
|
|
25
19
|
metrics = base_query.select(
|
|
26
20
|
"SUM(rails_pulse_summaries.p95_duration * rails_pulse_summaries.count) AS total_weighted_p95",
|
|
27
21
|
"SUM(rails_pulse_summaries.count) AS total_runs",
|
|
28
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
29
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
30
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
31
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
22
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :current_start THEN rails_pulse_summaries.p95_duration * rails_pulse_summaries.count ELSE 0 END) AS current_weighted_p95"),
|
|
23
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS current_runs"),
|
|
24
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :range_start AND rails_pulse_summaries.period_start < :current_start THEN rails_pulse_summaries.p95_duration * rails_pulse_summaries.count ELSE 0 END) AS previous_weighted_p95"),
|
|
25
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :range_start AND rails_pulse_summaries.period_start < :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS previous_runs")
|
|
32
26
|
).take
|
|
33
27
|
|
|
34
28
|
total_runs = metrics&.total_runs.to_i
|
|
@@ -42,7 +36,7 @@ module RailsPulse
|
|
|
42
36
|
current_p95 = weighted_average(current_weighted_p95, current_runs)
|
|
43
37
|
previous_p95 = weighted_average(previous_weighted_p95, previous_runs)
|
|
44
38
|
|
|
45
|
-
baseline = baseline_trend_for(@
|
|
39
|
+
baseline = baseline_trend_for(@subject, metric: :p95)
|
|
46
40
|
|
|
47
41
|
if baseline
|
|
48
42
|
trend_icon, trend_amount, trend_caption = baseline
|
|
@@ -2,14 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Jobs
|
|
3
3
|
module Cards
|
|
4
4
|
class TotalRuns < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(job: 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(job: nil, period: 14, **kwargs)
|
|
6
|
+
super(subject: job, period: period, **kwargs)
|
|
13
7
|
end
|
|
14
8
|
|
|
15
9
|
def to_metric_card
|
|
@@ -20,12 +14,12 @@ module RailsPulse
|
|
|
20
14
|
period_type: @period_type,
|
|
21
15
|
period_start: range_start..now
|
|
22
16
|
)
|
|
23
|
-
base_query = base_query.where(summarizable_id: @
|
|
17
|
+
base_query = base_query.where(summarizable_id: @subject.id) if @subject
|
|
24
18
|
|
|
25
19
|
metrics = base_query.select(
|
|
26
20
|
"SUM(rails_pulse_summaries.count) AS total_count",
|
|
27
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
28
|
-
"SUM(CASE WHEN rails_pulse_summaries.period_start >=
|
|
21
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS current_count"),
|
|
22
|
+
window_sql("SUM(CASE WHEN rails_pulse_summaries.period_start >= :range_start AND rails_pulse_summaries.period_start < :current_start THEN rails_pulse_summaries.count ELSE 0 END) AS previous_count")
|
|
29
23
|
).take
|
|
30
24
|
|
|
31
25
|
total_runs = metrics&.total_count.to_i
|
|
@@ -33,7 +33,7 @@ module RailsPulse
|
|
|
33
33
|
# Convert to final values (weighted averages) and pad missing data
|
|
34
34
|
step = time_step
|
|
35
35
|
daily_data = {}
|
|
36
|
-
(@start_time.to_i..@end_time.to_i).step(step) do |timestamp|
|
|
36
|
+
(@window.start_time.to_i..@window.end_time.to_i).step(step) do |timestamp|
|
|
37
37
|
if raw_data[timestamp]
|
|
38
38
|
count = raw_data[timestamp][:total_count]
|
|
39
39
|
daily_data[timestamp] = {
|
|
@@ -21,7 +21,7 @@ module RailsPulse
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Pad missing data with zeros using base class helper
|
|
24
|
-
daily_data = pad_data_with_zeros(raw_data, @start_time, @end_time, time_step)
|
|
24
|
+
daily_data = pad_data_with_zeros(raw_data, @window.start_time, @window.end_time, time_step)
|
|
25
25
|
|
|
26
26
|
# Build series data as [timestamp_ms, value] pairs for ECharts time axis
|
|
27
27
|
series = [ {
|
|
@@ -25,7 +25,7 @@ module RailsPulse
|
|
|
25
25
|
# Pad missing data with zeros
|
|
26
26
|
step = time_step
|
|
27
27
|
daily_data = {}
|
|
28
|
-
(@start_time.to_i..@end_time.to_i).step(step) do |timestamp|
|
|
28
|
+
(@window.start_time.to_i..@window.end_time.to_i).step(step) do |timestamp|
|
|
29
29
|
daily_data[timestamp] = raw_data[timestamp] || nil
|
|
30
30
|
end
|
|
31
31
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
# Builds LIKE patterns whose `_`, `%` and escape characters match literally.
|
|
3
|
+
#
|
|
4
|
+
# Every pattern built here must be used with CLAUSE appended to the LIKE
|
|
5
|
+
# expression. SQLite has no default escape character, so `foo!_bar` without an
|
|
6
|
+
# explicit ESCAPE matches a literal `!` while the `_` still wildcards, and the
|
|
7
|
+
# comparison silently matches nothing. `!` is the escape character rather than
|
|
8
|
+
# `\` because MySQL and PostgreSQL disagree on how a backslash is quoted.
|
|
9
|
+
module LikePattern
|
|
10
|
+
ESCAPE_CHARACTER = "!".freeze
|
|
11
|
+
|
|
12
|
+
# Interpolated directly into SQL strings, so it must stay a literal.
|
|
13
|
+
CLAUSE = "ESCAPE '#{ESCAPE_CHARACTER}'".freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# @param value [#to_s] user-supplied text to match anywhere in a column
|
|
18
|
+
# @return [String] escaped pattern wrapped in leading and trailing wildcards
|
|
19
|
+
def containing(value)
|
|
20
|
+
"%#{escape(value)}%"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param value [#to_s] user-supplied text
|
|
24
|
+
# @return [String] the text with LIKE metacharacters escaped
|
|
25
|
+
def escape(value)
|
|
26
|
+
ActiveRecord::Base.sanitize_sql_like(value.to_s, ESCAPE_CHARACTER)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -2,14 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class AverageQueryTimes < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(query: 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(query: nil, **kwargs)
|
|
6
|
+
super(subject: query, **kwargs)
|
|
13
7
|
end
|
|
14
8
|
|
|
15
9
|
def to_metric_card
|
|
@@ -19,10 +13,10 @@ module RailsPulse
|
|
|
19
13
|
metrics = base_query.select(
|
|
20
14
|
"SUM(avg_duration * count) AS total_weighted_duration",
|
|
21
15
|
"SUM(count) AS total_requests",
|
|
22
|
-
"SUM(CASE WHEN period_start >=
|
|
23
|
-
"SUM(CASE WHEN period_start >=
|
|
24
|
-
"SUM(CASE WHEN period_start >=
|
|
25
|
-
"SUM(CASE WHEN period_start >=
|
|
16
|
+
window_sql("SUM(CASE WHEN period_start >= :current_start THEN avg_duration * count ELSE 0 END) AS current_weighted_duration"),
|
|
17
|
+
window_sql("SUM(CASE WHEN period_start >= :current_start THEN count ELSE 0 END) AS current_requests"),
|
|
18
|
+
window_sql("SUM(CASE WHEN period_start >= :range_start AND period_start < :current_start THEN avg_duration * count ELSE 0 END) AS previous_weighted_duration"),
|
|
19
|
+
window_sql("SUM(CASE WHEN period_start >= :range_start AND period_start < :current_start THEN count ELSE 0 END) AS previous_requests")
|
|
26
20
|
).take
|
|
27
21
|
|
|
28
22
|
# Calculate metrics from single query result
|
|
@@ -2,15 +2,6 @@ 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", start_time: nil, end_time: nil)
|
|
6
|
-
@disabled_tags = disabled_tags
|
|
7
|
-
@show_non_tagged = show_non_tagged
|
|
8
|
-
@period = period
|
|
9
|
-
@period_type = period_type
|
|
10
|
-
@start_time = start_time
|
|
11
|
-
@end_time = end_time
|
|
12
|
-
end
|
|
13
|
-
|
|
14
5
|
def to_metric_card
|
|
15
6
|
# Use base class time period helpers
|
|
16
7
|
last_n_units = current_window_start
|
|
@@ -2,14 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class ExecutionRate < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(query: 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(query: nil, **kwargs)
|
|
6
|
+
super(subject: query, **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
|
|
@@ -38,7 +32,7 @@ module RailsPulse
|
|
|
38
32
|
period_type: @period_type,
|
|
39
33
|
period_start: current_window_start..now
|
|
40
34
|
)
|
|
41
|
-
sparkline_query = sparkline_query.where(summarizable_id: @
|
|
35
|
+
sparkline_query = sparkline_query.where(summarizable_id: @subject.id) if @subject
|
|
42
36
|
|
|
43
37
|
grouped_data = bucket_by_period(sparkline_query) { |relation| relation.sum(:count) }
|
|
44
38
|
|
|
@@ -2,14 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class PercentileQueryTimes < RailsPulse::Cards::Base
|
|
5
|
-
def initialize(query: 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(query: nil, **kwargs)
|
|
6
|
+
super(subject: query, **kwargs)
|
|
13
7
|
end
|
|
14
8
|
|
|
15
9
|
def to_metric_card
|
|
@@ -19,8 +13,8 @@ module RailsPulse
|
|
|
19
13
|
metrics = base_query.select(
|
|
20
14
|
"SUM(p95_duration * count) / NULLIF(SUM(count), 0) AS overall_p95",
|
|
21
15
|
"SUM(count) AS total_count",
|
|
22
|
-
"SUM(CASE WHEN period_start >=
|
|
23
|
-
"SUM(CASE WHEN period_start >=
|
|
16
|
+
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"),
|
|
17
|
+
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")
|
|
24
18
|
).take
|
|
25
19
|
|
|
26
20
|
# Calculate metrics from single query result
|
|
@@ -28,7 +22,7 @@ module RailsPulse
|
|
|
28
22
|
current_period_p95 = metrics.current_p95 || 0
|
|
29
23
|
previous_period_p95 = metrics.previous_p95 || 0
|
|
30
24
|
|
|
31
|
-
baseline = baseline_trend_for(@
|
|
25
|
+
baseline = baseline_trend_for(@subject, metric: :p95)
|
|
32
26
|
|
|
33
27
|
if baseline
|
|
34
28
|
trend_icon, trend_amount, trend_caption = baseline
|
|
@@ -44,7 +38,7 @@ module RailsPulse
|
|
|
44
38
|
period_type: @period_type,
|
|
45
39
|
period_start: current_window_start..now
|
|
46
40
|
)
|
|
47
|
-
sparkline_query = sparkline_query.where(summarizable_id: @
|
|
41
|
+
sparkline_query = sparkline_query.where(summarizable_id: @subject.id) if @subject
|
|
48
42
|
|
|
49
43
|
weighted_sums = bucket_by_period(sparkline_query) { |relation| relation.sum("p95_duration * count") }
|
|
50
44
|
period_counts = bucket_by_period(sparkline_query) { |relation| relation.sum(:count) }
|
|
@@ -2,9 +2,8 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Charts
|
|
4
4
|
class DatabaseLoad
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
7
|
-
@end_time = end_time
|
|
5
|
+
def initialize(window:, period_type: :day, disabled_tags: [], show_non_tagged: true)
|
|
6
|
+
@window = window
|
|
8
7
|
@period_type = period_type
|
|
9
8
|
@disabled_tags = disabled_tags
|
|
10
9
|
@show_non_tagged = show_non_tagged
|
|
@@ -19,7 +18,7 @@ module RailsPulse
|
|
|
19
18
|
.where(
|
|
20
19
|
summarizable_type: "RailsPulse::Query",
|
|
21
20
|
period_type: @period_type,
|
|
22
|
-
period_start:
|
|
21
|
+
period_start: @window.start_time..@window.end_time
|
|
23
22
|
)
|
|
24
23
|
|
|
25
24
|
# Get route summaries (total request time)
|
|
@@ -28,7 +27,7 @@ module RailsPulse
|
|
|
28
27
|
.where(
|
|
29
28
|
summarizable_type: "RailsPulse::Route",
|
|
30
29
|
period_type: @period_type,
|
|
31
|
-
period_start:
|
|
30
|
+
period_start: @window.start_time..@window.end_time
|
|
32
31
|
)
|
|
33
32
|
|
|
34
33
|
return nil if query_summaries.empty? || route_summaries.empty?
|
|
@@ -52,7 +51,7 @@ module RailsPulse
|
|
|
52
51
|
# <25% = green (healthy), 25-40% = yellow (watch), >40% = red (bottleneck)
|
|
53
52
|
bar_data = []
|
|
54
53
|
|
|
55
|
-
(@start_time.to_i..@end_time.to_i).step(step) do |timestamp|
|
|
54
|
+
(@window.start_time.to_i..@window.end_time.to_i).step(step) do |timestamp|
|
|
56
55
|
query_time = query_time_by_period[timestamp] || 0
|
|
57
56
|
request_time = request_time_by_period[timestamp] || 0
|
|
58
57
|
percentage = request_time > 0 ? (query_time.to_f / request_time * 100).round(1) : 0
|
|
@@ -18,7 +18,7 @@ module RailsPulse
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# Pad missing data with zeros using base class helper
|
|
21
|
-
execution_data = pad_data_with_zeros(raw_data, @start_time, @end_time, time_step)
|
|
21
|
+
execution_data = pad_data_with_zeros(raw_data, @window.start_time, @window.end_time, time_step)
|
|
22
22
|
|
|
23
23
|
# Build series data as [timestamp_ms, value] pairs for ECharts time axis
|
|
24
24
|
series = [ {
|