rails_pulse 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +72 -176
- data/Rakefile +77 -2
- data/app/assets/stylesheets/rails_pulse/application.css +0 -12
- data/app/controllers/concerns/chart_table_concern.rb +21 -4
- data/app/controllers/concerns/response_range_concern.rb +6 -3
- data/app/controllers/concerns/time_range_concern.rb +5 -10
- data/app/controllers/concerns/zoom_range_concern.rb +1 -1
- data/app/controllers/rails_pulse/application_controller.rb +8 -4
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -0
- data/app/controllers/rails_pulse/queries_controller.rb +65 -50
- data/app/controllers/rails_pulse/requests_controller.rb +24 -12
- data/app/controllers/rails_pulse/routes_controller.rb +59 -24
- data/app/helpers/rails_pulse/application_helper.rb +0 -1
- data/app/helpers/rails_pulse/chart_formatters.rb +3 -3
- data/app/helpers/rails_pulse/chart_helper.rb +6 -2
- data/app/helpers/rails_pulse/status_helper.rb +10 -4
- data/app/javascript/rails_pulse/controllers/index_controller.js +117 -33
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +17 -27
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +41 -0
- data/app/jobs/rails_pulse/summary_job.rb +53 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +28 -7
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +18 -22
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +18 -7
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +34 -41
- data/app/models/rails_pulse/operation.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +47 -23
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +33 -26
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +34 -45
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +23 -97
- data/app/models/rails_pulse/queries/tables/index.rb +74 -0
- data/app/models/rails_pulse/query.rb +1 -0
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +23 -84
- data/app/models/rails_pulse/route.rb +1 -6
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +45 -23
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +38 -45
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +34 -47
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +30 -25
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +23 -100
- data/app/models/rails_pulse/routes/tables/index.rb +57 -40
- data/app/models/rails_pulse/summary.rb +143 -0
- data/app/services/rails_pulse/summary_service.rb +199 -0
- data/app/views/layouts/rails_pulse/application.html.erb +4 -4
- data/app/views/rails_pulse/components/_empty_state.html.erb +11 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +10 -24
- data/app/views/rails_pulse/dashboard/index.html.erb +54 -36
- data/app/views/rails_pulse/queries/_show_table.html.erb +1 -1
- data/app/views/rails_pulse/queries/_table.html.erb +10 -12
- data/app/views/rails_pulse/queries/index.html.erb +41 -34
- data/app/views/rails_pulse/queries/show.html.erb +38 -31
- data/app/views/rails_pulse/requests/_operations.html.erb +32 -26
- data/app/views/rails_pulse/requests/_table.html.erb +1 -3
- data/app/views/rails_pulse/requests/index.html.erb +42 -34
- data/app/views/rails_pulse/routes/_table.html.erb +13 -13
- data/app/views/rails_pulse/routes/index.html.erb +43 -35
- data/app/views/rails_pulse/routes/show.html.erb +42 -35
- data/config/initializers/rails_pulse.rb +0 -12
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +54 -0
- data/db/rails_pulse_schema.rb +121 -0
- data/lib/generators/rails_pulse/install_generator.rb +41 -4
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +60 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +0 -12
- data/lib/rails_pulse/configuration.rb +0 -11
- data/lib/rails_pulse/engine.rb +0 -1
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/tasks/rails_pulse.rake +58 -0
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +1 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +3 -3
- data/public/rails-pulse-assets/search.svg +43 -0
- metadata +27 -11
- data/app/controllers/rails_pulse/caches_controller.rb +0 -115
- data/app/helpers/rails_pulse/cached_component_helper.rb +0 -73
- data/app/models/rails_pulse/component_cache_key.rb +0 -33
- data/app/views/rails_pulse/caches/show.html.erb +0 -9
- data/db/migrate/20250227235904_create_routes.rb +0 -12
- data/db/migrate/20250227235915_create_requests.rb +0 -19
- data/db/migrate/20250228000000_create_queries.rb +0 -14
- data/db/migrate/20250228000056_create_operations.rb +0 -24
- data/lib/rails_pulse/migration.rb +0 -29
@@ -0,0 +1,143 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
class Summary < RailsPulse::ApplicationRecord
|
3
|
+
self.table_name = "rails_pulse_summaries"
|
4
|
+
|
5
|
+
PERIOD_TYPES = %w[hour day week month].freeze
|
6
|
+
|
7
|
+
# Polymorphic association
|
8
|
+
belongs_to :summarizable, polymorphic: true, optional: true # Optional for Request summaries
|
9
|
+
|
10
|
+
# Convenience associations for easier querying
|
11
|
+
belongs_to :route, -> { where(rails_pulse_summaries: { summarizable_type: "RailsPulse::Route" }) },
|
12
|
+
foreign_key: "summarizable_id", class_name: "RailsPulse::Route", optional: true
|
13
|
+
belongs_to :query, -> { where(rails_pulse_summaries: { summarizable_type: "RailsPulse::Query" }) },
|
14
|
+
foreign_key: "summarizable_id", class_name: "RailsPulse::Query", optional: true
|
15
|
+
|
16
|
+
# Validations
|
17
|
+
validates :period_type, inclusion: { in: PERIOD_TYPES }
|
18
|
+
validates :period_start, presence: true
|
19
|
+
validates :period_end, presence: true
|
20
|
+
|
21
|
+
# Scopes
|
22
|
+
scope :for_period_type, ->(type) { where(period_type: type) }
|
23
|
+
scope :for_date_range, ->(start_date, end_date) {
|
24
|
+
where(period_start: start_date..end_date)
|
25
|
+
}
|
26
|
+
scope :for_requests, -> { where(summarizable_type: "RailsPulse::Request") }
|
27
|
+
scope :for_routes, -> { where(summarizable_type: "RailsPulse::Route") }
|
28
|
+
scope :for_queries, -> { where(summarizable_type: "RailsPulse::Query") }
|
29
|
+
scope :recent, -> { order(period_start: :desc) }
|
30
|
+
|
31
|
+
# Special scope for overall request summaries
|
32
|
+
scope :overall_requests, -> {
|
33
|
+
where(summarizable_type: "RailsPulse::Request", summarizable_id: 0)
|
34
|
+
}
|
35
|
+
|
36
|
+
# Ransack configuration
|
37
|
+
def self.ransackable_attributes(auth_object = nil)
|
38
|
+
%w[
|
39
|
+
period_start period_end avg_duration max_duration count error_count
|
40
|
+
requests_per_minute error_rate_percentage route_path_cont
|
41
|
+
execution_count total_time_consumed normalized_sql
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.ransackable_associations(auth_object = nil)
|
46
|
+
%w[route query]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Custom ransackers for calculated fields (designed to work with GROUP BY queries)
|
50
|
+
ransacker :count do
|
51
|
+
Arel.sql("SUM(rails_pulse_summaries.count)") # Use SUM for proper grouping
|
52
|
+
end
|
53
|
+
|
54
|
+
ransacker :requests_per_minute do
|
55
|
+
Arel.sql("SUM(rails_pulse_summaries.count) / 60.0") # Use SUM for consistency
|
56
|
+
end
|
57
|
+
|
58
|
+
ransacker :error_rate_percentage do
|
59
|
+
Arel.sql("(SUM(rails_pulse_summaries.error_count) * 100.0) / SUM(rails_pulse_summaries.count)") # Use SUM for both
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Ransacker for route path sorting (when joined with routes table)
|
64
|
+
ransacker :route_path do
|
65
|
+
Arel.sql("rails_pulse_routes.path")
|
66
|
+
end
|
67
|
+
|
68
|
+
# Ransacker for route path filtering using subquery (works without JOIN)
|
69
|
+
ransacker :route_path_cont do |parent|
|
70
|
+
Arel.sql(<<-SQL)
|
71
|
+
rails_pulse_summaries.summarizable_id IN (
|
72
|
+
SELECT id FROM rails_pulse_routes
|
73
|
+
WHERE rails_pulse_routes.path LIKE '%' || ? || '%'
|
74
|
+
)
|
75
|
+
SQL
|
76
|
+
end
|
77
|
+
|
78
|
+
# Sorting-specific ransackers for GROUP BY compatibility (used only in ORDER BY)
|
79
|
+
# These use different names to avoid conflicts with filtering
|
80
|
+
ransacker :avg_duration_sort do
|
81
|
+
Arel.sql("AVG(rails_pulse_summaries.avg_duration)")
|
82
|
+
end
|
83
|
+
|
84
|
+
ransacker :max_duration_sort do
|
85
|
+
Arel.sql("MAX(rails_pulse_summaries.max_duration)")
|
86
|
+
end
|
87
|
+
|
88
|
+
ransacker :count_sort do
|
89
|
+
Arel.sql("SUM(rails_pulse_summaries.count)")
|
90
|
+
end
|
91
|
+
|
92
|
+
ransacker :error_count_sort do
|
93
|
+
Arel.sql("SUM(rails_pulse_summaries.error_count)")
|
94
|
+
end
|
95
|
+
|
96
|
+
ransacker :success_count_sort do
|
97
|
+
Arel.sql("SUM(rails_pulse_summaries.success_count)")
|
98
|
+
end
|
99
|
+
|
100
|
+
ransacker :total_time_consumed_sort do
|
101
|
+
Arel.sql("SUM(rails_pulse_summaries.count * rails_pulse_summaries.avg_duration)")
|
102
|
+
end
|
103
|
+
|
104
|
+
# Alias execution_count_sort to count_sort for queries table compatibility
|
105
|
+
ransacker :execution_count_sort do
|
106
|
+
Arel.sql("SUM(rails_pulse_summaries.count)")
|
107
|
+
end
|
108
|
+
|
109
|
+
# Ransackers for queries table calculated fields
|
110
|
+
ransacker :execution_count do
|
111
|
+
Arel.sql("SUM(rails_pulse_summaries.count)") # Total executions
|
112
|
+
end
|
113
|
+
|
114
|
+
ransacker :total_time_consumed do
|
115
|
+
Arel.sql("SUM(rails_pulse_summaries.count * rails_pulse_summaries.avg_duration)") # Total time consumed
|
116
|
+
end
|
117
|
+
|
118
|
+
# Ransacker for query SQL sorting (when joined with queries table)
|
119
|
+
ransacker :normalized_sql do
|
120
|
+
Arel.sql("rails_pulse_queries.normalized_sql")
|
121
|
+
end
|
122
|
+
|
123
|
+
class << self
|
124
|
+
def calculate_period_end(period_type, start_time)
|
125
|
+
case period_type
|
126
|
+
when "hour" then start_time.end_of_hour
|
127
|
+
when "day" then start_time.end_of_day
|
128
|
+
when "week" then start_time.end_of_week
|
129
|
+
when "month" then start_time.end_of_month
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def normalize_period_start(period_type, time)
|
134
|
+
case period_type
|
135
|
+
when "hour" then time.beginning_of_hour
|
136
|
+
when "day" then time.beginning_of_day
|
137
|
+
when "week" then time.beginning_of_week
|
138
|
+
when "month" then time.beginning_of_month
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
|
2
|
+
module RailsPulse
|
3
|
+
class SummaryService
|
4
|
+
attr_reader :period_type, :start_time, :end_time
|
5
|
+
|
6
|
+
def initialize(period_type, start_time)
|
7
|
+
@period_type = period_type
|
8
|
+
@start_time = Summary.normalize_period_start(period_type, start_time)
|
9
|
+
@end_time = Summary.calculate_period_end(period_type, @start_time)
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform
|
13
|
+
Rails.logger.info "[RailsPulse] Starting #{period_type} summary for #{start_time}"
|
14
|
+
|
15
|
+
ActiveRecord::Base.transaction do
|
16
|
+
aggregate_requests # Overall system metrics
|
17
|
+
aggregate_routes # Per-route metrics
|
18
|
+
aggregate_queries # Per-query metrics
|
19
|
+
end
|
20
|
+
|
21
|
+
Rails.logger.info "[RailsPulse] Completed #{period_type} summary"
|
22
|
+
rescue => e
|
23
|
+
Rails.logger.error "[RailsPulse] Summary failed: #{e.message}"
|
24
|
+
raise
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def aggregate_requests
|
30
|
+
# Create a single summary for ALL requests in this period
|
31
|
+
requests = Request.where(occurred_at: start_time...end_time)
|
32
|
+
|
33
|
+
return if requests.empty?
|
34
|
+
|
35
|
+
# Get all durations and statuses for percentile calculations
|
36
|
+
request_data = requests.pluck(:duration, :status)
|
37
|
+
durations = request_data.map(&:first).compact.sort
|
38
|
+
statuses = request_data.map(&:second)
|
39
|
+
|
40
|
+
# Find or create the overall request summary
|
41
|
+
summary = Summary.find_or_initialize_by(
|
42
|
+
summarizable_type: "RailsPulse::Request",
|
43
|
+
summarizable_id: 0, # Use 0 as a special ID for overall summaries
|
44
|
+
period_type: period_type,
|
45
|
+
period_start: start_time
|
46
|
+
)
|
47
|
+
|
48
|
+
summary.assign_attributes(
|
49
|
+
period_end: end_time,
|
50
|
+
count: durations.size,
|
51
|
+
avg_duration: durations.any? ? durations.sum.to_f / durations.size : 0,
|
52
|
+
min_duration: durations.min,
|
53
|
+
max_duration: durations.max,
|
54
|
+
total_duration: durations.sum,
|
55
|
+
p50_duration: calculate_percentile(durations, 0.5),
|
56
|
+
p95_duration: calculate_percentile(durations, 0.95),
|
57
|
+
p99_duration: calculate_percentile(durations, 0.99),
|
58
|
+
stddev_duration: calculate_stddev(durations, durations.sum.to_f / durations.size),
|
59
|
+
error_count: statuses.count { |s| s >= 400 },
|
60
|
+
success_count: statuses.count { |s| s < 400 },
|
61
|
+
status_2xx: statuses.count { |s| s.between?(200, 299) },
|
62
|
+
status_3xx: statuses.count { |s| s.between?(300, 399) },
|
63
|
+
status_4xx: statuses.count { |s| s.between?(400, 499) },
|
64
|
+
status_5xx: statuses.count { |s| s >= 500 }
|
65
|
+
)
|
66
|
+
|
67
|
+
summary.save!
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def aggregate_routes
|
73
|
+
# Use ActiveRecord for cross-database compatibility
|
74
|
+
route_groups = Request
|
75
|
+
.where(occurred_at: start_time...end_time)
|
76
|
+
.where.not(route_id: nil)
|
77
|
+
.group(:route_id)
|
78
|
+
|
79
|
+
# Calculate basic aggregates
|
80
|
+
basic_stats = route_groups.pluck(
|
81
|
+
:route_id,
|
82
|
+
Arel.sql("COUNT(*) as request_count"),
|
83
|
+
Arel.sql("AVG(duration) as avg_duration"),
|
84
|
+
Arel.sql("MIN(duration) as min_duration"),
|
85
|
+
Arel.sql("MAX(duration) as max_duration"),
|
86
|
+
Arel.sql("SUM(duration) as total_duration")
|
87
|
+
)
|
88
|
+
|
89
|
+
basic_stats.each do |stats|
|
90
|
+
route_id = stats[0]
|
91
|
+
|
92
|
+
# Calculate percentiles and status counts separately for cross-DB compatibility
|
93
|
+
durations = Request
|
94
|
+
.where(occurred_at: start_time...end_time)
|
95
|
+
.where(route_id: route_id)
|
96
|
+
.pluck(:duration, :status)
|
97
|
+
|
98
|
+
sorted_durations = durations.map(&:first).compact.sort
|
99
|
+
statuses = durations.map(&:last)
|
100
|
+
|
101
|
+
summary = Summary.find_or_initialize_by(
|
102
|
+
summarizable_type: "RailsPulse::Route",
|
103
|
+
summarizable_id: route_id,
|
104
|
+
period_type: period_type,
|
105
|
+
period_start: start_time
|
106
|
+
)
|
107
|
+
|
108
|
+
summary.assign_attributes(
|
109
|
+
period_end: end_time,
|
110
|
+
count: stats[1],
|
111
|
+
avg_duration: stats[2],
|
112
|
+
min_duration: stats[3],
|
113
|
+
max_duration: stats[4],
|
114
|
+
total_duration: stats[5],
|
115
|
+
p50_duration: calculate_percentile(sorted_durations, 0.5),
|
116
|
+
p95_duration: calculate_percentile(sorted_durations, 0.95),
|
117
|
+
p99_duration: calculate_percentile(sorted_durations, 0.99),
|
118
|
+
stddev_duration: calculate_stddev(sorted_durations, stats[2]),
|
119
|
+
error_count: statuses.count { |s| s >= 400 },
|
120
|
+
success_count: statuses.count { |s| s < 400 },
|
121
|
+
status_2xx: statuses.count { |s| s.between?(200, 299) },
|
122
|
+
status_3xx: statuses.count { |s| s.between?(300, 399) },
|
123
|
+
status_4xx: statuses.count { |s| s.between?(400, 499) },
|
124
|
+
status_5xx: statuses.count { |s| s >= 500 }
|
125
|
+
)
|
126
|
+
|
127
|
+
summary.save!
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def aggregate_queries
|
132
|
+
query_groups = Operation
|
133
|
+
.where(occurred_at: start_time...end_time)
|
134
|
+
.where.not(query_id: nil)
|
135
|
+
.group(:query_id)
|
136
|
+
|
137
|
+
basic_stats = query_groups.pluck(
|
138
|
+
:query_id,
|
139
|
+
Arel.sql("COUNT(*) as execution_count"),
|
140
|
+
Arel.sql("AVG(duration) as avg_duration"),
|
141
|
+
Arel.sql("MIN(duration) as min_duration"),
|
142
|
+
Arel.sql("MAX(duration) as max_duration"),
|
143
|
+
Arel.sql("SUM(duration) as total_duration")
|
144
|
+
)
|
145
|
+
|
146
|
+
basic_stats.each do |stats|
|
147
|
+
query_id = stats[0]
|
148
|
+
|
149
|
+
# Calculate percentiles separately
|
150
|
+
durations = Operation
|
151
|
+
.where(occurred_at: start_time...end_time)
|
152
|
+
.where(query_id: query_id)
|
153
|
+
.pluck(:duration)
|
154
|
+
.compact
|
155
|
+
.sort
|
156
|
+
|
157
|
+
summary = Summary.find_or_initialize_by(
|
158
|
+
summarizable_type: "RailsPulse::Query",
|
159
|
+
summarizable_id: query_id,
|
160
|
+
period_type: period_type,
|
161
|
+
period_start: start_time
|
162
|
+
)
|
163
|
+
|
164
|
+
summary.assign_attributes(
|
165
|
+
period_end: end_time,
|
166
|
+
count: stats[1],
|
167
|
+
avg_duration: stats[2],
|
168
|
+
min_duration: stats[3],
|
169
|
+
max_duration: stats[4],
|
170
|
+
total_duration: stats[5],
|
171
|
+
p50_duration: calculate_percentile(durations, 0.5),
|
172
|
+
p95_duration: calculate_percentile(durations, 0.95),
|
173
|
+
p99_duration: calculate_percentile(durations, 0.99),
|
174
|
+
stddev_duration: calculate_stddev(durations, stats[2])
|
175
|
+
)
|
176
|
+
|
177
|
+
summary.save!
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def calculate_percentile(sorted_array, percentile)
|
182
|
+
return nil if sorted_array.empty?
|
183
|
+
|
184
|
+
k = (percentile * (sorted_array.length - 1)).floor
|
185
|
+
f = (percentile * (sorted_array.length - 1)) - k
|
186
|
+
|
187
|
+
return sorted_array[k] if f == 0 || k + 1 >= sorted_array.length
|
188
|
+
|
189
|
+
sorted_array[k] + (sorted_array[k + 1] - sorted_array[k]) * f
|
190
|
+
end
|
191
|
+
|
192
|
+
def calculate_stddev(values, mean)
|
193
|
+
return nil if values.empty? || values.size == 1
|
194
|
+
|
195
|
+
sum_of_squares = values.sum { |v| (v - mean) ** 2 }
|
196
|
+
Math.sqrt(sum_of_squares / (values.size - 1))
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -28,12 +28,12 @@
|
|
28
28
|
<dialog class="sheet sheet--left" style="--sheet-size: 288px;" data-rails-pulse--dialog-target="menu" data-action="click->rails-pulse--dialog#closeOnClickOutside">
|
29
29
|
<div class="sheet__content p-2">
|
30
30
|
<div class="sidebar-menu">
|
31
|
-
|
31
|
+
<%= link_to rails_pulse.root_path, class: "btn sidebar-menu__button" do %>
|
32
32
|
<div class="flex flex-col text-start leading-tight overflow-hidden">
|
33
33
|
<span class="overflow-ellipsis font-semibold">Rails Pulse</span>
|
34
34
|
<span class="overflow-ellipsis text-xs">Open Source</span>
|
35
35
|
</div>
|
36
|
-
|
36
|
+
<% end %>
|
37
37
|
<div class="sidebar-menu__content">
|
38
38
|
<div class="sidebar-menu__group">
|
39
39
|
<nav class="sidebar-menu__items">
|
@@ -47,9 +47,9 @@
|
|
47
47
|
</div>
|
48
48
|
|
49
49
|
<div class="flex items-center gap">
|
50
|
-
|
50
|
+
<%= link_to rails_pulse.root_path, class: "flex items-center gap mie-2" do %>
|
51
51
|
<span class="font-bold">Rails Pulse</span>
|
52
|
-
|
52
|
+
<% end %>
|
53
53
|
<nav class="flex items-center gap text-sm text-subtle show@md" style="--column-gap: 1rem">
|
54
54
|
<%= render 'layouts/rails_pulse/menu_items' %>
|
55
55
|
</nav>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<div class="flex items-center justify-center pbs-12 pbe-12 pis-6 pie-6 mb-8">
|
2
|
+
<div class="flex items-center gap">
|
3
|
+
<div class="shrink-0">
|
4
|
+
<img src="<%= asset_path('search.svg') %>" class="w-32 h-24 opacity-50" alt="No data available" />
|
5
|
+
</div>
|
6
|
+
<div class="text-subtle pis-8">
|
7
|
+
<p class="text-lg font-semibold mbe-2"><%= title %></p>
|
8
|
+
<p class="text-sm"><%= description %></p>
|
9
|
+
</div>
|
10
|
+
</div>
|
11
|
+
</div>
|
@@ -1,15 +1,15 @@
|
|
1
1
|
<%
|
2
|
-
|
3
|
-
id
|
4
|
-
context
|
5
|
-
title
|
6
|
-
summary
|
7
|
-
line_chart_data
|
8
|
-
trend_icon
|
9
|
-
trend_amount
|
10
|
-
trend_text
|
2
|
+
# Use the passed data object directly
|
3
|
+
id = data[:id]
|
4
|
+
context = data[:context]
|
5
|
+
title = data[:title]
|
6
|
+
summary = data[:summary]
|
7
|
+
line_chart_data = data[:line_chart_data]
|
8
|
+
trend_icon = data[:trend_icon]
|
9
|
+
trend_amount = data[:trend_amount]
|
10
|
+
trend_text = data[:trend_text]
|
11
11
|
%>
|
12
|
-
<div class="grid-item" data-controller="rails-pulse--chart">
|
12
|
+
<div class="grid-item" data-controller="rails-pulse--chart" id="<%= id %>">
|
13
13
|
<%= render 'rails_pulse/components/panel', { title: title, card_classes: 'card-compact' } do %>
|
14
14
|
<div class="row mbs-2" style="height: 50px; margin-bottom: 0;">
|
15
15
|
<div class="grid-item">
|
@@ -35,20 +35,6 @@
|
|
35
35
|
<%= trend_amount %>
|
36
36
|
<p class="mis-2 text-subtle text-xs"><%= trend_text %></p>
|
37
37
|
</span>
|
38
|
-
<%
|
39
|
-
refresh_path = rails_pulse.cache_path(id: id, context: context, refresh: true)
|
40
|
-
cached_iso = @cached_at ? @cached_at.iso8601 : nil
|
41
|
-
%>
|
42
|
-
<%= link_to refresh_path,
|
43
|
-
data: {
|
44
|
-
controller: "rails-pulse--timezone",
|
45
|
-
rails_pulse__timezone_target_frame_value: "#{id}_card",
|
46
|
-
rails_pulse__timezone_cached_at_value: cached_iso,
|
47
|
-
turbo_frame: "#{id}_card",
|
48
|
-
turbo_prefetch: "false"
|
49
|
-
} do %>
|
50
|
-
<%= rails_pulse_icon 'refresh-cw', height: "15px", width: "15px" %>
|
51
|
-
<% end %>
|
52
38
|
</div>
|
53
39
|
</div>
|
54
40
|
<% end %>
|
@@ -1,64 +1,82 @@
|
|
1
1
|
<div class="row">
|
2
|
-
<%=
|
3
|
-
<%=
|
4
|
-
<%=
|
5
|
-
<%=
|
2
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
|
3
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_response_times_metric_card } %>
|
4
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @request_count_totals_metric_card } %>
|
5
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @error_rate_per_route_metric_card } %>
|
6
6
|
</div>
|
7
7
|
|
8
8
|
<div class="row">
|
9
9
|
<div class="grid-item" style="height:300px">
|
10
|
-
<%=
|
11
|
-
component: "panel",
|
12
|
-
id: "dashboard_average_response_time",
|
13
|
-
card_classes: 'b-full',
|
10
|
+
<%= render 'rails_pulse/components/panel', {
|
14
11
|
title: 'Average Response Time',
|
12
|
+
card_classes: 'b-full',
|
15
13
|
help_heading: 'Average Response Time',
|
16
14
|
help_text: 'This panel measures the average server-side response time in milliseconds for all HTTP requests processed by your application. This is the time it takes from the server receiving a request until the request is returned to the client in milliseconds.',
|
17
|
-
actions: [{ url: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }]
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
actions: [{ url: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }]
|
16
|
+
} do %>
|
17
|
+
<% if @average_response_time_chart_data.present? %>
|
18
|
+
<div class="chart-container chart-container--slim">
|
19
|
+
<%= bar_chart(
|
20
|
+
@average_response_time_chart_data,
|
21
|
+
code: false,
|
22
|
+
id: "dashboard_average_response_time_chart",
|
23
|
+
height: "100%",
|
24
|
+
options: bar_chart_options(
|
25
|
+
units: "ms"
|
26
|
+
)
|
27
|
+
) %>
|
28
|
+
</div>
|
29
|
+
<% end %>
|
30
|
+
<% end %>
|
21
31
|
</div>
|
22
32
|
|
23
33
|
<div class="grid-item">
|
24
|
-
<%=
|
25
|
-
component: "panel",
|
26
|
-
id: "dashboard_p95_response_time",
|
27
|
-
card_classes: 'b-full',
|
34
|
+
<%= render 'rails_pulse/components/panel', {
|
28
35
|
title: 'Query Performance',
|
36
|
+
card_classes: 'b-full',
|
29
37
|
help_heading: 'Query Performance',
|
30
38
|
help_text: 'This panel measures the 95th percentile response time in milliseconds for queries in your application. This represents the query performance below which 95% of queries fall, indicating the upper-bound performance for most database operations.',
|
31
|
-
actions: [{ url:
|
32
|
-
|
33
|
-
|
34
|
-
|
39
|
+
actions: [{ url: queries_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }]
|
40
|
+
} do %>
|
41
|
+
<% if @p95_response_time_chart_data.present? %>
|
42
|
+
<div class="chart-container chart-container--slim">
|
43
|
+
<%= bar_chart(
|
44
|
+
@p95_response_time_chart_data,
|
45
|
+
code: false,
|
46
|
+
id: "dashboard_p95_response_time_chart",
|
47
|
+
height: "100%",
|
48
|
+
options: bar_chart_options(
|
49
|
+
units: "ms"
|
50
|
+
)
|
51
|
+
) %>
|
52
|
+
</div>
|
53
|
+
<% end %>
|
54
|
+
<% end %>
|
35
55
|
</div>
|
36
56
|
</div>
|
37
57
|
|
38
58
|
<div class="row">
|
39
59
|
<div class="grid-item">
|
40
|
-
<%=
|
41
|
-
component: "panel",
|
42
|
-
id: "dashboard_slow_routes",
|
60
|
+
<%= render 'rails_pulse/components/panel', {
|
43
61
|
title: 'Slowest Routes This Week',
|
44
62
|
help_heading: 'Slowest Routes',
|
45
|
-
help_text: 'This panel shows the slowest routes in your application this week
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
63
|
+
help_text: 'This panel shows the slowest routes in your application this week, including average response time and total request count.',
|
64
|
+
actions: [{ url: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
|
65
|
+
card_classes: 'table-container'
|
66
|
+
} do %>
|
67
|
+
<%= render 'rails_pulse/components/table', table_data: @slow_routes_table_data %>
|
68
|
+
<% end %>
|
50
69
|
</div>
|
51
70
|
|
52
71
|
<div class="grid-item">
|
53
|
-
<%=
|
54
|
-
component: "panel",
|
55
|
-
id: "dashboard_slow_queries",
|
72
|
+
<%= render 'rails_pulse/components/panel', {
|
56
73
|
title: 'Slowest Queries This Week',
|
57
|
-
help_heading: 'Slowest Queries',
|
74
|
+
help_heading: 'Slowest Queries',
|
58
75
|
help_text: 'This panel shows the slowest database queries in your application this week, including average execution time and when they were last seen.',
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
76
|
+
actions: [{ url: queries_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
|
77
|
+
card_classes: 'table-container'
|
78
|
+
} do %>
|
79
|
+
<%= render 'rails_pulse/components/table', table_data: @slow_queries_table_data %>
|
80
|
+
<% end %>
|
63
81
|
</div>
|
64
82
|
</div>
|
@@ -1,28 +1,26 @@
|
|
1
1
|
<% columns = [
|
2
2
|
{ field: :normalized_sql, label: 'Query', class: 'w-auto' },
|
3
|
-
{ field: :
|
4
|
-
{ field: :
|
5
|
-
{ field: :
|
6
|
-
{ field: :performance_status, label: 'Status', class: 'w-16' }
|
7
|
-
{ field: :occurred_at, label: 'Last Seen', class: 'w-32' }
|
3
|
+
{ field: :execution_count_sort, label: 'Executions', class: 'w-24' },
|
4
|
+
{ field: :avg_duration_sort, label: 'Avg Time', class: 'w-24' },
|
5
|
+
{ field: :total_time_consumed_sort, label: 'Total Time', class: 'w-28' },
|
6
|
+
{ field: :performance_status, label: 'Status', class: 'w-16', sortable: false }
|
8
7
|
] %>
|
9
8
|
|
10
9
|
<table class="table mbs-4">
|
11
10
|
<%= render "rails_pulse/components/table_head", columns: columns %>
|
12
11
|
|
13
12
|
<tbody>
|
14
|
-
<% @table_data.each do |
|
13
|
+
<% @table_data.each do |summary| %>
|
15
14
|
<tr>
|
16
15
|
<td class="whitespace-nowrap">
|
17
16
|
<div>
|
18
|
-
<%= link_to html_escape(truncate_sql(
|
17
|
+
<%= link_to html_escape(truncate_sql(summary.normalized_sql)), query_path(summary.query_id), data: { turbo_frame: '_top', } %>
|
19
18
|
</div>
|
20
19
|
</td>
|
21
|
-
<td class="whitespace-nowrap"><%= number_with_delimiter
|
22
|
-
<td class="whitespace-nowrap"><%=
|
23
|
-
<td class="whitespace-nowrap"><%= number_with_delimiter
|
24
|
-
<td class="whitespace-nowrap text-center"><%= query_status_indicator(
|
25
|
-
<td class="whitespace-nowrap"><%= human_readable_occurred_at(query.occurred_at) if query.occurred_at %></td>
|
20
|
+
<td class="whitespace-nowrap"><%= number_with_delimiter summary.execution_count %></td>
|
21
|
+
<td class="whitespace-nowrap"><%= summary.avg_duration.to_i %> ms</td>
|
22
|
+
<td class="whitespace-nowrap"><%= number_with_delimiter summary.total_time_consumed.to_i %> ms</td>
|
23
|
+
<td class="whitespace-nowrap text-center"><%= query_status_indicator(summary.avg_duration) %></td>
|
26
24
|
</tr>
|
27
25
|
<% end %>
|
28
26
|
</tbody>
|