rails_pulse 0.1.0 → 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 +74 -178
- data/Rakefile +75 -173
- 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 +20 -9
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +19 -7
- 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 +6 -12
- 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 +28 -12
- 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
@@ -1,11 +1,13 @@
|
|
1
1
|
<%= render 'rails_pulse/components/breadcrumbs' %>
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
<% unless turbo_frame_request? %>
|
4
|
+
<div class="row">
|
5
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_response_times_metric_card } %>
|
6
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_response_times_metric_card } %>
|
7
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @request_count_totals_metric_card } %>
|
8
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @error_rate_per_route_metric_card } %>
|
9
|
+
</div>
|
10
|
+
<% end %>
|
9
11
|
|
10
12
|
<div
|
11
13
|
class="row"
|
@@ -16,12 +18,12 @@
|
|
16
18
|
<%= render 'rails_pulse/components/panel', { title: 'Average Response Time', } do %>
|
17
19
|
<%= search_form_for @ransack_query, url: requests_path, class: "flex items-center justify-between gap mb-4" do |form| %>
|
18
20
|
<div class="flex items-center grow gap">
|
19
|
-
<%= form.select :
|
21
|
+
<%= form.select :period_start_range,
|
20
22
|
RailsPulse::RequestsController::TIME_RANGE_OPTIONS,
|
21
23
|
{ selected: @selected_time_range },
|
22
24
|
{ class: "input" }
|
23
25
|
%>
|
24
|
-
<%= form.select :
|
26
|
+
<%= form.select :avg_duration,
|
25
27
|
duration_options(:request),
|
26
28
|
{ selected: @selected_response_range },
|
27
29
|
{ class: "input" }
|
@@ -31,33 +33,39 @@
|
|
31
33
|
</div>
|
32
34
|
<% end %>
|
33
35
|
|
34
|
-
<% if @
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
36
|
+
<% if @has_data %>
|
37
|
+
<% if @chart_data && @chart_data.values.any? { |v| v > 0 } %>
|
38
|
+
<div
|
39
|
+
class="chart-container chart-container--slim"
|
40
|
+
data-rails-pulse--index-target="chart"
|
41
|
+
>
|
42
|
+
<%= bar_chart(
|
43
|
+
@chart_data,
|
44
|
+
code: false,
|
45
|
+
id: "average_response_times_chart",
|
46
|
+
height: "100%",
|
47
|
+
options: bar_chart_options(
|
48
|
+
units: "ms",
|
49
|
+
zoom: true,
|
50
|
+
chart_start: 0,
|
51
|
+
chart_end: @chart_data.length - 1,
|
52
|
+
xaxis_formatter: @xaxis_formatter,
|
53
|
+
tooltip_formatter: @tooltip_formatter,
|
54
|
+
zoom_start: @zoom_start,
|
55
|
+
zoom_end: @zoom_end,
|
56
|
+
chart_data: @chart_data
|
57
|
+
)
|
58
|
+
) %>
|
59
|
+
</div>
|
60
|
+
<% end %>
|
58
61
|
|
59
|
-
|
60
|
-
|
62
|
+
<%= turbo_frame_tag :requests_index_table, data: { rails_pulse__index_target: "indexTable" } do %>
|
63
|
+
<%= render 'rails_pulse/requests/table' %>
|
64
|
+
<% end %>
|
65
|
+
<% else %>
|
66
|
+
<%= render 'rails_pulse/components/empty_state',
|
67
|
+
title: 'No request data found for the selected filters.',
|
68
|
+
description: 'Try adjusting your time range or filters to see results.' %>
|
61
69
|
<% end %>
|
62
70
|
<% end %>
|
63
71
|
</div>
|
@@ -1,26 +1,26 @@
|
|
1
1
|
<% columns = [
|
2
|
-
{ field: :
|
3
|
-
{ field: :
|
4
|
-
{ field: :
|
5
|
-
{ field: :
|
2
|
+
{ field: :route_path, label: 'Route', class: 'w-auto' },
|
3
|
+
{ field: :avg_duration_sort, label: 'Average Response Time', class: 'w-36' },
|
4
|
+
{ field: :max_duration_sort, label: 'Max Response Time', class: 'w-32' },
|
5
|
+
{ field: :count_sort, label: 'Requests', class: 'w-24' },
|
6
6
|
{ field: :requests_per_minute, label: 'Requests Per Minute', class: 'w-28' },
|
7
7
|
{ field: :error_rate_percentage, label: 'Error Rate (%)', class: 'w-20' },
|
8
|
-
{ field: :status_indicator, label: 'Status', class: 'w-16' }
|
8
|
+
{ field: :status_indicator, label: 'Status', class: 'w-16', sortable: false }
|
9
9
|
] %>
|
10
10
|
|
11
11
|
<table class="table mbs-4">
|
12
12
|
<%= render "rails_pulse/components/table_head", columns: columns %>
|
13
13
|
|
14
14
|
<tbody>
|
15
|
-
<% @table_data.each do |
|
15
|
+
<% @table_data.each do |summary| %>
|
16
16
|
<tr>
|
17
|
-
<td class="whitespace-nowrap"><%= link_to
|
18
|
-
<td class="whitespace-nowrap"><%=
|
19
|
-
<td class="whitespace-nowrap"><%=
|
20
|
-
<td class="whitespace-nowrap"><%= number_with_delimiter
|
21
|
-
<td class="whitespace-nowrap"><%=
|
22
|
-
<td class="whitespace-nowrap"><%=
|
23
|
-
<td class="whitespace-nowrap text-center"><%= route_status_indicator(
|
17
|
+
<td class="whitespace-nowrap"><%= link_to "#{summary.path} #{summary.route_method}", route_path(summary.route_id), data: { turbo_frame: '_top' } %></td>
|
18
|
+
<td class="whitespace-nowrap"><%= summary.avg_duration.to_i %> ms</td>
|
19
|
+
<td class="whitespace-nowrap"><%= summary.max_duration.to_i %> ms</td>
|
20
|
+
<td class="whitespace-nowrap"><%= number_with_delimiter summary.count %></td>
|
21
|
+
<td class="whitespace-nowrap"><%= summary.count < 1 ? '< 1' : (summary.count / 60.0).round(2) %></td>
|
22
|
+
<td class="whitespace-nowrap"><%= ((summary.error_count.to_f / summary.count) * 100).round(2) %>%</td>
|
23
|
+
<td class="whitespace-nowrap text-center"><%= route_status_indicator(summary.avg_duration >= 500 ? 1 : 0) %></td>
|
24
24
|
</tr>
|
25
25
|
<% end %>
|
26
26
|
</tbody>
|
@@ -1,11 +1,13 @@
|
|
1
1
|
<%= render 'rails_pulse/components/breadcrumbs' %>
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
<% unless turbo_frame_request? %>
|
4
|
+
<div class="row">
|
5
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
|
6
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_response_times_metric_card } %>
|
7
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @request_count_totals_metric_card } %>
|
8
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @error_rate_per_route_metric_card } %>
|
9
|
+
</div>
|
10
|
+
<% end %>
|
9
11
|
|
10
12
|
<div
|
11
13
|
class="row"
|
@@ -16,13 +18,13 @@
|
|
16
18
|
<%= render 'rails_pulse/components/panel', { title: 'Average Response Time', } do %>
|
17
19
|
<%= search_form_for @ransack_query, url: routes_path, class: "flex items-center justify-between gap mb-4" do |form| %>
|
18
20
|
<div class="flex items-center grow gap">
|
19
|
-
<%= form.search_field :
|
20
|
-
<%= form.select :
|
21
|
+
<%= form.search_field :route_path_cont, placeholder: "Filter by route", autocomplete: "off", class: "input", style: "max-inline-size: 250px" %>
|
22
|
+
<%= form.select :period_start_range,
|
21
23
|
RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
|
22
24
|
{ selected: @selected_time_range },
|
23
25
|
{ class: "input" }
|
24
26
|
%>
|
25
|
-
<%= form.select :
|
27
|
+
<%= form.select :avg_duration,
|
26
28
|
duration_options(:route),
|
27
29
|
{ selected: @selected_response_range },
|
28
30
|
{ class: "input" }
|
@@ -32,33 +34,39 @@
|
|
32
34
|
</div>
|
33
35
|
<% end %>
|
34
36
|
|
35
|
-
<% if @
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
37
|
+
<% if @has_data %>
|
38
|
+
<% if @chart_data && @chart_data.values.any? { |v| v > 0 } %>
|
39
|
+
<div
|
40
|
+
class="chart-container chart-container--slim"
|
41
|
+
data-rails-pulse--index-target="chart"
|
42
|
+
>
|
43
|
+
<%= bar_chart(
|
44
|
+
@chart_data,
|
45
|
+
code: false,
|
46
|
+
id: "average_response_times_chart",
|
47
|
+
height: "100%",
|
48
|
+
options: bar_chart_options(
|
49
|
+
units: "ms",
|
50
|
+
zoom: true,
|
51
|
+
chart_start: 0,
|
52
|
+
chart_end: @chart_data.length - 1,
|
53
|
+
xaxis_formatter: @xaxis_formatter,
|
54
|
+
tooltip_formatter: @tooltip_formatter,
|
55
|
+
zoom_start: @zoom_start,
|
56
|
+
zoom_end: @zoom_end,
|
57
|
+
chart_data: @chart_data
|
58
|
+
)
|
59
|
+
) %>
|
60
|
+
</div>
|
61
|
+
<% end %>
|
59
62
|
|
60
|
-
|
61
|
-
|
63
|
+
<%= turbo_frame_tag :routes_index_table, data: { rails_pulse__index_target: "indexTable" } do %>
|
64
|
+
<%= render 'rails_pulse/routes/table' %>
|
65
|
+
<% end %>
|
66
|
+
<% else %>
|
67
|
+
<%= render 'rails_pulse/components/empty_state',
|
68
|
+
title: 'No route data found for the selected filters.',
|
69
|
+
description: 'Try adjusting your time range or filters to see results.' %>
|
62
70
|
<% end %>
|
63
71
|
<% end %>
|
64
72
|
</div>
|
@@ -2,29 +2,30 @@
|
|
2
2
|
|
3
3
|
<h1 class="text-2xl mbe-2"><%= @route.path_and_method %></h1>
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
<% unless turbo_frame_request? %>
|
6
|
+
<div class="row">
|
7
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
|
8
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_response_times_metric_card } %>
|
9
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @request_count_totals_metric_card } %>
|
10
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @error_rate_per_route_metric_card } %>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
11
13
|
|
12
14
|
<div
|
13
15
|
class="row"
|
14
16
|
data-controller="rails-pulse--index"
|
15
17
|
data-rails-pulse--index-chart-id-value="route_repsonses_chart"
|
16
|
-
data-rails-pulse--index-occurred-at-param-value="occurred_at"
|
17
18
|
>
|
18
19
|
<div class="grid-item">
|
19
20
|
<%= render 'rails_pulse/components/panel', { title: 'Route Reqeusts', } do %>
|
20
21
|
<%= search_form_for @ransack_query, url: route_path(@route), class: "flex items-center justify-between gap mb-4" do |form| %>
|
21
22
|
<div class="flex items-center grow gap">
|
22
|
-
<%= form.select :
|
23
|
+
<%= form.select :period_start_range,
|
23
24
|
RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
|
24
25
|
{ selected: @selected_time_range },
|
25
26
|
{ class: "input" }
|
26
27
|
%>
|
27
|
-
<%= form.select :
|
28
|
+
<%= form.select :avg_duration,
|
28
29
|
duration_options(:route),
|
29
30
|
{ selected: @selected_response_range },
|
30
31
|
{ class: "input" }
|
@@ -34,33 +35,39 @@
|
|
34
35
|
</div>
|
35
36
|
<% end %>
|
36
37
|
|
37
|
-
<% if @
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
38
|
+
<% if @has_data %>
|
39
|
+
<% if @chart_data && @chart_data.values.any? { |v| v > 0 } %>
|
40
|
+
<div
|
41
|
+
class="chart-container chart-container--slim"
|
42
|
+
data-rails-pulse--index-target="chart"
|
43
|
+
>
|
44
|
+
<%= bar_chart(
|
45
|
+
@chart_data,
|
46
|
+
code: false,
|
47
|
+
id: "route_repsonses_chart",
|
48
|
+
height: "100%",
|
49
|
+
options: bar_chart_options(
|
50
|
+
units: "ms",
|
51
|
+
zoom: true,
|
52
|
+
chart_start: 0,
|
53
|
+
chart_end: @chart_data.length - 1,
|
54
|
+
xaxis_formatter: @xaxis_formatter,
|
55
|
+
tooltip_formatter: @tooltip_formatter,
|
56
|
+
zoom_start: @zoom_start,
|
57
|
+
zoom_end: @zoom_end,
|
58
|
+
chart_data: @chart_data
|
59
|
+
)
|
60
|
+
) %>
|
61
|
+
</div>
|
62
|
+
<% end %>
|
61
63
|
|
62
|
-
|
63
|
-
|
64
|
+
<%= turbo_frame_tag :index_table do %>
|
65
|
+
<%= render 'rails_pulse/requests/table' %>
|
66
|
+
<% end %>
|
67
|
+
<% else %>
|
68
|
+
<%= render 'rails_pulse/components/empty_state',
|
69
|
+
title: 'No route requests found for the selected filters.',
|
70
|
+
description: 'Try adjusting your time range or filters to see results.' %>
|
64
71
|
<% end %>
|
65
72
|
<% end %>
|
66
73
|
</div>
|
@@ -71,18 +71,6 @@ RailsPulse.configure do |config|
|
|
71
71
|
config.ignored_requests = []
|
72
72
|
config.ignored_queries = []
|
73
73
|
|
74
|
-
# ====================================================================================================
|
75
|
-
# CACHING
|
76
|
-
# ====================================================================================================
|
77
|
-
# Configure metric card caching to improve performance of the Rails Pulse dashboard.
|
78
|
-
# Caching reduces database load for expensive metric calculations.
|
79
|
-
|
80
|
-
# Enable/disable metric card caching
|
81
|
-
config.component_cache_enabled = true
|
82
|
-
|
83
|
-
# How long to cache metric card results
|
84
|
-
config.component_cache_duration = 1.hour
|
85
|
-
|
86
74
|
# ====================================================================================================
|
87
75
|
# DATABASE CONFIGURATION
|
88
76
|
# ====================================================================================================
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class CreateRailsPulseSummaries < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
create_table :rails_pulse_summaries do |t|
|
4
|
+
# Time fields
|
5
|
+
t.datetime :period_start, null: false
|
6
|
+
t.datetime :period_end, null: false
|
7
|
+
t.string :period_type, null: false # 'hour', 'day', 'week', 'month'
|
8
|
+
|
9
|
+
# Polymorphic association to handle both routes and queries
|
10
|
+
t.references :summarizable, polymorphic: true, null: false, index: true
|
11
|
+
# This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
|
12
|
+
# and summarizable_id (route_id or query_id)
|
13
|
+
|
14
|
+
# Universal metrics
|
15
|
+
t.integer :count, default: 0, null: false
|
16
|
+
t.float :avg_duration
|
17
|
+
t.float :min_duration
|
18
|
+
t.float :max_duration
|
19
|
+
t.float :p50_duration
|
20
|
+
t.float :p95_duration
|
21
|
+
t.float :p99_duration
|
22
|
+
t.float :total_duration
|
23
|
+
t.float :stddev_duration
|
24
|
+
|
25
|
+
# Request/Route specific metrics
|
26
|
+
t.integer :error_count, default: 0
|
27
|
+
t.integer :success_count, default: 0
|
28
|
+
t.integer :status_2xx, default: 0
|
29
|
+
t.integer :status_3xx, default: 0
|
30
|
+
t.integer :status_4xx, default: 0
|
31
|
+
t.integer :status_5xx, default: 0
|
32
|
+
|
33
|
+
t.timestamps
|
34
|
+
|
35
|
+
# Unique constraint and indexes
|
36
|
+
t.index [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
|
37
|
+
unique: true,
|
38
|
+
name: 'idx_pulse_summaries_unique'
|
39
|
+
t.index [ :period_type, :period_start ]
|
40
|
+
t.index :created_at
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add indexes to existing tables for efficient aggregation
|
44
|
+
add_index :rails_pulse_requests, [ :created_at, :route_id ],
|
45
|
+
name: 'idx_requests_for_aggregation'
|
46
|
+
add_index :rails_pulse_requests, :created_at,
|
47
|
+
name: 'idx_requests_created_at'
|
48
|
+
|
49
|
+
add_index :rails_pulse_operations, [ :created_at, :query_id ],
|
50
|
+
name: 'idx_operations_for_aggregation'
|
51
|
+
add_index :rails_pulse_operations, :created_at,
|
52
|
+
name: 'idx_operations_created_at'
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# Rails Pulse Database Schema
|
2
|
+
# This file contains the complete schema for Rails Pulse tables
|
3
|
+
# Load with: rails db:schema:load:rails_pulse or db:prepare
|
4
|
+
|
5
|
+
RailsPulse::Schema = lambda do |connection|
|
6
|
+
# Skip if all tables already exist to prevent conflicts
|
7
|
+
required_tables = [:rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_summaries]
|
8
|
+
|
9
|
+
if ENV["CI"] == "true"
|
10
|
+
existing_tables = required_tables.select { |table| connection.table_exists?(table) }
|
11
|
+
missing_tables = required_tables - existing_tables
|
12
|
+
puts "[RailsPulse::Schema] Existing tables: #{existing_tables.join(', ')}" if existing_tables.any?
|
13
|
+
puts "[RailsPulse::Schema] Missing tables: #{missing_tables.join(', ')}" if missing_tables.any?
|
14
|
+
end
|
15
|
+
|
16
|
+
return if required_tables.all? { |table| connection.table_exists?(table) }
|
17
|
+
|
18
|
+
connection.create_table :rails_pulse_routes do |t|
|
19
|
+
t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
|
20
|
+
t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
|
24
|
+
connection.add_index :rails_pulse_routes, [ :method, :path ], unique: true, name: "index_rails_pulse_routes_on_method_and_path"
|
25
|
+
|
26
|
+
connection.create_table :rails_pulse_queries do |t|
|
27
|
+
t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
|
28
|
+
t.timestamps
|
29
|
+
end
|
30
|
+
|
31
|
+
connection.add_index :rails_pulse_queries, :normalized_sql, unique: true, name: "index_rails_pulse_queries_on_normalized_sql", length: 191
|
32
|
+
|
33
|
+
connection.create_table :rails_pulse_requests do |t|
|
34
|
+
t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
|
35
|
+
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
|
36
|
+
t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
|
37
|
+
t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
|
38
|
+
t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
|
39
|
+
t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
|
40
|
+
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
41
|
+
t.timestamps
|
42
|
+
end
|
43
|
+
|
44
|
+
connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
|
45
|
+
connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
|
46
|
+
connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
|
47
|
+
|
48
|
+
connection.create_table :rails_pulse_operations do |t|
|
49
|
+
t.references :request, null: false, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
|
50
|
+
t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
|
51
|
+
t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
|
52
|
+
t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
|
53
|
+
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
|
54
|
+
t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
|
55
|
+
t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
|
56
|
+
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
57
|
+
t.timestamps
|
58
|
+
end
|
59
|
+
|
60
|
+
connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
|
61
|
+
connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
|
62
|
+
connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
|
63
|
+
connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
|
64
|
+
connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
|
65
|
+
|
66
|
+
connection.create_table :rails_pulse_summaries do |t|
|
67
|
+
# Time fields
|
68
|
+
t.datetime :period_start, null: false, comment: "Start of the aggregation period"
|
69
|
+
t.datetime :period_end, null: false, comment: "End of the aggregation period"
|
70
|
+
t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
|
71
|
+
|
72
|
+
# Polymorphic association to handle both routes and queries
|
73
|
+
t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
|
74
|
+
# This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
|
75
|
+
# and summarizable_id (route_id or query_id)
|
76
|
+
|
77
|
+
# Universal metrics
|
78
|
+
t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
|
79
|
+
t.float :avg_duration, comment: "Average duration in milliseconds"
|
80
|
+
t.float :min_duration, comment: "Minimum duration in milliseconds"
|
81
|
+
t.float :max_duration, comment: "Maximum duration in milliseconds"
|
82
|
+
t.float :p50_duration, comment: "50th percentile duration"
|
83
|
+
t.float :p95_duration, comment: "95th percentile duration"
|
84
|
+
t.float :p99_duration, comment: "99th percentile duration"
|
85
|
+
t.float :total_duration, comment: "Total duration in milliseconds"
|
86
|
+
t.float :stddev_duration, comment: "Standard deviation of duration"
|
87
|
+
|
88
|
+
# Request/Route specific metrics
|
89
|
+
t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
|
90
|
+
t.integer :success_count, default: 0, comment: "Number of successful responses"
|
91
|
+
t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
|
92
|
+
t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
|
93
|
+
t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
|
94
|
+
t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
|
95
|
+
|
96
|
+
t.timestamps
|
97
|
+
end
|
98
|
+
|
99
|
+
# Unique constraint and indexes for summaries
|
100
|
+
connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
|
101
|
+
unique: true,
|
102
|
+
name: "idx_pulse_summaries_unique"
|
103
|
+
connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
|
104
|
+
connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
|
105
|
+
|
106
|
+
# Add indexes to existing tables for efficient aggregation
|
107
|
+
connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
|
108
|
+
connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
|
109
|
+
|
110
|
+
connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
|
111
|
+
connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
|
112
|
+
|
113
|
+
if ENV["CI"] == "true"
|
114
|
+
created_tables = required_tables.select { |table| connection.table_exists?(table) }
|
115
|
+
puts "[RailsPulse::Schema] Successfully created tables: #{created_tables.join(', ')}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if defined?(RailsPulse::ApplicationRecord)
|
120
|
+
RailsPulse::Schema.call(RailsPulse::ApplicationRecord.connection)
|
121
|
+
end
|
@@ -3,15 +3,52 @@ module RailsPulse
|
|
3
3
|
class InstallGenerator < Rails::Generators::Base
|
4
4
|
source_root File.expand_path("templates", __dir__)
|
5
5
|
|
6
|
-
desc "
|
7
|
-
|
8
|
-
|
6
|
+
desc "Install Rails Pulse with schema-based setup"
|
7
|
+
|
8
|
+
def copy_schema
|
9
|
+
copy_file "db/rails_pulse_schema.rb", "db/rails_pulse_schema.rb"
|
9
10
|
end
|
10
11
|
|
11
|
-
desc "Copies Rails Pulse example configuration file to the application."
|
12
12
|
def copy_initializer
|
13
13
|
copy_file "rails_pulse.rb", "config/initializers/rails_pulse.rb"
|
14
14
|
end
|
15
|
+
|
16
|
+
def create_database_migration_paths
|
17
|
+
if separate_database?
|
18
|
+
create_file "db/rails_pulse_migrate/.keep"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def display_post_install_message
|
23
|
+
say <<~MESSAGE
|
24
|
+
|
25
|
+
Rails Pulse installation complete!
|
26
|
+
|
27
|
+
Next steps:
|
28
|
+
1. Configure your database in config/database.yml (see README for examples)
|
29
|
+
2. Run: rails db:prepare (creates database and loads schema)
|
30
|
+
3. Restart your Rails server
|
31
|
+
|
32
|
+
For separate database setup, add to config/database.yml:
|
33
|
+
#{environment}:
|
34
|
+
rails_pulse:
|
35
|
+
<<: *default
|
36
|
+
database: storage/#{environment}_rails_pulse.sqlite3
|
37
|
+
migrations_paths: db/rails_pulse_migrate
|
38
|
+
|
39
|
+
MESSAGE
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def separate_database?
|
45
|
+
# Could make this configurable via options
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def environment
|
50
|
+
Rails.env.production? ? "production" : "development"
|
51
|
+
end
|
15
52
|
end
|
16
53
|
end
|
17
54
|
end
|