rails_pulse 0.1.3 → 0.2.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 +134 -16
- data/Rakefile +315 -83
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/stylesheets/rails_pulse/components/datepicker.css +191 -0
- data/app/assets/stylesheets/rails_pulse/components/switch.css +36 -0
- data/app/assets/stylesheets/rails_pulse/components/tags.css +98 -0
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +26 -0
- data/app/controllers/concerns/response_range_concern.rb +15 -2
- data/app/controllers/concerns/tag_filter_concern.rb +26 -0
- data/app/controllers/concerns/time_range_concern.rb +27 -8
- data/app/controllers/rails_pulse/application_controller.rb +73 -0
- data/app/controllers/rails_pulse/queries_controller.rb +18 -21
- data/app/controllers/rails_pulse/requests_controller.rb +80 -35
- data/app/controllers/rails_pulse/routes_controller.rb +4 -2
- data/app/controllers/rails_pulse/tags_controller.rb +51 -0
- data/app/helpers/rails_pulse/application_helper.rb +2 -0
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +1 -1
- data/app/helpers/rails_pulse/chart_helper.rb +1 -1
- data/app/helpers/rails_pulse/form_helper.rb +75 -0
- data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
- data/app/helpers/rails_pulse/tags_helper.rb +29 -0
- data/app/javascript/rails_pulse/application.js +6 -0
- data/app/javascript/rails_pulse/controllers/custom_range_controller.js +115 -0
- data/app/javascript/rails_pulse/controllers/datepicker_controller.js +48 -0
- data/app/javascript/rails_pulse/controllers/global_filters_controller.js +110 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +11 -3
- data/app/models/concerns/taggable.rb +61 -0
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +1 -1
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +56 -17
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +3 -7
- data/app/models/rails_pulse/queries/tables/index.rb +10 -2
- data/app/models/rails_pulse/query.rb +2 -0
- data/app/models/rails_pulse/request.rb +10 -2
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +2 -2
- data/app/models/rails_pulse/requests/tables/index.rb +77 -0
- data/app/models/rails_pulse/route.rb +2 -0
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +16 -5
- data/app/models/rails_pulse/routes/tables/index.rb +14 -4
- data/app/models/rails_pulse/summary.rb +7 -7
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +11 -3
- data/app/services/rails_pulse/summary_service.rb +2 -0
- data/app/views/layouts/rails_pulse/_global_filters.html.erb +84 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +5 -5
- data/app/views/layouts/rails_pulse/application.html.erb +8 -5
- data/app/views/rails_pulse/components/_metric_card.html.erb +2 -2
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
- data/app/views/rails_pulse/components/_page_header.html.erb +20 -0
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
- data/app/views/rails_pulse/operations/show.html.erb +1 -1
- data/app/views/rails_pulse/queries/_analysis_results.html.erb +53 -23
- data/app/views/rails_pulse/queries/_show_table.html.erb +33 -5
- data/app/views/rails_pulse/queries/_table.html.erb +4 -6
- data/app/views/rails_pulse/queries/index.html.erb +3 -7
- data/app/views/rails_pulse/queries/show.html.erb +3 -7
- data/app/views/rails_pulse/requests/_table.html.erb +32 -19
- data/app/views/rails_pulse/requests/index.html.erb +45 -55
- data/app/views/rails_pulse/requests/show.html.erb +1 -3
- data/app/views/rails_pulse/routes/_requests_table.html.erb +41 -0
- data/app/views/rails_pulse/routes/_table.html.erb +4 -8
- data/app/views/rails_pulse/routes/index.html.erb +4 -8
- data/app/views/rails_pulse/routes/show.html.erb +6 -12
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +73 -0
- data/config/initializers/rails_charts_csp_patch.rb +32 -40
- data/config/routes.rb +5 -0
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
- data/db/rails_pulse_schema.rb +4 -1
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +25 -9
- data/lib/generators/rails_pulse/install_generator.rb +30 -7
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +75 -2
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +3 -2
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +21 -0
- data/lib/generators/rails_pulse/upgrade_generator.rb +147 -30
- data/lib/rails_pulse/configuration.rb +16 -1
- data/lib/rails_pulse/engine.rb +21 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/public/rails-pulse-assets/rails-pulse-icons.js +16 -15
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
- 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 +73 -69
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- metadata +20 -5
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +0 -12
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +0 -54
- data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +0 -13
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["wrapper", "dialog", "dateRange", "indicator", "form"]
|
|
5
|
+
static values = {
|
|
6
|
+
active: { type: Boolean, default: false }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.updateIndicator()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Open the global filters dialog
|
|
14
|
+
open(event) {
|
|
15
|
+
event.preventDefault()
|
|
16
|
+
|
|
17
|
+
// If there's a value in the date range input, make sure flatpickr knows about it
|
|
18
|
+
if (this.dateRangeTarget.value) {
|
|
19
|
+
const datepickerController = this.application.getControllerForElementAndIdentifier(
|
|
20
|
+
this.dateRangeTarget,
|
|
21
|
+
'rails-pulse--datepicker'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if (datepickerController && datepickerController.flatpickr) {
|
|
25
|
+
const value = this.dateRangeTarget.value
|
|
26
|
+
// Parse the "start to end" format
|
|
27
|
+
if (value.includes(' to ')) {
|
|
28
|
+
const [start, end] = value.split(' to ').map(d => d.trim())
|
|
29
|
+
// Set the dates in flatpickr
|
|
30
|
+
datepickerController.flatpickr.setDate([start, end], false)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.wrapperTarget.style.display = 'flex'
|
|
36
|
+
// Prevent body scroll when dialog is open
|
|
37
|
+
document.body.style.overflow = 'hidden'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Close the dialog
|
|
41
|
+
close(event) {
|
|
42
|
+
if (event) {
|
|
43
|
+
event.preventDefault()
|
|
44
|
+
}
|
|
45
|
+
this.wrapperTarget.style.display = 'none'
|
|
46
|
+
// Restore body scroll
|
|
47
|
+
document.body.style.overflow = ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Close dialog when clicking outside
|
|
51
|
+
closeOnClickOutside(event) {
|
|
52
|
+
if (event.target === this.wrapperTarget) {
|
|
53
|
+
this.close(event)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle form submission - parse date range and add individual params
|
|
58
|
+
submit(event) {
|
|
59
|
+
// If clear button was clicked, let it through as-is
|
|
60
|
+
if (event.submitter && event.submitter.name === "clear") {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dateRangeValue = this.dateRangeTarget.value
|
|
65
|
+
const form = event.target
|
|
66
|
+
|
|
67
|
+
// Parse date range if provided
|
|
68
|
+
if (dateRangeValue && dateRangeValue.includes(' to ')) {
|
|
69
|
+
const [startTime, endTime] = dateRangeValue.split(' to ').map(d => d.trim())
|
|
70
|
+
|
|
71
|
+
// Remove any existing hidden inputs
|
|
72
|
+
form.querySelectorAll('input[name="start_time"], input[name="end_time"]').forEach(el => el.remove())
|
|
73
|
+
|
|
74
|
+
// Add new hidden inputs
|
|
75
|
+
const startInput = document.createElement('input')
|
|
76
|
+
startInput.type = 'hidden'
|
|
77
|
+
startInput.name = 'start_time'
|
|
78
|
+
startInput.value = startTime
|
|
79
|
+
form.appendChild(startInput)
|
|
80
|
+
|
|
81
|
+
const endInput = document.createElement('input')
|
|
82
|
+
endInput.type = 'hidden'
|
|
83
|
+
endInput.name = 'end_time'
|
|
84
|
+
endInput.value = endTime
|
|
85
|
+
form.appendChild(endInput)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Tag switches are already being submitted as enabled_tags[]
|
|
89
|
+
// The controller will convert these to disabled_tags
|
|
90
|
+
// No additional processing needed here
|
|
91
|
+
|
|
92
|
+
// No validation needed - user can apply any combination of filters
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Update visual indicator based on activeValue
|
|
96
|
+
updateIndicator() {
|
|
97
|
+
if (this.hasIndicatorTarget) {
|
|
98
|
+
if (this.activeValue) {
|
|
99
|
+
this.indicatorTarget.classList.add("global-filters-active")
|
|
100
|
+
} else {
|
|
101
|
+
this.indicatorTarget.classList.remove("global-filters-active")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Called when activeValue changes
|
|
107
|
+
activeValueChanged() {
|
|
108
|
+
this.updateIndicator()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -192,6 +192,8 @@ export default class extends Controller {
|
|
|
192
192
|
updatePaginationLimit() {
|
|
193
193
|
// Update or set the limit param in the browser so if the user refreshes the page,
|
|
194
194
|
// the limit will be preserved.
|
|
195
|
+
if (!this.hasPaginationLimitTarget) return;
|
|
196
|
+
|
|
195
197
|
const url = new URL(window.location.href);
|
|
196
198
|
const currentParams = new URLSearchParams(url.search);
|
|
197
199
|
const limit = this.paginationLimitTarget.value;
|
|
@@ -244,7 +246,9 @@ export default class extends Controller {
|
|
|
244
246
|
currentParams.set('zoom_end_time', endTimestamp);
|
|
245
247
|
|
|
246
248
|
// Set the limit param based on the value in the pagination selector
|
|
247
|
-
|
|
249
|
+
if (this.hasPaginationLimitTarget) {
|
|
250
|
+
currentParams.set('limit', this.paginationLimitTarget.value);
|
|
251
|
+
}
|
|
248
252
|
|
|
249
253
|
// Update the URL's search parameters
|
|
250
254
|
url.search = currentParams.toString();
|
|
@@ -443,7 +447,9 @@ export default class extends Controller {
|
|
|
443
447
|
currentParams.set('selected_column_time', selectedTimestamp);
|
|
444
448
|
|
|
445
449
|
// Preserve pagination limit
|
|
446
|
-
|
|
450
|
+
if (this.hasPaginationLimitTarget) {
|
|
451
|
+
currentParams.set('limit', this.paginationLimitTarget.value);
|
|
452
|
+
}
|
|
447
453
|
|
|
448
454
|
url.search = currentParams.toString();
|
|
449
455
|
|
|
@@ -463,7 +469,9 @@ export default class extends Controller {
|
|
|
463
469
|
currentParams.delete('selected_column_time');
|
|
464
470
|
|
|
465
471
|
// Preserve pagination limit
|
|
466
|
-
|
|
472
|
+
if (this.hasPaginationLimitTarget) {
|
|
473
|
+
currentParams.set('limit', this.paginationLimitTarget.value);
|
|
474
|
+
}
|
|
467
475
|
|
|
468
476
|
url.search = currentParams.toString();
|
|
469
477
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Taggable
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
# Callbacks
|
|
6
|
+
before_save :ensure_tags_is_array
|
|
7
|
+
|
|
8
|
+
# Scopes with table name qualification to avoid ambiguity
|
|
9
|
+
scope :with_tag, ->(tag) { where("#{table_name}.tags LIKE ?", "%#{tag}%") }
|
|
10
|
+
scope :without_tag, ->(tag) { where.not("#{table_name}.tags LIKE ?", "%#{tag}%") }
|
|
11
|
+
scope :with_tags, -> { where("#{table_name}.tags IS NOT NULL AND #{table_name}.tags != '[]'") }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Tag management methods
|
|
15
|
+
def tag_list
|
|
16
|
+
parsed_tags || []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tag_list=(value)
|
|
20
|
+
self.tags = value.to_json
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def has_tag?(tag)
|
|
24
|
+
tag_list.include?(tag.to_s)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_tag(tag)
|
|
28
|
+
current_tags = tag_list
|
|
29
|
+
unless current_tags.include?(tag.to_s)
|
|
30
|
+
current_tags << tag.to_s
|
|
31
|
+
self.tag_list = current_tags
|
|
32
|
+
save
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remove_tag(tag)
|
|
37
|
+
current_tags = tag_list
|
|
38
|
+
if current_tags.include?(tag.to_s)
|
|
39
|
+
current_tags.delete(tag.to_s)
|
|
40
|
+
self.tag_list = current_tags
|
|
41
|
+
save
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def parsed_tags
|
|
48
|
+
return [] if tags.nil? || tags.empty?
|
|
49
|
+
JSON.parse(tags)
|
|
50
|
+
rescue JSON::ParserError
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ensure_tags_is_array
|
|
55
|
+
if tags.nil?
|
|
56
|
+
self.tags = "[]"
|
|
57
|
+
elsif tags.is_a?(Array)
|
|
58
|
+
self.tags = tags.to_json
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -32,7 +32,7 @@ module RailsPulse
|
|
|
32
32
|
{
|
|
33
33
|
query_text: truncate_query(record.normalized_sql),
|
|
34
34
|
query_id: record.query_id,
|
|
35
|
-
query_link:
|
|
35
|
+
query_link: RailsPulse::Engine.routes.url_helpers.query_path(record.query_id),
|
|
36
36
|
average_time: record.avg_duration.to_f.round(0),
|
|
37
37
|
request_count: record.request_count,
|
|
38
38
|
last_request: time_ago_in_words(record.last_seen)
|
|
@@ -33,7 +33,7 @@ module RailsPulse
|
|
|
33
33
|
{
|
|
34
34
|
route_path: record.path,
|
|
35
35
|
route_id: record.route_id,
|
|
36
|
-
route_link:
|
|
36
|
+
route_link: RailsPulse::Engine.routes.url_helpers.route_path(record.route_id),
|
|
37
37
|
average_time: record.avg_duration.to_f.round(0),
|
|
38
38
|
request_count: record.request_count,
|
|
39
39
|
last_request: time_ago_in_words(record.last_seen)
|
|
@@ -64,7 +64,7 @@ module RailsPulse
|
|
|
64
64
|
context: "queries",
|
|
65
65
|
title: "Average Query Time",
|
|
66
66
|
summary: "#{average_query_time} ms",
|
|
67
|
-
|
|
67
|
+
chart_data: sparkline_data,
|
|
68
68
|
trend_icon: trend_icon,
|
|
69
69
|
trend_amount: trend_amount,
|
|
70
70
|
trend_text: "Compared to last week"
|
|
@@ -10,10 +10,20 @@ module RailsPulse
|
|
|
10
10
|
last_7_days = 7.days.ago.beginning_of_day
|
|
11
11
|
previous_7_days = 14.days.ago.beginning_of_day
|
|
12
12
|
|
|
13
|
+
# Get the most common period type for this query, or fall back to "day"
|
|
14
|
+
period_type = if @query
|
|
15
|
+
RailsPulse::Summary.where(
|
|
16
|
+
summarizable_type: "RailsPulse::Query",
|
|
17
|
+
summarizable_id: @query.id
|
|
18
|
+
).group(:period_type).count.max_by(&:last)&.first || "day"
|
|
19
|
+
else
|
|
20
|
+
"day"
|
|
21
|
+
end
|
|
22
|
+
|
|
13
23
|
# Single query to get all count metrics with conditional aggregation
|
|
14
24
|
base_query = RailsPulse::Summary.where(
|
|
15
25
|
summarizable_type: "RailsPulse::Query",
|
|
16
|
-
period_type:
|
|
26
|
+
period_type: period_type,
|
|
17
27
|
period_start: 2.weeks.ago.beginning_of_day..Time.current
|
|
18
28
|
)
|
|
19
29
|
base_query = base_query.where(summarizable_id: @query.id) if @query
|
|
@@ -33,31 +43,60 @@ module RailsPulse
|
|
|
33
43
|
trend_icon = percentage < 0.1 ? "move-right" : current_period_count < previous_period_count ? "trending-down" : "trending-up"
|
|
34
44
|
trend_amount = previous_period_count.zero? ? "0%" : "#{percentage}%"
|
|
35
45
|
|
|
36
|
-
# Sparkline data
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
# Sparkline data with zero-filled periods over the last 14 days
|
|
47
|
+
if period_type == "day"
|
|
48
|
+
grouped_data = base_query
|
|
49
|
+
.group_by_day(:period_start, time_zone: "UTC")
|
|
50
|
+
.sum(:count)
|
|
51
|
+
|
|
52
|
+
start_period = 2.weeks.ago.beginning_of_day.to_date
|
|
53
|
+
end_period = Time.current.to_date
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
sparkline_data = {}
|
|
56
|
+
(start_period..end_period).each do |day|
|
|
57
|
+
total = grouped_data[day] || 0
|
|
58
|
+
label = day.strftime("%b %-d")
|
|
59
|
+
sparkline_data[label] = { value: total }
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
# For hourly data, group by day for sparkline display
|
|
63
|
+
grouped_data = base_query
|
|
64
|
+
.group("DATE(period_start)")
|
|
65
|
+
.sum(:count)
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
start_period = 2.weeks.ago.beginning_of_day.to_date
|
|
68
|
+
end_period = Time.current.to_date
|
|
69
|
+
|
|
70
|
+
sparkline_data = {}
|
|
71
|
+
(start_period..end_period).each do |day|
|
|
72
|
+
date_key = day.strftime("%Y-%m-%d")
|
|
73
|
+
total = grouped_data[date_key] || 0
|
|
74
|
+
label = day.strftime("%b %-d")
|
|
75
|
+
sparkline_data[label] = { value: total }
|
|
76
|
+
end
|
|
49
77
|
end
|
|
50
78
|
|
|
51
|
-
# Calculate
|
|
52
|
-
total_minutes = 2.weeks / 1.minute
|
|
53
|
-
|
|
79
|
+
# Calculate appropriate rate display based on frequency
|
|
80
|
+
total_minutes = 2.weeks / 1.minute.to_f
|
|
81
|
+
executions_per_minute = total_execution_count.to_f / total_minutes
|
|
82
|
+
|
|
83
|
+
# Choose appropriate time unit for display
|
|
84
|
+
if executions_per_minute >= 1
|
|
85
|
+
summary = "#{executions_per_minute.round(2)} / min"
|
|
86
|
+
elsif executions_per_minute * 60 >= 1
|
|
87
|
+
executions_per_hour = executions_per_minute * 60
|
|
88
|
+
summary = "#{executions_per_hour.round(2)} / hour"
|
|
89
|
+
else
|
|
90
|
+
executions_per_day = executions_per_minute * 60 * 24
|
|
91
|
+
summary = "#{executions_per_day.round(2)} / day"
|
|
92
|
+
end
|
|
54
93
|
|
|
55
94
|
{
|
|
56
95
|
id: "execution_rate",
|
|
57
96
|
context: "queries",
|
|
58
97
|
title: "Execution Rate",
|
|
59
|
-
summary:
|
|
60
|
-
|
|
98
|
+
summary: summary,
|
|
99
|
+
chart_data: sparkline_data,
|
|
61
100
|
trend_icon: trend_icon,
|
|
62
101
|
trend_amount: trend_amount,
|
|
63
102
|
trend_text: "Compared to last week"
|
|
@@ -53,7 +53,7 @@ module RailsPulse
|
|
|
53
53
|
context: "queries",
|
|
54
54
|
title: "95th Percentile Query Time",
|
|
55
55
|
summary: "#{p95_query_time} ms",
|
|
56
|
-
|
|
56
|
+
chart_data: sparkline_data,
|
|
57
57
|
trend_icon: trend_icon,
|
|
58
58
|
trend_amount: trend_amount,
|
|
59
59
|
trend_text: "Compared to last week"
|
|
@@ -12,13 +12,9 @@ module RailsPulse
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def to_rails_chart
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
period_type: @period_type
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
summaries = summaries.where(summarizable_id: @query.id) if @query
|
|
21
|
-
summaries = summaries
|
|
15
|
+
# The ransack query already contains the correct filters, just add period_type
|
|
16
|
+
summaries = @ransack_query.result(distinct: false)
|
|
17
|
+
.where(period_type: @period_type)
|
|
22
18
|
.group(:period_start)
|
|
23
19
|
.having("AVG(avg_duration) > ?", @start_duration || 0)
|
|
24
20
|
.average(:avg_duration)
|
|
@@ -2,12 +2,13 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Tables
|
|
4
4
|
class Index
|
|
5
|
-
def initialize(ransack_query:, period_type: nil, start_time:, params:, query: nil)
|
|
5
|
+
def initialize(ransack_query:, period_type: nil, start_time:, params:, query: nil, disabled_tags: [])
|
|
6
6
|
@ransack_query = ransack_query
|
|
7
7
|
@period_type = period_type
|
|
8
8
|
@start_time = start_time
|
|
9
9
|
@params = params
|
|
10
10
|
@query = query
|
|
11
|
+
@disabled_tags = disabled_tags
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_table
|
|
@@ -21,6 +22,11 @@ module RailsPulse
|
|
|
21
22
|
period_type: @period_type
|
|
22
23
|
)
|
|
23
24
|
|
|
25
|
+
# Apply tag filters by excluding queries with disabled tags
|
|
26
|
+
@disabled_tags.each do |tag|
|
|
27
|
+
base_query = base_query.where.not("rails_pulse_queries.tags LIKE ?", "%#{tag}%")
|
|
28
|
+
end
|
|
29
|
+
|
|
24
30
|
base_query = base_query.where(summarizable_id: @query.id) if @query
|
|
25
31
|
|
|
26
32
|
# Apply grouping and aggregation
|
|
@@ -29,13 +35,15 @@ module RailsPulse
|
|
|
29
35
|
"rails_pulse_summaries.summarizable_id",
|
|
30
36
|
"rails_pulse_summaries.summarizable_type",
|
|
31
37
|
"rails_pulse_queries.id",
|
|
32
|
-
"rails_pulse_queries.normalized_sql"
|
|
38
|
+
"rails_pulse_queries.normalized_sql",
|
|
39
|
+
"rails_pulse_queries.tags"
|
|
33
40
|
)
|
|
34
41
|
.select(
|
|
35
42
|
"rails_pulse_summaries.summarizable_id",
|
|
36
43
|
"rails_pulse_summaries.summarizable_type",
|
|
37
44
|
"rails_pulse_queries.id as query_id",
|
|
38
45
|
"rails_pulse_queries.normalized_sql",
|
|
46
|
+
"rails_pulse_queries.tags",
|
|
39
47
|
"AVG(rails_pulse_summaries.avg_duration) as avg_duration",
|
|
40
48
|
"MAX(rails_pulse_summaries.max_duration) as max_duration",
|
|
41
49
|
"SUM(rails_pulse_summaries.count) as execution_count",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
class Request < RailsPulse::ApplicationRecord
|
|
3
|
+
include Taggable
|
|
4
|
+
|
|
3
5
|
self.table_name = "rails_pulse_requests"
|
|
4
6
|
|
|
5
7
|
# Associations
|
|
@@ -17,7 +19,7 @@ module RailsPulse
|
|
|
17
19
|
before_create :set_request_uuid
|
|
18
20
|
|
|
19
21
|
def self.ransackable_attributes(auth_object = nil)
|
|
20
|
-
%w[id route_id occurred_at duration status status_indicator route_path]
|
|
22
|
+
%w[id route_id occurred_at duration status status_category status_indicator route_path]
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def self.ransackable_associations(auth_object = nil)
|
|
@@ -32,6 +34,12 @@ module RailsPulse
|
|
|
32
34
|
Arel.sql("rails_pulse_routes.path")
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
ransacker :status_category do |parent|
|
|
38
|
+
# Returns the first digit of the status code (2, 3, 4, or 5)
|
|
39
|
+
# Use FLOOR instead of CAST for cross-database compatibility
|
|
40
|
+
Arel.sql("FLOOR(#{parent.table[:status].name} / 100)")
|
|
41
|
+
end
|
|
42
|
+
|
|
35
43
|
ransacker :status_indicator do |parent|
|
|
36
44
|
# Calculate status indicator based on request_thresholds with safe defaults
|
|
37
45
|
config = RailsPulse.configuration rescue nil
|
|
@@ -52,7 +60,7 @@ module RailsPulse
|
|
|
52
60
|
end
|
|
53
61
|
|
|
54
62
|
def to_s
|
|
55
|
-
occurred_at.strftime("%b %d, %Y %l:%M %p")
|
|
63
|
+
occurred_at.getlocal.strftime("%b %d, %Y %l:%M %p")
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
private
|
|
@@ -13,11 +13,11 @@ module RailsPulse
|
|
|
13
13
|
|
|
14
14
|
def to_rails_chart
|
|
15
15
|
summaries = @ransack_query.result(distinct: false).where(
|
|
16
|
-
summarizable_type: "RailsPulse::
|
|
16
|
+
summarizable_type: "RailsPulse::Request",
|
|
17
|
+
summarizable_id: 0, # Overall request summaries
|
|
17
18
|
period_type: @period_type
|
|
18
19
|
)
|
|
19
20
|
|
|
20
|
-
summaries = summaries.where(summarizable_id: @route.id) if @route
|
|
21
21
|
summaries = summaries
|
|
22
22
|
.group(:period_start)
|
|
23
23
|
.having("AVG(avg_duration) > ?", @start_duration || 0)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Requests
|
|
3
|
+
module Tables
|
|
4
|
+
class Index
|
|
5
|
+
def initialize(ransack_query:, period_type: nil, start_time:, params:)
|
|
6
|
+
@ransack_query = ransack_query
|
|
7
|
+
@period_type = period_type
|
|
8
|
+
@start_time = start_time
|
|
9
|
+
@params = params
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_table
|
|
13
|
+
# Check if we have explicit ransack sorts
|
|
14
|
+
has_sorts = @ransack_query.sorts.any?
|
|
15
|
+
|
|
16
|
+
base_query = @ransack_query.result(distinct: false)
|
|
17
|
+
.where(
|
|
18
|
+
summarizable_type: "RailsPulse::Request",
|
|
19
|
+
summarizable_id: 0, # Overall request summaries
|
|
20
|
+
period_type: @period_type
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Apply grouping and aggregation for time periods
|
|
24
|
+
grouped_query = base_query
|
|
25
|
+
.group(
|
|
26
|
+
"rails_pulse_summaries.period_start",
|
|
27
|
+
"rails_pulse_summaries.period_end",
|
|
28
|
+
"rails_pulse_summaries.period_type"
|
|
29
|
+
)
|
|
30
|
+
.select(
|
|
31
|
+
"rails_pulse_summaries.period_start",
|
|
32
|
+
"rails_pulse_summaries.period_end",
|
|
33
|
+
"rails_pulse_summaries.period_type",
|
|
34
|
+
"AVG(rails_pulse_summaries.avg_duration) as avg_duration",
|
|
35
|
+
"MAX(rails_pulse_summaries.max_duration) as max_duration",
|
|
36
|
+
"MIN(rails_pulse_summaries.min_duration) as min_duration",
|
|
37
|
+
"SUM(rails_pulse_summaries.count) as count",
|
|
38
|
+
"SUM(rails_pulse_summaries.error_count) as error_count",
|
|
39
|
+
"SUM(rails_pulse_summaries.success_count) as success_count"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Apply sorting based on ransack sorts or use default
|
|
43
|
+
if has_sorts
|
|
44
|
+
# Apply custom sorting based on ransack parameters
|
|
45
|
+
sort = @ransack_query.sorts.first
|
|
46
|
+
direction = sort.dir == "desc" ? :desc : :asc
|
|
47
|
+
|
|
48
|
+
case sort.name
|
|
49
|
+
when "avg_duration"
|
|
50
|
+
grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").send(direction))
|
|
51
|
+
when "max_duration"
|
|
52
|
+
grouped_query = grouped_query.order(Arel.sql("MAX(rails_pulse_summaries.max_duration)").send(direction))
|
|
53
|
+
when "min_duration"
|
|
54
|
+
grouped_query = grouped_query.order(Arel.sql("MIN(rails_pulse_summaries.min_duration)").send(direction))
|
|
55
|
+
when "count"
|
|
56
|
+
grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count)").send(direction))
|
|
57
|
+
when "requests_per_minute"
|
|
58
|
+
grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count) / 60.0").send(direction))
|
|
59
|
+
when "error_rate_percentage"
|
|
60
|
+
grouped_query = grouped_query.order(Arel.sql("(SUM(rails_pulse_summaries.error_count) * 100.0) / SUM(rails_pulse_summaries.count)").send(direction))
|
|
61
|
+
when "period_start"
|
|
62
|
+
grouped_query = grouped_query.order(period_start: direction)
|
|
63
|
+
else
|
|
64
|
+
# Unknown sort field, fallback to default
|
|
65
|
+
grouped_query = grouped_query.order(period_start: :desc)
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
# Apply default sort when no explicit sort is provided (matches controller default_table_sort)
|
|
69
|
+
grouped_query = grouped_query.order(period_start: :desc)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
grouped_query
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -62,7 +62,7 @@ module RailsPulse
|
|
|
62
62
|
context: "routes",
|
|
63
63
|
title: "Average Response Time",
|
|
64
64
|
summary: "#{average_response_time} ms",
|
|
65
|
-
|
|
65
|
+
chart_data: sparkline_data,
|
|
66
66
|
trend_icon: trend_icon,
|
|
67
67
|
trend_amount: trend_amount,
|
|
68
68
|
trend_text: "Compared to last week"
|
|
@@ -59,7 +59,7 @@ module RailsPulse
|
|
|
59
59
|
context: "routes",
|
|
60
60
|
title: "Error Rate Per Route",
|
|
61
61
|
summary: "#{overall_error_rate}%",
|
|
62
|
-
|
|
62
|
+
chart_data: sparkline_data,
|
|
63
63
|
trend_icon: trend_icon,
|
|
64
64
|
trend_amount: trend_amount,
|
|
65
65
|
trend_text: "Compared to last week"
|
|
@@ -53,7 +53,7 @@ module RailsPulse
|
|
|
53
53
|
context: "routes",
|
|
54
54
|
title: "95th Percentile Response Time",
|
|
55
55
|
summary: "#{p95_response_time} ms",
|
|
56
|
-
|
|
56
|
+
chart_data: sparkline_data,
|
|
57
57
|
trend_icon: trend_icon,
|
|
58
58
|
trend_amount: trend_amount,
|
|
59
59
|
trend_text: "Compared to last week"
|
|
@@ -48,16 +48,27 @@ module RailsPulse
|
|
|
48
48
|
sparkline_data[label] = { value: total }
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# Calculate
|
|
52
|
-
total_minutes = 2.weeks / 1.minute
|
|
53
|
-
|
|
51
|
+
# Calculate appropriate rate display based on frequency
|
|
52
|
+
total_minutes = 2.weeks / 1.minute.to_f
|
|
53
|
+
requests_per_minute = total_request_count.to_f / total_minutes
|
|
54
|
+
|
|
55
|
+
# Choose appropriate time unit for display
|
|
56
|
+
if requests_per_minute >= 1
|
|
57
|
+
summary = "#{requests_per_minute.round(2)} / min"
|
|
58
|
+
elsif requests_per_minute * 60 >= 1
|
|
59
|
+
requests_per_hour = requests_per_minute * 60
|
|
60
|
+
summary = "#{requests_per_hour.round(2)} / hour"
|
|
61
|
+
else
|
|
62
|
+
requests_per_day = requests_per_minute * 60 * 24
|
|
63
|
+
summary = "#{requests_per_day.round(2)} / day"
|
|
64
|
+
end
|
|
54
65
|
|
|
55
66
|
{
|
|
56
67
|
id: "request_count_totals",
|
|
57
68
|
context: "routes",
|
|
58
69
|
title: "Request Count Total",
|
|
59
|
-
summary:
|
|
60
|
-
|
|
70
|
+
summary: summary,
|
|
71
|
+
chart_data: sparkline_data,
|
|
61
72
|
trend_icon: trend_icon,
|
|
62
73
|
trend_amount: trend_amount,
|
|
63
74
|
trend_text: "Compared to last week"
|