rails_pulse 0.2.4 → 0.2.5.pre.pre.3
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 +269 -12
- data/Rakefile +142 -8
- data/app/assets/stylesheets/rails_pulse/components/table.css +16 -1
- data/app/assets/stylesheets/rails_pulse/components/tags.css +7 -2
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +3 -0
- data/app/controllers/concerns/chart_table_concern.rb +2 -1
- data/app/controllers/rails_pulse/application_controller.rb +11 -1
- data/app/controllers/rails_pulse/assets_controller.rb +18 -2
- data/app/controllers/rails_pulse/job_runs_controller.rb +37 -0
- data/app/controllers/rails_pulse/jobs_controller.rb +80 -0
- data/app/controllers/rails_pulse/operations_controller.rb +43 -31
- data/app/controllers/rails_pulse/queries_controller.rb +1 -1
- data/app/controllers/rails_pulse/requests_controller.rb +3 -9
- data/app/controllers/rails_pulse/routes_controller.rb +1 -1
- data/app/controllers/rails_pulse/tags_controller.rb +31 -5
- data/app/helpers/rails_pulse/application_helper.rb +32 -1
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +15 -1
- data/app/helpers/rails_pulse/status_helper.rb +16 -0
- data/app/helpers/rails_pulse/tags_helper.rb +39 -1
- data/app/javascript/rails_pulse/controllers/chart_controller.js +112 -8
- data/app/models/concerns/rails_pulse/taggable.rb +25 -2
- data/app/models/rails_pulse/charts/operations_chart.rb +33 -0
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +1 -2
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
- data/app/models/rails_pulse/job.rb +85 -0
- data/app/models/rails_pulse/job_run.rb +76 -0
- data/app/models/rails_pulse/jobs/cards/average_duration.rb +85 -0
- data/app/models/rails_pulse/jobs/cards/base.rb +70 -0
- data/app/models/rails_pulse/jobs/cards/failure_rate.rb +85 -0
- data/app/models/rails_pulse/jobs/cards/total_jobs.rb +74 -0
- data/app/models/rails_pulse/jobs/cards/total_runs.rb +48 -0
- data/app/models/rails_pulse/operation.rb +16 -3
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +3 -3
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +1 -1
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/tables/index.rb +2 -1
- data/app/models/rails_pulse/query.rb +10 -1
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +3 -2
- 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 +1 -1
- data/app/models/rails_pulse/routes/tables/index.rb +2 -1
- data/app/models/rails_pulse/summary.rb +10 -3
- data/app/services/rails_pulse/summary_service.rb +46 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
- data/app/views/layouts/rails_pulse/application.html.erb +23 -0
- data/app/views/rails_pulse/components/_active_filters.html.erb +7 -6
- data/app/views/rails_pulse/components/_page_header.html.erb +8 -7
- data/app/views/rails_pulse/components/_table.html.erb +7 -4
- data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
- data/app/views/rails_pulse/job_runs/_operations.html.erb +78 -0
- data/app/views/rails_pulse/job_runs/index.html.erb +3 -0
- data/app/views/rails_pulse/job_runs/show.html.erb +51 -0
- data/app/views/rails_pulse/jobs/_job_runs_table.html.erb +35 -0
- data/app/views/rails_pulse/jobs/_table.html.erb +43 -0
- data/app/views/rails_pulse/jobs/index.html.erb +34 -0
- data/app/views/rails_pulse/jobs/show.html.erb +49 -0
- data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +29 -27
- data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +11 -9
- data/app/views/rails_pulse/operations/show.html.erb +10 -8
- data/app/views/rails_pulse/queries/_table.html.erb +3 -3
- data/app/views/rails_pulse/requests/_table.html.erb +6 -6
- data/app/views/rails_pulse/routes/_table.html.erb +3 -3
- data/app/views/rails_pulse/routes/show.html.erb +1 -1
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +7 -14
- data/config/brakeman.ignore +213 -0
- data/config/brakeman.yml +68 -0
- data/config/initializers/rails_pulse.rb +52 -0
- data/config/routes.rb +6 -0
- data/db/rails_pulse_migrate/20250113000000_add_jobs_to_rails_pulse.rb +95 -0
- data/db/rails_pulse_migrate/20250122000000_add_query_fingerprinting.rb +150 -0
- data/db/rails_pulse_migrate/20250202000000_add_index_to_request_uuid.rb +14 -0
- data/db/rails_pulse_schema.rb +186 -103
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +186 -103
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +30 -1
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +31 -0
- data/lib/rails_pulse/active_job_extensions.rb +13 -0
- data/lib/rails_pulse/adapters/delayed_job_plugin.rb +25 -0
- data/lib/rails_pulse/adapters/sidekiq_middleware.rb +41 -0
- data/lib/rails_pulse/cleanup_service.rb +65 -0
- data/lib/rails_pulse/configuration.rb +80 -7
- data/lib/rails_pulse/engine.rb +34 -3
- data/lib/rails_pulse/extensions/active_record.rb +82 -0
- data/lib/rails_pulse/job_run_collector.rb +172 -0
- data/lib/rails_pulse/middleware/request_collector.rb +20 -43
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +11 -5
- data/lib/rails_pulse/tracker.rb +82 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +2 -0
- data/lib/rails_pulse_server.ru +107 -0
- data/lib/tasks/rails_pulse_benchmark.rake +382 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +3 -2
- 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 +1 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +3 -3
- metadata +37 -9
- data/app/models/rails_pulse/requests/charts/operations_chart.rb +0 -35
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +0 -23
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Jobs
|
|
3
|
+
module Cards
|
|
4
|
+
class TotalJobs < Base
|
|
5
|
+
def initialize(job: nil)
|
|
6
|
+
@job = job
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def to_metric_card
|
|
10
|
+
# When scoped to a job, show runs count instead of job count
|
|
11
|
+
if @job
|
|
12
|
+
base_query = RailsPulse::Summary
|
|
13
|
+
.where(
|
|
14
|
+
summarizable_type: "RailsPulse::Job",
|
|
15
|
+
summarizable_id: @job.id,
|
|
16
|
+
period_type: "day",
|
|
17
|
+
period_start: range_start..now
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
metrics = base_query.select(
|
|
21
|
+
"SUM(count) AS total_count",
|
|
22
|
+
"SUM(CASE WHEN period_start >= #{quote(current_window_start)} THEN count ELSE 0 END) AS current_count",
|
|
23
|
+
"SUM(CASE WHEN period_start >= #{quote(range_start)} AND period_start < #{quote(current_window_start)} THEN count ELSE 0 END) AS previous_count"
|
|
24
|
+
).take
|
|
25
|
+
|
|
26
|
+
total_runs = metrics&.total_count.to_i
|
|
27
|
+
current_runs = metrics&.current_count.to_i
|
|
28
|
+
previous_runs = metrics&.previous_count.to_i
|
|
29
|
+
|
|
30
|
+
trend_icon, trend_amount = trend_for(current_runs, previous_runs)
|
|
31
|
+
|
|
32
|
+
grouped_runs = base_query
|
|
33
|
+
.group_by_date(:period_start)
|
|
34
|
+
.sum(:count)
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
id: "jobs_total_jobs",
|
|
38
|
+
context: "jobs",
|
|
39
|
+
title: "Total Runs",
|
|
40
|
+
summary: "#{format_number(total_runs)} runs",
|
|
41
|
+
chart_data: sparkline_from(grouped_runs),
|
|
42
|
+
trend_icon: trend_icon,
|
|
43
|
+
trend_amount: trend_amount,
|
|
44
|
+
trend_text: "Compared to previous week"
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
total_jobs = RailsPulse::Job.count
|
|
48
|
+
|
|
49
|
+
current_new_jobs = RailsPulse::Job.where(created_at: current_window_start..now).count
|
|
50
|
+
previous_new_jobs = RailsPulse::Job.where(created_at: range_start...current_window_start).count
|
|
51
|
+
|
|
52
|
+
trend_icon, trend_amount = trend_for(current_new_jobs, previous_new_jobs)
|
|
53
|
+
|
|
54
|
+
grouped_new_jobs = RailsPulse::Job
|
|
55
|
+
.where(created_at: range_start..now)
|
|
56
|
+
.group_by_date(:created_at)
|
|
57
|
+
.count
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
id: "jobs_total_jobs",
|
|
61
|
+
context: "jobs",
|
|
62
|
+
title: "Total Jobs",
|
|
63
|
+
summary: "#{format_number(total_jobs)} jobs",
|
|
64
|
+
chart_data: sparkline_from(grouped_new_jobs),
|
|
65
|
+
trend_icon: trend_icon,
|
|
66
|
+
trend_amount: trend_amount,
|
|
67
|
+
trend_text: "New jobs vs previous week"
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Jobs
|
|
3
|
+
module Cards
|
|
4
|
+
class TotalRuns < Base
|
|
5
|
+
def initialize(job: nil)
|
|
6
|
+
@job = job
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def to_metric_card
|
|
10
|
+
base_query = RailsPulse::Summary
|
|
11
|
+
.where(
|
|
12
|
+
summarizable_type: "RailsPulse::Job",
|
|
13
|
+
period_type: "day",
|
|
14
|
+
period_start: range_start..now
|
|
15
|
+
)
|
|
16
|
+
base_query = base_query.where(summarizable_id: @job.id) if @job
|
|
17
|
+
|
|
18
|
+
metrics = base_query.select(
|
|
19
|
+
"SUM(count) AS total_count",
|
|
20
|
+
"SUM(CASE WHEN period_start >= #{quote(current_window_start)} THEN count ELSE 0 END) AS current_count",
|
|
21
|
+
"SUM(CASE WHEN period_start >= #{quote(range_start)} AND period_start < #{quote(current_window_start)} THEN count ELSE 0 END) AS previous_count"
|
|
22
|
+
).take
|
|
23
|
+
|
|
24
|
+
total_runs = metrics&.total_count.to_i
|
|
25
|
+
current_runs = metrics&.current_count.to_i
|
|
26
|
+
previous_runs = metrics&.previous_count.to_i
|
|
27
|
+
|
|
28
|
+
trend_icon, trend_amount = trend_for(current_runs, previous_runs)
|
|
29
|
+
|
|
30
|
+
grouped_runs = base_query
|
|
31
|
+
.group_by_date(:period_start)
|
|
32
|
+
.sum(:count)
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
id: "jobs_total_runs",
|
|
36
|
+
context: "jobs",
|
|
37
|
+
title: "Job Runs",
|
|
38
|
+
summary: "#{format_number(total_runs)} runs",
|
|
39
|
+
chart_data: sparkline_from(grouped_runs),
|
|
40
|
+
trend_icon: trend_icon,
|
|
41
|
+
trend_amount: trend_amount,
|
|
42
|
+
trend_text: "Compared to previous week"
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
1
3
|
module RailsPulse
|
|
2
4
|
class Operation < RailsPulse::ApplicationRecord
|
|
3
5
|
self.table_name = "rails_pulse_operations"
|
|
@@ -18,15 +20,16 @@ module RailsPulse
|
|
|
18
20
|
].freeze
|
|
19
21
|
|
|
20
22
|
# Associations
|
|
21
|
-
belongs_to :request, class_name: "RailsPulse::Request"
|
|
23
|
+
belongs_to :request, class_name: "RailsPulse::Request", optional: true
|
|
24
|
+
belongs_to :job_run, class_name: "RailsPulse::JobRun", optional: true
|
|
22
25
|
belongs_to :query, class_name: "RailsPulse::Query", optional: true
|
|
23
26
|
|
|
24
27
|
# Validations
|
|
25
|
-
validates :request_id, presence: true
|
|
26
28
|
validates :operation_type, presence: true, inclusion: { in: OPERATION_TYPES }
|
|
27
29
|
validates :label, presence: true
|
|
28
30
|
validates :occurred_at, presence: true
|
|
29
31
|
validates :duration, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
32
|
+
validate :has_request_or_job_run
|
|
30
33
|
|
|
31
34
|
# Scopes (optional, for convenience)
|
|
32
35
|
scope :by_type, ->(type) { where(operation_type: type) }
|
|
@@ -72,11 +75,21 @@ module RailsPulse
|
|
|
72
75
|
|
|
73
76
|
private
|
|
74
77
|
|
|
78
|
+
def has_request_or_job_run
|
|
79
|
+
return if request_id.present? || job_run_id.present?
|
|
80
|
+
|
|
81
|
+
errors.add(:base, "Operation must belong to a request or a job run")
|
|
82
|
+
end
|
|
83
|
+
|
|
75
84
|
def associate_query
|
|
76
85
|
return unless operation_type == "sql" && label.present?
|
|
77
86
|
|
|
78
87
|
normalized = normalize_query_label(label)
|
|
79
|
-
|
|
88
|
+
hashed = Digest::MD5.hexdigest(normalized)
|
|
89
|
+
|
|
90
|
+
self.query = RailsPulse::Query.find_or_create_by(hashed_sql: hashed) do |q|
|
|
91
|
+
q.normalized_sql = normalized
|
|
92
|
+
end
|
|
80
93
|
end
|
|
81
94
|
|
|
82
95
|
# Normalize SQL query using the dedicated service
|
|
@@ -41,13 +41,13 @@ module RailsPulse
|
|
|
41
41
|
trend_amount = previous_period_avg.zero? ? "0%" : "#{percentage}%"
|
|
42
42
|
|
|
43
43
|
# Sparkline data by day with zero-filled days over the last 14 days
|
|
44
|
-
# Use
|
|
44
|
+
# Use database-agnostic date grouping (works regardless of ActiveRecord.default_timezone)
|
|
45
45
|
grouped_weighted = base_query
|
|
46
|
-
.
|
|
46
|
+
.group_by_date(:period_start)
|
|
47
47
|
.sum(Arel.sql("avg_duration * count"))
|
|
48
48
|
|
|
49
49
|
grouped_counts = base_query
|
|
50
|
-
.
|
|
50
|
+
.group_by_date(:period_start)
|
|
51
51
|
.sum(:count)
|
|
52
52
|
|
|
53
53
|
# Build a continuous 14-day range, fill missing days with 0
|
|
@@ -52,7 +52,7 @@ module RailsPulse
|
|
|
52
52
|
# Sparkline data with zero-filled periods over the last 14 days
|
|
53
53
|
if period_type == "day"
|
|
54
54
|
grouped_data = base_query
|
|
55
|
-
.
|
|
55
|
+
.group_by_date(:period_start)
|
|
56
56
|
.sum(:count)
|
|
57
57
|
|
|
58
58
|
start_period = 2.weeks.ago.beginning_of_day.to_date
|
|
@@ -39,7 +39,7 @@ module RailsPulse
|
|
|
39
39
|
|
|
40
40
|
# Sparkline data by day with zero-filled days over the last 14 days
|
|
41
41
|
grouped_daily = base_query
|
|
42
|
-
.
|
|
42
|
+
.group_by_date(:period_start)
|
|
43
43
|
.average(:p95_duration)
|
|
44
44
|
|
|
45
45
|
start_day = 2.weeks.ago.beginning_of_day.to_date
|
|
@@ -29,7 +29,8 @@ module RailsPulse
|
|
|
29
29
|
|
|
30
30
|
# Exclude queries with actual disabled tags
|
|
31
31
|
actual_disabled_tags.each do |tag|
|
|
32
|
-
|
|
32
|
+
sanitized_tag = ActiveRecord::Base.sanitize_sql_like(tag.to_s, "\\")
|
|
33
|
+
base_query = base_query.where.not("rails_pulse_queries.tags LIKE ?", "%#{sanitized_tag}%")
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
# Exclude non-tagged queries if show_non_tagged is false
|
|
@@ -9,7 +9,11 @@ module RailsPulse
|
|
|
9
9
|
has_many :summaries, as: :summarizable, class_name: "RailsPulse::Summary", dependent: :destroy
|
|
10
10
|
|
|
11
11
|
# Validations
|
|
12
|
-
validates :normalized_sql, presence: true
|
|
12
|
+
validates :normalized_sql, presence: true
|
|
13
|
+
validates :hashed_sql, presence: true, uniqueness: true
|
|
14
|
+
|
|
15
|
+
# Callbacks
|
|
16
|
+
before_validation :generate_hashed_sql, if: -> { normalized_sql.present? && (normalized_sql_changed? || hashed_sql.blank?) }
|
|
13
17
|
|
|
14
18
|
# JSON serialization for analysis columns
|
|
15
19
|
serialize :issues, type: Array, coder: JSON
|
|
@@ -103,5 +107,10 @@ module RailsPulse
|
|
|
103
107
|
def to_s
|
|
104
108
|
id
|
|
105
109
|
end
|
|
110
|
+
|
|
111
|
+
def generate_hashed_sql
|
|
112
|
+
require "digest"
|
|
113
|
+
self.hashed_sql = Digest::MD5.hexdigest(normalized_sql)
|
|
114
|
+
end
|
|
106
115
|
end
|
|
107
116
|
end
|
|
@@ -41,12 +41,13 @@ module RailsPulse
|
|
|
41
41
|
trend_amount = previous_period_avg.zero? ? "0%" : "#{percentage}%"
|
|
42
42
|
|
|
43
43
|
# Sparkline data by day with zero-filled days over the last 14 days
|
|
44
|
+
# Use database-agnostic date grouping (works regardless of ActiveRecord.default_timezone)
|
|
44
45
|
grouped_weighted = base_query
|
|
45
|
-
.
|
|
46
|
+
.group_by_date(:period_start)
|
|
46
47
|
.sum(Arel.sql("avg_duration * count"))
|
|
47
48
|
|
|
48
49
|
grouped_counts = base_query
|
|
49
|
-
.
|
|
50
|
+
.group_by_date(:period_start)
|
|
50
51
|
.sum(:count)
|
|
51
52
|
|
|
52
53
|
start_day = 2.weeks.ago.beginning_of_day.to_date
|
|
@@ -45,7 +45,7 @@ module RailsPulse
|
|
|
45
45
|
|
|
46
46
|
# Sparkline data by day with zero-filled days over the last 14 days
|
|
47
47
|
grouped_daily = base_query
|
|
48
|
-
.
|
|
48
|
+
.group_by_date(:period_start)
|
|
49
49
|
.sum(:error_count)
|
|
50
50
|
|
|
51
51
|
start_day = 2.weeks.ago.beginning_of_day.to_date
|
|
@@ -39,7 +39,7 @@ module RailsPulse
|
|
|
39
39
|
|
|
40
40
|
# Sparkline data by day with zero-filled days over the last 14 days
|
|
41
41
|
grouped_daily = base_query
|
|
42
|
-
.
|
|
42
|
+
.group_by_date(:period_start)
|
|
43
43
|
.average(:p95_duration)
|
|
44
44
|
|
|
45
45
|
start_day = 2.weeks.ago.beginning_of_day.to_date
|
|
@@ -39,7 +39,7 @@ module RailsPulse
|
|
|
39
39
|
|
|
40
40
|
# Sparkline data by day with zero-filled days over the last 14 days
|
|
41
41
|
grouped_daily = base_query
|
|
42
|
-
.
|
|
42
|
+
.group_by_date(:period_start)
|
|
43
43
|
.sum(:count)
|
|
44
44
|
|
|
45
45
|
start_day = 2.weeks.ago.beginning_of_day.to_date
|
|
@@ -30,7 +30,8 @@ module RailsPulse
|
|
|
30
30
|
|
|
31
31
|
# Exclude routes with actual disabled tags
|
|
32
32
|
actual_disabled_tags.each do |tag|
|
|
33
|
-
|
|
33
|
+
sanitized_tag = ActiveRecord::Base.sanitize_sql_like(tag.to_s, "\\")
|
|
34
|
+
base_query = base_query.where.not("rails_pulse_routes.tags LIKE ?", "%#{sanitized_tag}%")
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# Exclude non-tagged routes if show_non_tagged is false
|
|
@@ -12,6 +12,9 @@ module RailsPulse
|
|
|
12
12
|
foreign_key: "summarizable_id", class_name: "RailsPulse::Route", optional: true
|
|
13
13
|
belongs_to :query, -> { where(rails_pulse_summaries: { summarizable_type: "RailsPulse::Query" }) },
|
|
14
14
|
foreign_key: "summarizable_id", class_name: "RailsPulse::Query", optional: true
|
|
15
|
+
belongs_to :job,
|
|
16
|
+
-> { where(rails_pulse_summaries: { summarizable_type: "RailsPulse::Job" }) },
|
|
17
|
+
foreign_key: "summarizable_id", class_name: "RailsPulse::Job", optional: true
|
|
15
18
|
|
|
16
19
|
# Validations
|
|
17
20
|
validates :period_type, inclusion: { in: PERIOD_TYPES }
|
|
@@ -26,6 +29,8 @@ module RailsPulse
|
|
|
26
29
|
scope :for_requests, -> { where(summarizable_type: "RailsPulse::Request") }
|
|
27
30
|
scope :for_routes, -> { where(summarizable_type: "RailsPulse::Route") }
|
|
28
31
|
scope :for_queries, -> { where(summarizable_type: "RailsPulse::Query") }
|
|
32
|
+
scope :for_jobs, -> { where(summarizable_type: "RailsPulse::Job") }
|
|
33
|
+
scope :by_job_name, ->(job_name) { for_jobs.joins(:job).where(rails_pulse_jobs: { name: job_name }) }
|
|
29
34
|
scope :recent, -> { order(period_start: :desc) }
|
|
30
35
|
|
|
31
36
|
# Special scope for overall request summaries
|
|
@@ -51,7 +56,8 @@ module RailsPulse
|
|
|
51
56
|
|
|
52
57
|
# Exclude routes with disabled tags
|
|
53
58
|
actual_disabled_tags.each do |tag|
|
|
54
|
-
|
|
59
|
+
sanitized_tag = ActiveRecord::Base.sanitize_sql_like(tag.to_s, "\\")
|
|
60
|
+
route_ids = route_ids.where.not("tags LIKE ?", "%#{sanitized_tag}%")
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
# Exclude non-tagged routes if show_non_tagged is false
|
|
@@ -64,7 +70,8 @@ module RailsPulse
|
|
|
64
70
|
|
|
65
71
|
# Exclude queries with disabled tags
|
|
66
72
|
actual_disabled_tags.each do |tag|
|
|
67
|
-
|
|
73
|
+
sanitized_tag = ActiveRecord::Base.sanitize_sql_like(tag.to_s, "\\")
|
|
74
|
+
query_ids = query_ids.where.not("tags LIKE ?", "%#{sanitized_tag}%")
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
# Exclude non-tagged queries if show_non_tagged is false
|
|
@@ -99,7 +106,7 @@ module RailsPulse
|
|
|
99
106
|
end
|
|
100
107
|
|
|
101
108
|
def self.ransackable_associations(auth_object = nil)
|
|
102
|
-
%w[route query]
|
|
109
|
+
%w[route query job]
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
# Note: Basic fields like count, avg_duration, min_duration, max_duration
|
|
@@ -16,6 +16,7 @@ module RailsPulse
|
|
|
16
16
|
aggregate_requests # Overall system metrics
|
|
17
17
|
aggregate_routes # Per-route metrics
|
|
18
18
|
aggregate_queries # Per-query metrics
|
|
19
|
+
aggregate_jobs # Per-job metrics
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
Rails.logger.info "[RailsPulse] Completed #{period_type} summary"
|
|
@@ -180,6 +181,51 @@ module RailsPulse
|
|
|
180
181
|
end
|
|
181
182
|
end
|
|
182
183
|
|
|
184
|
+
def aggregate_jobs
|
|
185
|
+
job_runs = JobRun
|
|
186
|
+
.includes(:job)
|
|
187
|
+
.where(occurred_at: start_time...end_time)
|
|
188
|
+
.where(status: JobRun::FINAL_STATUSES)
|
|
189
|
+
|
|
190
|
+
return if job_runs.empty?
|
|
191
|
+
|
|
192
|
+
job_runs.group_by(&:job_id).each do |job_id, runs|
|
|
193
|
+
job = runs.first&.job
|
|
194
|
+
next unless job
|
|
195
|
+
|
|
196
|
+
duration_values = runs.map(&:duration).compact.map(&:to_f).sort
|
|
197
|
+
next if duration_values.empty?
|
|
198
|
+
|
|
199
|
+
duration_count = duration_values.size
|
|
200
|
+
total_duration = duration_values.sum
|
|
201
|
+
average_duration = total_duration / duration_count
|
|
202
|
+
|
|
203
|
+
summary = Summary.find_or_initialize_by(
|
|
204
|
+
summarizable_type: "RailsPulse::Job",
|
|
205
|
+
summarizable_id: job.id,
|
|
206
|
+
period_type: period_type,
|
|
207
|
+
period_start: start_time
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
summary.assign_attributes(
|
|
211
|
+
period_end: end_time,
|
|
212
|
+
count: runs.size,
|
|
213
|
+
avg_duration: average_duration,
|
|
214
|
+
min_duration: duration_values.first,
|
|
215
|
+
max_duration: duration_values.last,
|
|
216
|
+
total_duration: total_duration,
|
|
217
|
+
p50_duration: calculate_percentile(duration_values, 0.5),
|
|
218
|
+
p95_duration: calculate_percentile(duration_values, 0.95),
|
|
219
|
+
p99_duration: calculate_percentile(duration_values, 0.99),
|
|
220
|
+
stddev_duration: calculate_stddev(duration_values, average_duration),
|
|
221
|
+
error_count: runs.count(&:failure_like_status?),
|
|
222
|
+
success_count: runs.count { |run| run.status == "success" }
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
summary.save!
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
183
229
|
def calculate_percentile(sorted_array, percentile)
|
|
184
230
|
return nil if sorted_array.empty?
|
|
185
231
|
|
|
@@ -17,3 +17,10 @@
|
|
|
17
17
|
<%= rails_pulse_icon 'audio-lines', width: '16' %>
|
|
18
18
|
<span class="overflow-ellipsis">Requests</span>
|
|
19
19
|
<% end %>
|
|
20
|
+
|
|
21
|
+
<% if RailsPulse.configuration.track_jobs %>
|
|
22
|
+
<%= link_to jobs_path, class: 'btn sidebar-menu__button' do %>
|
|
23
|
+
<%= rails_pulse_icon 'zap', width: '16' %>
|
|
24
|
+
<span class="overflow-ellipsis">Jobs</span>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% end %>
|
|
@@ -10,6 +10,29 @@
|
|
|
10
10
|
|
|
11
11
|
<%= yield :head %>
|
|
12
12
|
|
|
13
|
+
<script nonce="<%= rails_pulse_csp_nonce %>">
|
|
14
|
+
(function() {
|
|
15
|
+
var storageKey = 'color-scheme';
|
|
16
|
+
var root = document.documentElement;
|
|
17
|
+
var scheme = 'light';
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
var stored = localStorage.getItem(storageKey);
|
|
21
|
+
if (stored === 'dark' || stored === 'light') {
|
|
22
|
+
scheme = stored;
|
|
23
|
+
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
24
|
+
scheme = 'dark';
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
28
|
+
scheme = 'dark';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
root.setAttribute('data-color-scheme', scheme);
|
|
33
|
+
})();
|
|
34
|
+
</script>
|
|
35
|
+
|
|
13
36
|
<!-- Rails Pulse Pre-compiled Assets (CSP-safe) -->
|
|
14
37
|
<%= stylesheet_link_tag rails_pulse.asset_path('rails-pulse.css'), 'data-turbo-track': 'reload' %>
|
|
15
38
|
<%= javascript_include_tag rails_pulse.asset_path('rails-pulse-icons.js'), 'data-turbo-track': 'reload', defer: true, nonce: rails_pulse_csp_nonce %>
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
<% has_any_filters = has_date_filters || has_performance_filter || has_tag_filters %>
|
|
7
7
|
|
|
8
8
|
<% if has_any_filters %>
|
|
9
|
-
<div class="flex items-center gap text-sm">
|
|
10
|
-
|
|
9
|
+
<div class="flex items-center gap text-sm card pi-3 pb-2 shadow-xs">
|
|
10
|
+
Global Filters:
|
|
11
11
|
<% if has_date_filters %>
|
|
12
12
|
<% start_time = Time.parse(global_filters['start_time']) %>
|
|
13
13
|
<% end_time = Time.parse(global_filters['end_time']) %>
|
|
14
|
-
|
|
14
|
+
<% date_range = "#{start_time.strftime("%b %d, %Y %-I:%M %p")} - #{end_time.strftime("%b %d, %Y %-I:%M %p")}" %>
|
|
15
|
+
<%= render_tag_badge(date_range, variant: :secondary) %>
|
|
15
16
|
<% end %>
|
|
16
17
|
|
|
17
18
|
<% if has_performance_filter %>
|
|
@@ -21,15 +22,15 @@
|
|
|
21
22
|
when 'critical' then 'Critical'
|
|
22
23
|
else global_filters['performance_threshold'].titleize
|
|
23
24
|
end %>
|
|
24
|
-
|
|
25
|
+
<%= render_tag_badge(threshold_label, variant: :secondary) %>
|
|
25
26
|
<% end %>
|
|
26
27
|
|
|
27
28
|
<% if has_tag_filters %>
|
|
28
29
|
<% disabled_tags.each do |tag| %>
|
|
29
|
-
|
|
30
|
+
<%= render_tag_badge(tag.humanize, variant: :secondary) %>
|
|
30
31
|
<% end %>
|
|
31
32
|
<% if session[:show_non_tagged] == false %>
|
|
32
|
-
|
|
33
|
+
<%= render_tag_badge("Non tagged hidden", variant: :secondary) %>
|
|
33
34
|
<% end %>
|
|
34
35
|
<% end %>
|
|
35
36
|
</div>
|
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
<% end %>
|
|
13
13
|
</nav>
|
|
14
14
|
|
|
15
|
-
<% if defined?(taggable) && taggable.present? %>
|
|
16
|
-
<div class="breadcrumb-tags">
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
<% if (defined?(taggable) && taggable.present?) || (defined?(show_active_filters) && show_active_filters) %>
|
|
16
|
+
<div class="breadcrumb-tags gap">
|
|
17
|
+
<% if defined?(show_active_filters) && show_active_filters %>
|
|
18
|
+
<%= render 'rails_pulse/components/active_filters' %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% if defined?(taggable) && taggable.present? %>
|
|
21
|
+
<%= render 'rails_pulse/tags/tag_manager', taggable: taggable %>
|
|
22
|
+
<% end %>
|
|
22
23
|
</div>
|
|
23
24
|
<% end %>
|
|
24
25
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%
|
|
2
2
|
# Extract the table data from the arguments
|
|
3
3
|
table_data = defined?(table_data) ? table_data : (defined?(data) ? data : {})
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
# Handle both old format (array) and new format (hash with columns and data)
|
|
6
6
|
if table_data.is_a?(Array)
|
|
7
7
|
# Legacy format - assume route data
|
|
@@ -18,11 +18,14 @@
|
|
|
18
18
|
columns = table_data[:columns] || []
|
|
19
19
|
data_rows = table_data[:data] || []
|
|
20
20
|
end
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
colspan = columns.length
|
|
23
|
+
has_truncation = columns.any? { |col| col[:cell_class]&.include?('truncate-cell') }
|
|
24
|
+
table_classes = ['table', 'mbs-4']
|
|
25
|
+
table_classes << 'table-fixed' if has_truncation
|
|
23
26
|
%>
|
|
24
27
|
|
|
25
|
-
<table class="
|
|
28
|
+
<table class="<%= table_classes.join(' ') %>">
|
|
26
29
|
<thead>
|
|
27
30
|
<tr>
|
|
28
31
|
<% columns.each do |column| %>
|
|
@@ -47,4 +50,4 @@
|
|
|
47
50
|
</tr>
|
|
48
51
|
<% end %>
|
|
49
52
|
</tbody>
|
|
50
|
-
</table>
|
|
53
|
+
</table>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<%= render 'rails_pulse/components/panel', { title: 'Event Sequence ' } do %>
|
|
2
|
+
<% if operations.any? %>
|
|
3
|
+
<table class="table operations-table">
|
|
4
|
+
<thead>
|
|
5
|
+
<tr>
|
|
6
|
+
<th class="w-8"></th>
|
|
7
|
+
<th class="operations-label-cell">Operation</th>
|
|
8
|
+
<th class="operations-duration-cell">Duration</th>
|
|
9
|
+
<th class="w-20">Impact</th>
|
|
10
|
+
<th class="w-16"></th>
|
|
11
|
+
<th class="operations-event-cell">Timeline</th>
|
|
12
|
+
<th class="w-16">Actions</th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody data-controller="rails-pulse--expandable-rows" data-action="click->rails-pulse--expandable-rows#toggle">
|
|
16
|
+
<% total_run_duration = @run.duration.to_f %>
|
|
17
|
+
<% @operation_timeline.bars.each_with_index do |bar, index| %>
|
|
18
|
+
<tr data-operation-id="<%= bar.operation.id %>">
|
|
19
|
+
<!-- Toggle chevron column -->
|
|
20
|
+
<td class="text-center cursor-pointer">
|
|
21
|
+
<div class="chevron mbs-2">
|
|
22
|
+
<%= rails_pulse_icon('chevron-right', width: '16', class: 'transition-transform duration-200') %>
|
|
23
|
+
</div>
|
|
24
|
+
</td>
|
|
25
|
+
|
|
26
|
+
<td class="operations-label-cell">
|
|
27
|
+
<span class="text-link" style="color: <%= event_color(bar.operation.operation_type) %>;">
|
|
28
|
+
<%= html_escape(bar.operation.label) %>
|
|
29
|
+
</span>
|
|
30
|
+
</td>
|
|
31
|
+
|
|
32
|
+
<td class="whitespace-nowrap">
|
|
33
|
+
<%= bar.duration.round(2) %> ms
|
|
34
|
+
</td>
|
|
35
|
+
|
|
36
|
+
<td class="whitespace-nowrap">
|
|
37
|
+
<% impact_percentage = total_run_duration > 0 ? (bar.operation.duration / total_run_duration * 100).round(1) : 0 %>
|
|
38
|
+
<%= impact_percentage %>%
|
|
39
|
+
</td>
|
|
40
|
+
|
|
41
|
+
<td class="whitespace-nowrap text-center">
|
|
42
|
+
<%= operation_status_indicator(bar.operation) %>
|
|
43
|
+
</td>
|
|
44
|
+
|
|
45
|
+
<td class="operations-event-cell">
|
|
46
|
+
<div
|
|
47
|
+
class="operations-event"
|
|
48
|
+
style="left: <%= bar.left_pct %>%; width: <%= bar.width_pct %>%; background-color: <%= event_color(bar.operation.operation_type) %>;">
|
|
49
|
+
</div>
|
|
50
|
+
</td>
|
|
51
|
+
|
|
52
|
+
<td>
|
|
53
|
+
<%= link_to rails_pulse_icon('eye', width: '16', class: 'inline-block mbi-2'), operation_path(bar.operation), title: 'View details', data: { turbo_frame: "_top" } %>
|
|
54
|
+
<% if bar.operation.operation_type == "sql" && bar.operation.query.present? %>
|
|
55
|
+
<%= link_to rails_pulse_icon('database', width: '16', class: 'inline-block'), query_path(bar.operation.query), title: 'View query', data: { turbo_frame: "_top" } %>
|
|
56
|
+
<% end %>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
|
|
60
|
+
<!-- Expandable details row -->
|
|
61
|
+
<tr
|
|
62
|
+
class="operation-details-row hidden"
|
|
63
|
+
data-operation-id="<%= bar.operation.id %>"
|
|
64
|
+
>
|
|
65
|
+
<td colspan="7" class="pi-8 pb-4">
|
|
66
|
+
<%= turbo_frame_tag "operation_#{bar.operation.id}_details", data: { "operation-url": operation_path(bar.operation) } do %>
|
|
67
|
+
<% end %>
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
<% end %>
|
|
71
|
+
</tbody>
|
|
72
|
+
</table>
|
|
73
|
+
<% else %>
|
|
74
|
+
<%= render 'rails_pulse/components/empty_state',
|
|
75
|
+
title: 'No operations found for this job run.',
|
|
76
|
+
description: 'This job run may not have had any tracked operations.' %>
|
|
77
|
+
<% end %>
|
|
78
|
+
<% end %>
|