rails_pulse 0.4.2 → 0.5.0.pre.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/CHANGELOG.md +58 -1
- data/README.md +15 -4
- data/app/assets/stylesheets/rails_pulse/components/chart.css +11 -0
- data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +19 -1
- data/app/assets/stylesheets/rails_pulse/components/row.css +3 -17
- data/app/controllers/concerns/chart_table_concern.rb +28 -69
- data/app/controllers/concerns/deployment_markers_concern.rb +3 -1
- data/app/controllers/concerns/metric_card_concern.rb +6 -7
- data/app/controllers/rails_pulse/api/v1/base_controller.rb +133 -0
- data/app/controllers/rails_pulse/api/v1/capabilities_controller.rb +27 -0
- data/app/controllers/rails_pulse/api/v1/coverage_controller.rb +158 -0
- data/app/controllers/rails_pulse/api/v1/deployments_controller.rb +40 -0
- data/app/controllers/rails_pulse/api/v1/exceptions_controller.rb +77 -0
- data/app/controllers/rails_pulse/api/v1/insights_controller.rb +62 -0
- data/app/controllers/rails_pulse/api/v1/job_runs_controller.rb +34 -0
- data/app/controllers/rails_pulse/api/v1/jobs_controller.rb +172 -0
- data/app/controllers/rails_pulse/api/v1/queries_controller.rb +123 -0
- data/app/controllers/rails_pulse/api/v1/requests_controller.rb +74 -0
- data/app/controllers/rails_pulse/api/v1/routes_controller.rb +103 -0
- data/app/controllers/rails_pulse/application_controller.rb +1 -1
- data/app/controllers/rails_pulse/dashboard_controller.rb +25 -21
- data/app/controllers/rails_pulse/deployments_controller.rb +23 -13
- data/app/controllers/rails_pulse/exceptions_controller.rb +10 -12
- data/app/controllers/rails_pulse/jobs_controller.rb +1 -2
- data/app/controllers/rails_pulse/queries_controller.rb +6 -5
- data/app/controllers/rails_pulse/requests_controller.rb +15 -14
- data/app/controllers/rails_pulse/routes_controller.rb +4 -8
- data/app/helpers/rails_pulse/application_helper.rb +15 -0
- data/app/helpers/rails_pulse/chart_helper.rb +9 -2
- data/app/helpers/rails_pulse/formatting_helper.rb +11 -12
- data/app/javascript/rails_pulse/controllers/chart_controller.js +80 -43
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +4 -2
- data/app/jobs/rails_pulse/cloud_health_job.rb +11 -0
- data/app/jobs/rails_pulse/cloud_sync_job.rb +11 -0
- data/app/jobs/rails_pulse/summary_job.rb +13 -0
- data/app/models/concerns/rails_pulse/has_metadata.rb +15 -0
- data/app/models/concerns/rails_pulse/taggable.rb +5 -12
- data/app/models/rails_pulse/cards/base.rb +20 -11
- data/app/models/rails_pulse/charts/base.rb +5 -18
- data/app/models/rails_pulse/charts/percentile_chart_base.rb +3 -5
- data/app/models/rails_pulse/cleanup_run.rb +32 -0
- data/app/models/rails_pulse/cloud/buffered_batch.rb +55 -0
- data/app/models/rails_pulse/cloud/installation.rb +55 -0
- data/app/models/rails_pulse/dashboard/charts/response_time_percentiles.rb +4 -5
- data/app/models/rails_pulse/dashboard/charts/throughput_and_errors.rb +4 -5
- data/app/models/rails_pulse/dashboard/concerns/attention_classification.rb +104 -0
- data/app/models/rails_pulse/dashboard/concerns/time_range_helper.rb +25 -2
- data/app/models/rails_pulse/dashboard/health_summary.rb +38 -13
- data/app/models/rails_pulse/dashboard/needs_attention.rb +31 -98
- data/app/models/rails_pulse/dashboard/storage_pressure.rb +39 -63
- data/app/models/rails_pulse/dashboard/storage_status.rb +130 -39
- data/app/models/rails_pulse/deployment.rb +2 -7
- data/app/models/rails_pulse/event.rb +41 -0
- data/app/models/rails_pulse/exception_occurrence.rb +1 -1
- data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +3 -6
- data/app/models/rails_pulse/exceptions/cards/open_groups.rb +1 -4
- data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +3 -6
- data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +4 -7
- data/app/models/rails_pulse/job_run.rb +1 -1
- data/app/models/rails_pulse/jobs/cards/failure_rate.rb +8 -14
- data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -14
- data/app/models/rails_pulse/jobs/cards/total_runs.rb +5 -11
- data/app/models/rails_pulse/jobs/charts/duration.rb +1 -2
- data/app/models/rails_pulse/jobs/charts/execution_volume.rb +1 -1
- data/app/models/rails_pulse/jobs/charts/failure_rate.rb +1 -2
- data/app/models/rails_pulse/like_pattern.rb +35 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +6 -12
- data/app/models/rails_pulse/queries/cards/database_load.rb +0 -9
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +5 -11
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +6 -12
- data/app/models/rails_pulse/queries/charts/database_load.rb +5 -8
- data/app/models/rails_pulse/queries/charts/execution_volume.rb +1 -1
- data/app/models/rails_pulse/request.rb +1 -1
- data/app/models/rails_pulse/routes/cards/error_rates.rb +5 -15
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +5 -17
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +5 -17
- data/app/models/rails_pulse/routes/charts/error_rate.rb +1 -1
- data/app/models/rails_pulse/routes/charts/request_volume.rb +1 -1
- data/app/models/rails_pulse/summary.rb +5 -0
- data/app/models/rails_pulse/tables/base.rb +5 -6
- data/app/models/rails_pulse/time_range.rb +309 -0
- data/app/models/rails_pulse/time_window.rb +15 -0
- data/app/models/rails_pulse/writer_heartbeat.rb +90 -0
- data/app/serializers/rails_pulse/api/v1/deployment_serializer.rb +23 -0
- data/app/serializers/rails_pulse/api/v1/exception_group_serializer.rb +23 -0
- data/app/serializers/rails_pulse/api/v1/exception_occurrence_serializer.rb +22 -0
- data/app/serializers/rails_pulse/api/v1/job_run_serializer.rb +28 -0
- data/app/serializers/rails_pulse/api/v1/job_serializer.rb +26 -0
- data/app/serializers/rails_pulse/api/v1/query_serializer.rb +25 -0
- data/app/serializers/rails_pulse/api/v1/request_serializer.rb +22 -0
- data/app/serializers/rails_pulse/api/v1/route_serializer.rb +19 -0
- data/app/services/rails_pulse/cloud/batch.rb +83 -0
- data/app/services/rails_pulse/cloud/client.rb +68 -0
- data/app/services/rails_pulse/cloud/combined_metrics.rb +49 -0
- data/app/services/rails_pulse/cloud/health_item.rb +77 -0
- data/app/services/rails_pulse/cloud/path_prefix.rb +50 -0
- data/app/services/rails_pulse/cloud/preview.rb +83 -0
- data/app/services/rails_pulse/cloud/query_shape.rb +195 -0
- data/app/services/rails_pulse/cloud/record_items.rb +60 -0
- data/app/services/rails_pulse/cloud/route_pattern.rb +111 -0
- data/app/services/rails_pulse/cloud/summary_items.rb +188 -0
- data/app/services/rails_pulse/cloud/sync.rb +275 -0
- data/app/services/rails_pulse/config_recommendations.rb +132 -0
- data/app/services/rails_pulse/deployment_comparison.rb +177 -0
- data/app/services/rails_pulse/exception_capture_service.rb +1 -1
- data/app/services/rails_pulse/operations/change_point.rb +1 -1
- data/app/services/rails_pulse/operations/comparison.rb +1 -1
- data/app/services/rails_pulse/period_insights.rb +131 -0
- data/app/services/rails_pulse/sql_query_normalizer.rb +222 -122
- data/app/services/rails_pulse/summary_service/from_child_periods.rb +161 -0
- data/app/services/rails_pulse/summary_service/from_raw_rows.rb +115 -0
- data/app/services/rails_pulse/summary_service/metrics.rb +38 -0
- data/app/services/rails_pulse/summary_service.rb +121 -133
- data/app/services/rails_pulse/tag_filter_service.rb +5 -5
- data/app/views/layouts/rails_pulse/_time_range_selector.html.erb +4 -1
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
- data/app/views/rails_pulse/components/_table_head.html.erb +2 -2
- data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -1
- data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +2 -2
- data/app/views/rails_pulse/dashboard/_health_badge.html.erb +29 -20
- data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +13 -3
- data/app/views/rails_pulse/dashboard/index.html.erb +16 -2
- data/app/views/rails_pulse/jobs/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/jobs/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/queries/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/show.html.erb +1 -1
- data/app/views/rails_pulse/requests/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/routes/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/show.html.erb +1 -1
- data/app/views/rails_pulse/shared/_aggregation_zone_badge.html.erb +4 -0
- data/app/views/rails_pulse/storage/show.html.erb +39 -2
- data/config/routes.rb +23 -0
- data/db/rails_pulse_cloud_schema.rb +58 -0
- data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +1 -1
- data/db/rails_pulse_migrate/20260921000001_create_rails_pulse_events.rb +21 -0
- data/db/rails_pulse_schema.rb +20 -2
- data/exe/rails-pulse +7 -0
- data/lib/generators/rails_pulse/base_methods.rb +19 -0
- data/lib/generators/rails_pulse/install_cloud_generator.rb +84 -0
- data/lib/generators/rails_pulse/templates/db/rails_pulse_cloud_schema.rb +58 -0
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +20 -2
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_cloud_tables.rb +30 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +71 -19
- data/lib/generators/rails_pulse/upgrade_generator.rb +9 -16
- data/lib/rails_pulse/cleanup_service.rb +37 -20
- data/lib/rails_pulse/cli/agent_files/agents.md +89 -0
- data/lib/rails_pulse/cli/agent_files/claude_skill.md +182 -0
- data/lib/rails_pulse/cli/base_command.rb +37 -0
- data/lib/rails_pulse/cli/client.rb +64 -0
- data/lib/rails_pulse/cli/config.rb +87 -0
- data/lib/rails_pulse/cli/configure.rb +75 -0
- data/lib/rails_pulse/cli/coverage.rb +91 -0
- data/lib/rails_pulse/cli/deployments.rb +54 -0
- data/lib/rails_pulse/cli/exceptions.rb +116 -0
- data/lib/rails_pulse/cli/formatter.rb +31 -0
- data/lib/rails_pulse/cli/insights.rb +70 -0
- data/lib/rails_pulse/cli/install.rb +145 -0
- data/lib/rails_pulse/cli/job_runs.rb +53 -0
- data/lib/rails_pulse/cli/jobs.rb +71 -0
- data/lib/rails_pulse/cli/main.rb +76 -0
- data/lib/rails_pulse/cli/mcp.rb +47 -0
- data/lib/rails_pulse/cli/queries.rb +67 -0
- data/lib/rails_pulse/cli/requests.rb +57 -0
- data/lib/rails_pulse/cli/routes.rb +73 -0
- data/lib/rails_pulse/configuration.rb +100 -2
- data/lib/rails_pulse/current.rb +11 -0
- data/lib/rails_pulse/engine.rb +12 -3
- data/lib/rails_pulse/installers/config_updater.rb +9 -2
- data/lib/rails_pulse/job_run_collector.rb +15 -15
- data/lib/rails_pulse/mcp/server.rb +78 -0
- data/lib/rails_pulse/mcp/tools/coverage.rb +96 -0
- data/lib/rails_pulse/mcp/tools/deployments.rb +117 -0
- data/lib/rails_pulse/mcp/tools/endpoint.rb +167 -0
- data/lib/rails_pulse/mcp/tools/errors.rb +104 -0
- data/lib/rails_pulse/mcp/tools/exception_detail.rb +118 -0
- data/lib/rails_pulse/mcp/tools/exceptions.rb +128 -0
- data/lib/rails_pulse/mcp/tools/helpers.rb +162 -0
- data/lib/rails_pulse/mcp/tools/insights.rb +99 -0
- data/lib/rails_pulse/mcp/tools/jobs.rb +155 -0
- data/lib/rails_pulse/mcp/tools/queries.rb +142 -0
- data/lib/rails_pulse/mcp/tools/routes.rb +108 -0
- data/lib/rails_pulse/mcp/tools/slow_requests.rb +118 -0
- data/lib/rails_pulse/middleware/request_collector.rb +9 -9
- data/lib/rails_pulse/scoped_inflector.rb +35 -0
- data/lib/rails_pulse/statistics.rb +27 -0
- data/lib/rails_pulse/subscribers/exception_subscriber.rb +2 -2
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +15 -29
- data/lib/rails_pulse/tasks/status_reporter.rb +201 -0
- data/lib/rails_pulse/tracker.rb +76 -14
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +0 -4
- data/lib/tasks/rails_pulse.rake +6 -0
- data/lib/tasks/rails_pulse_tasks.rake +7 -0
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +13 -13
- metadata +123 -24
- data/app/controllers/concerns/page_timings.rb +0 -10
- data/app/controllers/concerns/response_range_concern.rb +0 -45
- data/app/controllers/concerns/time_range_concern.rb +0 -152
- data/app/controllers/concerns/zoom_range_concern.rb +0 -86
|
@@ -7,7 +7,7 @@ require "rails_pulse/route_indexes" unless defined?(RailsPulse::RouteIndexes)
|
|
|
7
7
|
RailsPulse::Schema = lambda do |connection|
|
|
8
8
|
adapter = connection.adapter_name.downcase
|
|
9
9
|
# Skip if all tables already exist to prevent conflicts
|
|
10
|
-
required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_jobs, :rails_pulse_job_runs, :rails_pulse_summaries, :rails_pulse_deployments, :rails_pulse_exception_groups, :rails_pulse_exception_occurrences ]
|
|
10
|
+
required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_jobs, :rails_pulse_job_runs, :rails_pulse_summaries, :rails_pulse_deployments, :rails_pulse_exception_groups, :rails_pulse_exception_occurrences, :rails_pulse_events ]
|
|
11
11
|
|
|
12
12
|
# Check which tables already exist
|
|
13
13
|
existing_tables = required_tables.select { |table| connection.table_exists?(table) }
|
|
@@ -249,7 +249,7 @@ RailsPulse::Schema = lambda do |connection|
|
|
|
249
249
|
t.string :request_url, comment: "Nullable — web requests only"
|
|
250
250
|
t.string :request_method, comment: "GET, POST, etc."
|
|
251
251
|
t.string :environment, comment: "production, staging, etc."
|
|
252
|
-
t.string :deploy_sha, comment: "Captured
|
|
252
|
+
t.string :deploy_sha, comment: "Captured at occurrence time — cannot backfill later"
|
|
253
253
|
t.text :request_params, comment: "JSON hash of filtered request params — web requests only"
|
|
254
254
|
t.datetime :occurred_at, null: false
|
|
255
255
|
t.timestamps
|
|
@@ -259,6 +259,24 @@ RailsPulse::Schema = lambda do |connection|
|
|
|
259
259
|
connection.add_index :rails_pulse_exception_occurrences, :exception_group_id, name: "index_rp_exception_occurrences_on_group_id"
|
|
260
260
|
end
|
|
261
261
|
|
|
262
|
+
unless connection.table_exists?(:rails_pulse_events)
|
|
263
|
+
connection.create_table :rails_pulse_events do |t|
|
|
264
|
+
t.string :kind, null: false, comment: "What noticed it, e.g. writer_heartbeat"
|
|
265
|
+
t.string :subject, comment: "Who it is about: host:pid, rule name, job name, exception class"
|
|
266
|
+
t.string :outcome, null: false, comment: "sampled, triggered, clean, insufficient_data, ran"
|
|
267
|
+
t.decimal :value, precision: 15, scale: 6, comment: "The number behind it: requests dropped since the last sample, or the triggered metric value"
|
|
268
|
+
t.datetime :occurred_at, null: false
|
|
269
|
+
t.text :message
|
|
270
|
+
t.text :metadata, comment: "JSON with the kind-specific detail"
|
|
271
|
+
t.timestamps
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
connection.add_index :rails_pulse_events, [ :kind, :occurred_at ],
|
|
275
|
+
name: "index_rp_events_on_kind_and_occurred_at"
|
|
276
|
+
connection.add_index :rails_pulse_events, [ :kind, :subject, :occurred_at ],
|
|
277
|
+
name: "index_rp_events_on_kind_subject_and_occurred_at"
|
|
278
|
+
end
|
|
279
|
+
|
|
262
280
|
# Add indexes to existing tables for efficient aggregation
|
|
263
281
|
unless connection.index_exists?(:rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation")
|
|
264
282
|
connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Generated by rails_pulse:install_cloud - loads the Rails Pulse Cloud schema definition
|
|
2
|
+
class InstallRailsPulseCloudTables < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
|
3
|
+
def up
|
|
4
|
+
if rails_pulse_cloud_installed?
|
|
5
|
+
say "Rails Pulse Cloud tables already exist. Skipping installation.", :yellow
|
|
6
|
+
return
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Load and execute the Cloud schema directly, so this migration always
|
|
10
|
+
# creates the tables as db/rails_pulse_cloud_schema.rb describes them.
|
|
11
|
+
schema_file = File.join(::Rails.root.to_s, "db/rails_pulse_cloud_schema.rb")
|
|
12
|
+
raise "Rails Pulse Cloud schema file not found at db/rails_pulse_cloud_schema.rb" unless File.exist?(schema_file)
|
|
13
|
+
|
|
14
|
+
say "Loading Rails Pulse Cloud schema from db/rails_pulse_cloud_schema.rb"
|
|
15
|
+
load schema_file
|
|
16
|
+
RailsPulse::CloudSchema.call(connection)
|
|
17
|
+
say "Rails Pulse Cloud tables created successfully"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def down
|
|
21
|
+
drop_table :rails_pulse_cloud_batches if table_exists?(:rails_pulse_cloud_batches)
|
|
22
|
+
drop_table :rails_pulse_cloud_installations if table_exists?(:rails_pulse_cloud_installations)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def rails_pulse_cloud_installed?
|
|
28
|
+
table_exists?(:rails_pulse_cloud_installations) && table_exists?(:rails_pulse_cloud_batches)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -327,22 +327,36 @@ RailsPulse.configure do |config|
|
|
|
327
327
|
# }
|
|
328
328
|
|
|
329
329
|
# ====================================================================================================
|
|
330
|
-
#
|
|
330
|
+
# API TOKENS AND DEPLOYMENT TRACKING
|
|
331
331
|
# ====================================================================================================
|
|
332
|
-
#
|
|
333
|
-
#
|
|
332
|
+
# Two tokens authenticate what is not the dashboard, so a credential given to a coding agent
|
|
333
|
+
# cannot write anything. Both are sent as an `X-Rails-Pulse-Token` header.
|
|
334
334
|
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
337
|
-
#
|
|
335
|
+
# api_token reads the JSON API under /rails_pulse/api/v1 — what the `rails-pulse` CLI and the
|
|
336
|
+
# MCP server your coding agent uses talk to. Every endpoint under it is read-only.
|
|
337
|
+
# config.api_token = Rails.application.credentials.dig(:rails_pulse, :api_token)
|
|
338
|
+
# config.api_token = ENV["RAILS_PULSE_API_TOKEN"]
|
|
338
339
|
#
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
# config.
|
|
340
|
+
# deployment_token records a release through POST /rails_pulse/deployments, which your CI calls.
|
|
341
|
+
# It is the only credential that endpoint accepts: api_token is refused there.
|
|
342
|
+
# config.deployment_token = Rails.application.credentials.dig(:rails_pulse, :deployment_token)
|
|
343
|
+
# config.deployment_token = ENV["RAILS_PULSE_DEPLOYMENT_TOKEN"]
|
|
342
344
|
#
|
|
343
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
345
|
+
# Set both to the same value if you would rather run one credential for both.
|
|
346
|
+
#
|
|
347
|
+
# With no api_token the JSON API refuses every request. With no deployment_token the deployments
|
|
348
|
+
# endpoint refuses every request too; a dashboard login is never accepted in its place. The
|
|
349
|
+
# rails_pulse:record_deployment and finish_deployment rake tasks need no token. (deployment_api_token
|
|
350
|
+
# is the pre-0.5 name for deployment_token and still works.)
|
|
351
|
+
#
|
|
352
|
+
# Point the CLI and MCP server at this app with `rails-pulse configure`, or set RAILS_PULSE_URL
|
|
353
|
+
# and RAILS_PULSE_TOKEN in the agent's environment. The MCP server needs `gem "mcp"` in this
|
|
354
|
+
# app's Gemfile (a development group is enough); Rails Pulse does not pull it into production.
|
|
355
|
+
#
|
|
356
|
+
# Deployments draw vertical markers on the charts so a change in performance can be lined up
|
|
357
|
+
# with the release that caused it. Limits: revision ≤ 255 characters, metadata ≤ 4 KB
|
|
358
|
+
# serialized, started_at at most one hour in the future. Rows beyond
|
|
359
|
+
# max_table_records[:rails_pulse_deployments] are pruned oldest-first by the cleanup task.
|
|
346
360
|
#
|
|
347
361
|
# Record a deployment from your CI/CD pipeline:
|
|
348
362
|
# curl -X POST https://yourapp.com/rails_pulse/deployments \
|
|
@@ -356,7 +370,37 @@ RailsPulse.configure do |config|
|
|
|
356
370
|
# Metadata for the rake task comes from an environment variable, as a JSON object:
|
|
357
371
|
# RAILS_PULSE_DEPLOYMENT_METADATA='{"environment":"production"}' rake rails_pulse:record_deployment[abc1234]
|
|
358
372
|
|
|
359
|
-
# config.
|
|
373
|
+
# config.api_token = ENV["RAILS_PULSE_API_TOKEN"]
|
|
374
|
+
# config.deployment_token = ENV["RAILS_PULSE_DEPLOYMENT_TOKEN"]
|
|
375
|
+
|
|
376
|
+
# ====================================================================================================
|
|
377
|
+
# RAILS PULSE CLOUD
|
|
378
|
+
# ====================================================================================================
|
|
379
|
+
# Off unless both api_key and application are set; until then Rails Pulse makes no network calls.
|
|
380
|
+
# When on, it sends Rails Pulse Cloud an hourly summary of each route, query, job and exception,
|
|
381
|
+
# exception group and deployment records, and a health update every minute. It never sends request
|
|
382
|
+
# URLs or parameters, values in SQL, exception messages, backtraces or job arguments; the detail stays
|
|
383
|
+
# in this database. Everything in deployment metadata is sent as recorded.
|
|
384
|
+
# Run `rails rails_pulse:cloud:preview` to print exactly what would be sent.
|
|
385
|
+
#
|
|
386
|
+
# Cloud keeps its state in two tables of its own. Add them with
|
|
387
|
+
# `rails generate rails_pulse:install_cloud`, then migrate.
|
|
388
|
+
#
|
|
389
|
+
# The hourly sync runs after RailsPulse::SummaryJob. Schedule the health update every minute:
|
|
390
|
+
# # config/recurring.yml
|
|
391
|
+
# rails_pulse_cloud_health:
|
|
392
|
+
# class: RailsPulse::CloudHealthJob
|
|
393
|
+
# schedule: every minute
|
|
394
|
+
#
|
|
395
|
+
# The key comes from your Application's settings in Rails Pulse Cloud; application is its slug.
|
|
396
|
+
# config.cloud.api_key = ENV["RAILS_PULSE_CLOUD_API_KEY"]
|
|
397
|
+
# config.cloud.application = "your-app"
|
|
398
|
+
# Defaults to Rails.env.
|
|
399
|
+
# config.cloud.environment = Rails.env
|
|
400
|
+
# config.cloud.url = "https://ingest.railspulse.com"
|
|
401
|
+
# Hosts are named by hostname, which can show internal addresses and changes with every container.
|
|
402
|
+
# Set a stable label instead (defaults to RAILS_PULSE_HOST_LABEL).
|
|
403
|
+
# config.cloud.host_label = ENV["RAILS_PULSE_HOST_LABEL"]
|
|
360
404
|
|
|
361
405
|
# ====================================================================================================
|
|
362
406
|
# DATA CLEANUP
|
|
@@ -376,6 +420,14 @@ RailsPulse.configure do |config|
|
|
|
376
420
|
# Time-based retention - delete records older than this period
|
|
377
421
|
config.full_retention_period = 30.days
|
|
378
422
|
|
|
423
|
+
# How long rows in rails_pulse_events are kept. The background writer's
|
|
424
|
+
# heartbeats are pruned after a day regardless.
|
|
425
|
+
config.event_retention_period = 90.days
|
|
426
|
+
|
|
427
|
+
# Event kinds exempt from event_retention_period — for rows a writer updates
|
|
428
|
+
# in place rather than appends.
|
|
429
|
+
config.event_retention_exempt_kinds = []
|
|
430
|
+
|
|
379
431
|
# Count-based retention - maximum records to keep per table
|
|
380
432
|
# After time-based cleanup, if tables still exceed these limits,
|
|
381
433
|
# the oldest remaining records will be deleted to stay under the limit
|
|
@@ -404,12 +456,12 @@ RailsPulse.configure do |config|
|
|
|
404
456
|
# config.baseline_window = 28.days
|
|
405
457
|
# config.comparison_window = 1.day
|
|
406
458
|
|
|
407
|
-
# Hourly summaries decide how precisely a change point can be placed
|
|
408
|
-
#
|
|
409
|
-
#
|
|
410
|
-
#
|
|
411
|
-
# at the cost of summary table growth.
|
|
412
|
-
# config.hourly_summary_retention =
|
|
459
|
+
# Hourly summaries decide how precisely a change point can be placed, and
|
|
460
|
+
# deployment comparisons read them. Inside this window Rails Pulse can say a
|
|
461
|
+
# route slowed down at 14:00 and compare a deploy's hour before and after;
|
|
462
|
+
# beyond it, the finest answer is the day and a deploy can no longer be
|
|
463
|
+
# compared. Raising this buys precision at the cost of summary table growth.
|
|
464
|
+
# config.hourly_summary_retention = 7.days
|
|
413
465
|
|
|
414
466
|
# A change is reported as a regression only when it clears both the ratio and
|
|
415
467
|
# the absolute floor for its unit. The ratio alone flags trivial millisecond
|
|
@@ -60,22 +60,6 @@ module RailsPulse
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def has_separate_database_config?
|
|
64
|
-
config_path = File.join(root_path, "config/database.yml")
|
|
65
|
-
return false unless File.exist?(config_path)
|
|
66
|
-
|
|
67
|
-
require "yaml"
|
|
68
|
-
require "erb"
|
|
69
|
-
# Process ERB before YAML parsing — database.yml files commonly use ERB
|
|
70
|
-
# for environment-specific values. YAML.safe_load alone raises SyntaxError
|
|
71
|
-
# on ERB tags.
|
|
72
|
-
yaml_content = ERB.new(File.read(config_path)).result
|
|
73
|
-
db_config = YAML.safe_load(yaml_content, aliases: true)
|
|
74
|
-
db_config.values.any? { |env| env.is_a?(Hash) && env.key?("rails_pulse") }
|
|
75
|
-
rescue
|
|
76
|
-
false
|
|
77
|
-
end
|
|
78
|
-
|
|
79
63
|
def rails_pulse_tables_exist?
|
|
80
64
|
return false unless defined?(ActiveRecord::Base)
|
|
81
65
|
|
|
@@ -129,6 +113,7 @@ module RailsPulse
|
|
|
129
113
|
# Refresh the schema file so fresh databases (test, CI) built from
|
|
130
114
|
# db/rails_pulse_schema.rb include all current columns and tables.
|
|
131
115
|
copy_file "db/rails_pulse_schema.rb", "db/rails_pulse_schema.rb", force: true
|
|
116
|
+
refresh_cloud_schema
|
|
132
117
|
sync_initializer
|
|
133
118
|
|
|
134
119
|
gem_migrations = get_gem_migrations
|
|
@@ -152,6 +137,14 @@ module RailsPulse
|
|
|
152
137
|
end
|
|
153
138
|
end
|
|
154
139
|
|
|
140
|
+
# Only an app that ran rails_pulse:install_cloud has the Cloud schema
|
|
141
|
+
# file; it is refreshed like the main one, never added.
|
|
142
|
+
def refresh_cloud_schema
|
|
143
|
+
return unless File.exist?(File.join(root_path, "db/rails_pulse_cloud_schema.rb"))
|
|
144
|
+
|
|
145
|
+
copy_file "db/rails_pulse_cloud_schema.rb", "db/rails_pulse_cloud_schema.rb", force: true
|
|
146
|
+
end
|
|
147
|
+
|
|
155
148
|
def announce_new_features(new_migrations)
|
|
156
149
|
notices = FEATURE_NOTICES.filter_map do |migration_key, notice|
|
|
157
150
|
notice if new_migrations.any? { |filename| filename.include?(migration_key) }
|
|
@@ -29,9 +29,11 @@ module RailsPulse
|
|
|
29
29
|
|
|
30
30
|
perform_time_based_cleanup
|
|
31
31
|
perform_count_based_cleanup
|
|
32
|
+
perform_event_cleanup
|
|
32
33
|
perform_summary_cleanup
|
|
33
34
|
|
|
34
35
|
log_cleanup_summary
|
|
36
|
+
record_run
|
|
35
37
|
raise @failures.first[:error] if @failures.any?
|
|
36
38
|
|
|
37
39
|
@stats
|
|
@@ -116,6 +118,22 @@ module RailsPulse
|
|
|
116
118
|
end
|
|
117
119
|
end
|
|
118
120
|
|
|
121
|
+
# event_retention_exempt_kinds are rows a writer updates in place, which
|
|
122
|
+
# must survive so "last ran" never becomes "never ran"
|
|
123
|
+
def perform_event_cleanup
|
|
124
|
+
return unless @config.event_retention_period
|
|
125
|
+
return unless RailsPulse::Event.table_available?
|
|
126
|
+
|
|
127
|
+
cutoff_time = [ @config.event_retention_period.ago, 1.hour.ago ].min
|
|
128
|
+
run_stage(:time_based, :events) do
|
|
129
|
+
delete_in_batches(
|
|
130
|
+
RailsPulse::Event
|
|
131
|
+
.where.not(kind: @config.event_retention_exempt_kinds)
|
|
132
|
+
.where(occurred_at: ...cutoff_time)
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
119
137
|
def perform_count_based_cleanup
|
|
120
138
|
return unless @config.max_table_records&.any?
|
|
121
139
|
|
|
@@ -313,37 +331,26 @@ module RailsPulse
|
|
|
313
331
|
RailsPulse::ExceptionOccurrence.where.not(exception_group_id: preserved_ids)
|
|
314
332
|
end
|
|
315
333
|
|
|
316
|
-
# Prune oldest
|
|
317
|
-
#
|
|
334
|
+
# Prune the oldest deletable groups when over the configured cap. Preserved
|
|
335
|
+
# and ignored groups are outside the cap: counting them would make a cap
|
|
336
|
+
# that they alone fill impossible to satisfy, so every run would trim the
|
|
337
|
+
# deletable groups for no gain (decision 0017). Child occurrences must be
|
|
338
|
+
# deleted first — there is no ON DELETE CASCADE on the FK.
|
|
318
339
|
def cleanup_exception_groups_by_count
|
|
319
340
|
max_records = @config.max_table_records[:rails_pulse_exception_groups]
|
|
320
341
|
return 0 unless max_records
|
|
321
342
|
|
|
322
|
-
current_count = RailsPulse::ExceptionGroup.count
|
|
323
|
-
return 0 if current_count <= max_records
|
|
324
|
-
|
|
325
|
-
overage = current_count - max_records
|
|
326
343
|
scope = RailsPulse::ExceptionGroup.where(preserve: false).where.not(status: "ignored")
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
deletable_count = scope.count
|
|
330
|
-
records_to_delete = [ overage, deletable_count ].min
|
|
331
|
-
return 0 if records_to_delete <= 0
|
|
344
|
+
overage = scope.count - max_records
|
|
345
|
+
return 0 if overage <= 0
|
|
332
346
|
|
|
333
|
-
|
|
347
|
+
delete_parents_in_batches(
|
|
334
348
|
scope,
|
|
335
349
|
order_column: :last_seen_at,
|
|
336
350
|
children: RailsPulse::ExceptionOccurrence,
|
|
337
351
|
foreign_key: :exception_group_id,
|
|
338
|
-
limit:
|
|
352
|
+
limit: overage
|
|
339
353
|
)
|
|
340
|
-
|
|
341
|
-
if overage > deletable_count
|
|
342
|
-
RailsPulse.logger.warn("[RailsPulse] Exception group cap #{max_records} cannot be met: " \
|
|
343
|
-
"#{current_count - deleted} remain (#{current_count - deletable_count} are preserved/ignored)")
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
deleted
|
|
347
354
|
end
|
|
348
355
|
|
|
349
356
|
# Delete groups whose last occurrence was removed — the orphan check and
|
|
@@ -374,6 +381,16 @@ module RailsPulse
|
|
|
374
381
|
end
|
|
375
382
|
end
|
|
376
383
|
|
|
384
|
+
# rails_pulse:status reads this to tell whether cleanup is scheduled. A
|
|
385
|
+
# failure to record must not turn a successful cleanup into a failed one.
|
|
386
|
+
def record_run
|
|
387
|
+
return unless RailsPulse::Event.table_available?
|
|
388
|
+
|
|
389
|
+
RailsPulse::CleanupRun.record!(stats: @stats, failed_stages: @failures.map { |failure| failure[:stage] })
|
|
390
|
+
rescue StandardError => e
|
|
391
|
+
RailsPulse.logger.error "Could not record the cleanup run: #{e.class}: #{e.message}"
|
|
392
|
+
end
|
|
393
|
+
|
|
377
394
|
# Returns the period_end of the most recent completed hourly overall-request
|
|
378
395
|
# summary, or nil if the summary job has never run. Records with occurred_at
|
|
379
396
|
# before this timestamp have been fully aggregated and are safe to delete.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Rails Pulse — Agent Integration
|
|
2
|
+
|
|
3
|
+
Rails Pulse gives AI coding agents read access to a Rails application's performance data: requests, SQL queries, background jobs, exceptions and deployments, all from the application's own database. Two interfaces, one token.
|
|
4
|
+
|
|
5
|
+
## MCP server
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
rails-pulse mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The server needs the `mcp` gem, which Rails Pulse does not depend on: add `gem "mcp", "~> 1.0"` to the application's Gemfile (a development group is enough), or `gem install mcp` when running `rails-pulse` outside Bundler.
|
|
12
|
+
|
|
13
|
+
**Claude Code** (`~/.claude.json`):
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"rails-pulse": {
|
|
18
|
+
"command": "rails-pulse",
|
|
19
|
+
"args": ["mcp"],
|
|
20
|
+
"env": {
|
|
21
|
+
"RAILS_PULSE_URL": "https://myapp.com",
|
|
22
|
+
"RAILS_PULSE_TOKEN": "your-token"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Cursor** (`.cursor/mcp.json`):
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"rails-pulse": { "command": "rails-pulse", "args": ["mcp"] }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Tools
|
|
39
|
+
|
|
40
|
+
All tools are read-only. Each returns a `summary` and `next_steps`.
|
|
41
|
+
|
|
42
|
+
| Tool | Purpose |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| `rails_pulse_routes` | Discover endpoints with request volume, latency and errors |
|
|
45
|
+
| `rails_pulse_slow_requests` | Slowest endpoints for a period |
|
|
46
|
+
| `rails_pulse_coverage` | What has been recorded, how recently, and any collection gaps |
|
|
47
|
+
| `rails_pulse_errors` | Recent errors grouped by endpoint |
|
|
48
|
+
| `rails_pulse_exceptions` | Exception groups with status, count, location and latest message |
|
|
49
|
+
| `rails_pulse_exception` | One group's recent occurrences with backtraces, request and params |
|
|
50
|
+
| `rails_pulse_endpoint` | Deep performance profile of one endpoint |
|
|
51
|
+
| `rails_pulse_queries` | Most expensive SQL queries with N+1 detection |
|
|
52
|
+
| `rails_pulse_jobs` | Background job health and recent failures |
|
|
53
|
+
| `rails_pulse_deployments` | Recent deployments with timing, metadata and a before/after comparison |
|
|
54
|
+
| `rails_pulse_insights` | What needs attention over one hour, day, week or month, and threshold suggestions |
|
|
55
|
+
|
|
56
|
+
Every tool that takes a `period` also takes `since` and `until` as ISO 8601 timestamps, read as UTC when no zone is given, and echoes the bounds it measured back as `window`. `period` itself is only `last_hour`, `last_24_hours` or `last_7_days` (plus `all` on `rails_pulse_exceptions`); any other window is `since`/`until`. `rails_pulse_insights` instead reads one whole summary period (`period`: `hour`, `day`, `week` or `month`, and `at` for a time inside it), so its P95s are exact. Use them to compare two fixed windows — the 24 hours before a release against the 24 hours after — rather than a relative period that moves between calls.
|
|
57
|
+
|
|
58
|
+
## CLI
|
|
59
|
+
|
|
60
|
+
The `rails-pulse` CLI works without MCP. Append `--json` for structured output.
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| `rails-pulse routes list` | Tracked routes (`--since` adds request stats) |
|
|
65
|
+
| `rails-pulse requests list` | Recorded requests with status and time filters |
|
|
66
|
+
| `rails-pulse queries list` | SQL queries (`--since` adds timing stats) |
|
|
67
|
+
| `rails-pulse jobs list` | Background jobs: lifetime stats, or one window's with `--since` / `--until` |
|
|
68
|
+
| `rails-pulse job_runs list` | Individual job runs with errors |
|
|
69
|
+
| `rails-pulse exceptions list` | Exception groups with status and occurrence counts |
|
|
70
|
+
| `rails-pulse exceptions show ID` | One group with backtraces and recent occurrences |
|
|
71
|
+
| `rails-pulse deployments list` | Recorded deployments, most recent first, with each one's comparison |
|
|
72
|
+
| `rails-pulse coverage show` | What has been recorded, how recently, and any collection gaps |
|
|
73
|
+
| `rails-pulse insights show` | What needs attention over one period, and threshold suggestions |
|
|
74
|
+
|
|
75
|
+
Common flags on every list command: `--limit N` (1 to 500, default 25), `--offset N`, `--json`, `--since TIME` / `--until TIME` (ISO 8601; no zone means UTC).
|
|
76
|
+
|
|
77
|
+
## Authentication
|
|
78
|
+
|
|
79
|
+
Set `RAILS_PULSE_URL` and `RAILS_PULSE_TOKEN` (the application's `config.api_token`), or run `rails-pulse configure` to write `~/.rails-pulse`. That token is read-only; recording a deployment needs `config.deployment_token`, which these tools do not carry.
|
|
80
|
+
|
|
81
|
+
## Investigation pattern
|
|
82
|
+
|
|
83
|
+
1. Pin the time window to a deployment (`rails_pulse_deployments`); its `comparison` says whether response time or error rate got worse in the hour after it
|
|
84
|
+
2. Identify slow or failing endpoints (`rails_pulse_insights` for the last day or week, `rails_pulse_slow_requests` for any window); resolve names with `rails_pulse_routes`
|
|
85
|
+
3. Profile the suspect endpoint (`rails_pulse_endpoint`)
|
|
86
|
+
4. Check errors (`rails_pulse_errors`)
|
|
87
|
+
5. Inspect SQL and jobs (`rails_pulse_queries`, `rails_pulse_jobs`)
|
|
88
|
+
6. Correlate with source code: pass the endpoint's `route_id` to `rails_pulse_queries` for the SQL inside it and the file and line each query came from
|
|
89
|
+
7. Fix, deploy, and re-check the same endpoint
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-pulse
|
|
3
|
+
description: Investigate Rails application performance with Rails Pulse. Use when asked about slow endpoints, regressions after a deploy, expensive or N+1 SQL, error rates, failing background jobs, or whether a performance fix worked. Prefers the rails_pulse_* MCP tools; falls back to the rails-pulse CLI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rails Pulse
|
|
7
|
+
|
|
8
|
+
Rails Pulse records every request, SQL query, background job and exception of a Rails application in the application's own database. This skill gives you read access to that data so you can find what is slow, why, and whether a change fixed it. Nothing here can modify the application.
|
|
9
|
+
|
|
10
|
+
## When to use it
|
|
11
|
+
|
|
12
|
+
- Investigating a performance complaint or a slow endpoint
|
|
13
|
+
- Diagnosing a regression after a deployment
|
|
14
|
+
- Finding expensive or N+1 SQL queries
|
|
15
|
+
- Investigating an increased error rate
|
|
16
|
+
- Analysing background job failures or slowness
|
|
17
|
+
- Validating whether a performance fix worked
|
|
18
|
+
|
|
19
|
+
## Interfaces
|
|
20
|
+
|
|
21
|
+
### MCP tools (preferred when available)
|
|
22
|
+
|
|
23
|
+
All tools are read-only. Each returns a `summary` and `next_steps`.
|
|
24
|
+
|
|
25
|
+
| Tool | Purpose |
|
|
26
|
+
|------|---------|
|
|
27
|
+
| `rails_pulse_routes` | Discover endpoints: request volume, latency and errors per route |
|
|
28
|
+
| `rails_pulse_slow_requests` | The slowest endpoints for a period |
|
|
29
|
+
| `rails_pulse_coverage` | What has been recorded, how recently, and any collection gaps |
|
|
30
|
+
| `rails_pulse_errors` | Recent 4xx/5xx responses grouped by endpoint |
|
|
31
|
+
| `rails_pulse_exceptions` | Exception groups with status, occurrence count, location and latest message |
|
|
32
|
+
| `rails_pulse_exception` | One group's recent occurrences with full backtraces, request, params and deploy SHA |
|
|
33
|
+
| `rails_pulse_endpoint` | Deep profile of one endpoint |
|
|
34
|
+
| `rails_pulse_queries` | Most expensive SQL queries, with N+1 detection |
|
|
35
|
+
| `rails_pulse_jobs` | Background job health and recent failures with error classes |
|
|
36
|
+
| `rails_pulse_deployments` | Recent deployments with timing, metadata, and whether each one degraded response time or error rate |
|
|
37
|
+
| `rails_pulse_insights` | What needs attention over one hour, day, week or month, and whether the thresholds fit it |
|
|
38
|
+
|
|
39
|
+
### CLI
|
|
40
|
+
|
|
41
|
+
The `rails-pulse` executable ships with the gem. Add `--json` for structured output.
|
|
42
|
+
|
|
43
|
+
| Command | Purpose |
|
|
44
|
+
|---------|---------|
|
|
45
|
+
| `rails-pulse routes list --json` | Tracked routes (add `--since` for stats) |
|
|
46
|
+
| `rails-pulse requests list --json` | Recorded requests with status and time filters |
|
|
47
|
+
| `rails-pulse queries list --json` | SQL queries (add `--since` for timing stats) |
|
|
48
|
+
| `rails-pulse jobs list --json` | Background jobs: lifetime stats, or one window's with `--since` / `--until` |
|
|
49
|
+
| `rails-pulse job_runs list --json` | Individual job runs with error class and message |
|
|
50
|
+
| `rails-pulse exceptions list --json` | Exception groups with status, count, location and message |
|
|
51
|
+
| `rails-pulse exceptions show ID --json` | One group with backtraces, request, params and deploy SHA |
|
|
52
|
+
| `rails-pulse deployments list --json` | Recorded deployments, most recent first, each with its before/after comparison |
|
|
53
|
+
| `rails-pulse coverage show --json` | What has been recorded, how recently, and any collection gaps |
|
|
54
|
+
| `rails-pulse insights show --period week --json` | What needs attention over one period, and suggested threshold changes |
|
|
55
|
+
|
|
56
|
+
## Investigation workflow
|
|
57
|
+
|
|
58
|
+
### 1. Establish the time period
|
|
59
|
+
|
|
60
|
+
Check recent deployments first; a regression usually lines up with one.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
rails_pulse_deployments(period: "last_7_days")
|
|
64
|
+
rails-pulse deployments list --json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Each deployment carries a `comparison` of the hour before the deploy started against the hour
|
|
68
|
+
after it finished. `degraded` means average or p95 response time got more than 1.5x and at
|
|
69
|
+
least 50ms worse, or the error rate more than 1.25x and at least a percentage point worse; the
|
|
70
|
+
`metrics` say which. `insufficient_data` means fewer than 10 requests in either hour, so it is
|
|
71
|
+
not an all-clear; `pending` means the hour after has not been summarized yet; `unavailable`
|
|
72
|
+
means it never will be because SummaryJob is not running. Use the `before` and `after` bounds
|
|
73
|
+
as `since`/`until` for the tools below to see which endpoints changed.
|
|
74
|
+
|
|
75
|
+
### 2. Identify affected endpoints
|
|
76
|
+
|
|
77
|
+
For "what should I look at", start with the needs-attention list for the last complete day or
|
|
78
|
+
week; each item carries the route, query or job `id` to drill into.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
rails_pulse_insights(period: "day")
|
|
82
|
+
rails-pulse insights show --period week --json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
rails_pulse_slow_requests(period: "last_24_hours", limit: 10)
|
|
87
|
+
rails_pulse_routes(search: "checkout", period: "last_7_days")
|
|
88
|
+
rails-pulse requests list --limit 50 --json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Profile the suspect endpoint
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
rails_pulse_endpoint(endpoint: "CheckoutController#create", period: "last_7_days")
|
|
95
|
+
rails-pulse requests list --since 2026-06-01T00:00:00Z --json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 4. Check errors
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
rails_pulse_errors(period: "last_24_hours", status: "5xx")
|
|
102
|
+
rails-pulse requests list --status 5xx --since 2026-06-01T00:00:00Z --json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 5. Inspect SQL and background jobs
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
rails_pulse_queries(period: "last_24_hours", sort: "total_duration")
|
|
109
|
+
rails_pulse_queries(n_plus_one_only: true)
|
|
110
|
+
rails_pulse_jobs(period: "last_24_hours")
|
|
111
|
+
rails-pulse queries list --since 2026-06-01T00:00:00Z --json
|
|
112
|
+
rails-pulse job_runs list --status failed --json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 6. Correlate with source code
|
|
116
|
+
|
|
117
|
+
Pass the endpoint's `route_id` to `rails_pulse_queries` to see only the SQL that ran inside it,
|
|
118
|
+
with `source_locations` naming the file and line each query was issued from.
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
rails_pulse_queries(route: 42, period: "last_24_hours")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Start at those locations rather than searching for the SQL. Look for N+1 queries, missing
|
|
125
|
+
indexes, expensive work in the request path, unnecessary serialization and missing caching.
|
|
126
|
+
|
|
127
|
+
### 7. Fix, then validate
|
|
128
|
+
|
|
129
|
+
After the fix is deployed, re-check the same endpoint over the period since the deploy.
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
rails_pulse_deployments(period: "last_24_hours")
|
|
133
|
+
rails_pulse_endpoint(endpoint: "CheckoutController#create", period: "last_hour")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
To show a change rather than a snapshot, measure the same endpoint over two fixed windows
|
|
137
|
+
either side of the release. Every windowed tool takes `since` and `until` as ISO 8601
|
|
138
|
+
timestamps, read as UTC when no zone is given, and echoes the bounds it used back as `window`.
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
rails_pulse_endpoint(endpoint: "CheckoutController#create", since: "2026-09-24T12:00:00Z", until: "2026-09-25T12:00:00Z")
|
|
142
|
+
rails_pulse_endpoint(endpoint: "CheckoutController#create", since: "2026-09-25T12:00:00Z", until: "2026-09-26T12:00:00Z")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use the deploy time as the boundary, and exclude the minutes around it when a rolling deploy
|
|
146
|
+
makes them a mix of both versions.
|
|
147
|
+
|
|
148
|
+
## Guidelines
|
|
149
|
+
|
|
150
|
+
- **SQL is not always the cause.** External API calls, serialization, view rendering and application logic slow requests too.
|
|
151
|
+
- **Use more than one tool.** Cross-reference latency with error rates, SQL timing and job performance before concluding.
|
|
152
|
+
- **Orient once before investigating.** `rails_pulse_coverage` names the application, environment and version you are querying. Call it first, so findings are attributed to the installation that actually produced them.
|
|
153
|
+
- **Confirm the data exists before reporting an all-clear.** An empty result means nothing was recorded, which only means nothing happened when collection was healthy over that window. Call `rails_pulse_coverage` before concluding a period was clean, and say so when it flags a gap, stale summaries or a kind that is not tracked.
|
|
154
|
+
- **Check the error rate.** A fast endpoint with a high error rate may be failing early rather than performing well.
|
|
155
|
+
- **Read percentiles, not just averages.** A low average with a high p95 or p99 means intermittent trouble.
|
|
156
|
+
- **Weigh request volume.** A slow endpoint nobody calls may not be worth the work.
|
|
157
|
+
- **Job figures cover the window.** `rails_pulse_jobs` reads them from summaries plus the runs not yet summarized. p95 and p99 are given only when one summary period holds the whole window; otherwise they are null and `note` says why, so compare averages and failure rates instead.
|
|
158
|
+
- **Threshold suggestions are a starting point.** `rails_pulse_insights` suggests a `config_snippet` from one period's data. Check the period was typical (not a deploy day or an incident) before proposing the change, and say which period it came from.
|
|
159
|
+
- **Check what the percentiles covered.** `rails_pulse_endpoint` computes them over the most recent requests it sampled, not the whole window. When `sampled_requests` is below `request_count` it says so in `latency.computed_over`; narrow the window until the two match before comparing percentiles across windows.
|
|
160
|
+
|
|
161
|
+
## Authentication
|
|
162
|
+
|
|
163
|
+
The CLI and MCP server read credentials from environment variables or `~/.rails-pulse`. The token is `config.api_token` in the application's Rails Pulse initializer. It is read-only: recording a deployment needs `config.deployment_token`, which these tools do not carry.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
RAILS_PULSE_URL=https://myapp.com
|
|
167
|
+
RAILS_PULSE_TOKEN=my-secret-token
|
|
168
|
+
RAILS_PULSE_MOUNT_PATH=/rails_pulse # optional, default /rails_pulse
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
# ~/.rails-pulse
|
|
173
|
+
url: https://myapp.com
|
|
174
|
+
token: my-secret-token
|
|
175
|
+
mount_path: /rails_pulse
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Interactive setup: `rails-pulse configure`.
|
|
179
|
+
|
|
180
|
+
## CLI reference
|
|
181
|
+
|
|
182
|
+
List commands accept `--limit N` (1 to 500, default 25), `--offset N` and `--json`. Every list command also accepts `--since TIME` and `--until TIME` as ISO 8601; a time with no zone is read as UTC. JSON output is `{ "data": [...], "meta": { "total", "limit", "offset" } }`; page with `--offset`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require_relative "config"
|
|
3
|
+
require_relative "client"
|
|
4
|
+
|
|
5
|
+
module RailsPulse
|
|
6
|
+
module CLI
|
|
7
|
+
class BaseCommand < Thor
|
|
8
|
+
# A usage error (unknown command, missing argument, bad flag) exits 1,
|
|
9
|
+
# so a script or CI step running the CLI can tell it failed.
|
|
10
|
+
def self.exit_on_failure?
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# A mistyped flag is an error rather than silently ignored, so
|
|
15
|
+
# `--limt 5` does not quietly return the default page.
|
|
16
|
+
check_unknown_options!
|
|
17
|
+
|
|
18
|
+
no_commands do
|
|
19
|
+
def with_error_handling
|
|
20
|
+
yield
|
|
21
|
+
rescue RailsPulse::CLI::Config::ConfigError => e
|
|
22
|
+
say "Error: #{e.message}.", :red
|
|
23
|
+
exit 1
|
|
24
|
+
rescue RailsPulse::CLI::Client::ApiError => e
|
|
25
|
+
say "API error: #{e.message}", :red
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def client
|
|
33
|
+
@client ||= Client.new
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|