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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require_relative "base_command"
|
|
3
|
+
|
|
4
|
+
module RailsPulse
|
|
5
|
+
module CLI
|
|
6
|
+
class Install < BaseCommand
|
|
7
|
+
AGENT_FILES_DIR = File.expand_path("agent_files", __dir__)
|
|
8
|
+
|
|
9
|
+
# Claude Code loads skills from ~/.claude/skills/<name>/SKILL.md; the
|
|
10
|
+
# file carries its own name and description in YAML front matter.
|
|
11
|
+
CLAUDE_SKILL_PATH = "~/.claude/skills/rails-pulse/SKILL.md".freeze
|
|
12
|
+
|
|
13
|
+
AGENTS_FILE = "agents.md".freeze
|
|
14
|
+
|
|
15
|
+
# Delimit the section so a re-run can replace exactly what a previous
|
|
16
|
+
# run wrote, leaving everything a project put around it untouched.
|
|
17
|
+
SECTION_START = "<!-- rails-pulse:start -->".freeze
|
|
18
|
+
SECTION_END = "<!-- rails-pulse:end -->".freeze
|
|
19
|
+
# Tolerates CRLF line endings and trailing spaces after a marker, which
|
|
20
|
+
# an editor or git's autocrlf can introduce between runs.
|
|
21
|
+
SECTION_PATTERN = /^#{Regexp.escape(SECTION_START)}[ \t]*\r?\n.*?^#{Regexp.escape(SECTION_END)}[ \t]*(?:\r?\n)?/m
|
|
22
|
+
|
|
23
|
+
INTEGRATIONS = {
|
|
24
|
+
"claude" => {
|
|
25
|
+
description: "Claude Code skill → #{CLAUDE_SKILL_PATH}",
|
|
26
|
+
source: "claude_skill.md"
|
|
27
|
+
},
|
|
28
|
+
"agents" => {
|
|
29
|
+
description: "Generic agent descriptor → ./#{AGENTS_FILE}",
|
|
30
|
+
source: "agents.md"
|
|
31
|
+
}
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
default_task :perform
|
|
35
|
+
|
|
36
|
+
desc "perform [INTEGRATION]", "Install an AI agent integration file"
|
|
37
|
+
long_desc <<~DESC
|
|
38
|
+
Copy a pre-built integration file to the correct location for the given agent framework.
|
|
39
|
+
|
|
40
|
+
Available integrations:
|
|
41
|
+
claude Installs a Claude Code skill to #{CLAUDE_SKILL_PATH}.
|
|
42
|
+
Claude Code then knows when and how to use the Rails Pulse MCP tools and CLI.
|
|
43
|
+
An earlier copy of the skill is replaced.
|
|
44
|
+
agents Installs a generic agent descriptor to ./#{AGENTS_FILE} in the current directory.
|
|
45
|
+
Compatible with other AI agent frameworks, including Codex. When an agents.md
|
|
46
|
+
or AGENTS.md already exists it is never overwritten: pass --append to add a
|
|
47
|
+
delimited Rails Pulse section to the end of it, which a later --append
|
|
48
|
+
replaces in place rather than duplicating.
|
|
49
|
+
|
|
50
|
+
Run --list to see all available integrations.
|
|
51
|
+
DESC
|
|
52
|
+
option :list, type: :boolean, desc: "List all available integrations and their destinations"
|
|
53
|
+
option :append, type: :boolean, default: false,
|
|
54
|
+
desc: "For 'agents': add a delimited Rails Pulse section to an existing AGENTS.md"
|
|
55
|
+
def perform(integration = nil)
|
|
56
|
+
if options[:list]
|
|
57
|
+
say "Available integrations:"
|
|
58
|
+
INTEGRATIONS.each { |name, meta| say " #{name.ljust(10)} #{meta[:description]}" }
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
case integration
|
|
63
|
+
when "claude"
|
|
64
|
+
install_claude
|
|
65
|
+
when "agents"
|
|
66
|
+
install_agents
|
|
67
|
+
else
|
|
68
|
+
say "Usage: rails-pulse install [claude|agents]", :yellow
|
|
69
|
+
say " rails-pulse install --list"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def install_claude
|
|
76
|
+
dest = File.expand_path(CLAUDE_SKILL_PATH)
|
|
77
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
78
|
+
FileUtils.cp(File.join(AGENT_FILES_DIR, "claude_skill.md"), dest)
|
|
79
|
+
say "Installed Claude Code skill to #{dest}", :green
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A project's own AGENTS.md is compared case-insensitively: on a
|
|
83
|
+
# case-insensitive filesystem (macOS by default) writing agents.md
|
|
84
|
+
# would silently replace it.
|
|
85
|
+
def install_agents
|
|
86
|
+
source = File.join(AGENT_FILES_DIR, "agents.md")
|
|
87
|
+
existing = Dir.children(Dir.pwd).find { |name| name.casecmp?(AGENTS_FILE) }
|
|
88
|
+
|
|
89
|
+
return append_to_agents(File.join(Dir.pwd, existing), source) if existing && options[:append]
|
|
90
|
+
|
|
91
|
+
if existing
|
|
92
|
+
say "#{File.join(Dir.pwd, existing)} already exists; not overwriting it.", :yellow
|
|
93
|
+
say "Run `rails-pulse install agents --append` to add a Rails Pulse section to it instead."
|
|
94
|
+
exit 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Written as a delimited section from the start, so a later --append
|
|
98
|
+
# recognises it and replaces it rather than adding a second copy.
|
|
99
|
+
dest = File.join(Dir.pwd, AGENTS_FILE)
|
|
100
|
+
File.write(dest, section(source, "\n"))
|
|
101
|
+
say "Installed agent descriptor to #{dest}", :green
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The project's own instructions are never rewritten: the Rails Pulse
|
|
105
|
+
# section is delimited, so a re-run replaces it where it sits and
|
|
106
|
+
# everything around it is left exactly as the project wrote it.
|
|
107
|
+
def append_to_agents(dest, source)
|
|
108
|
+
existing = File.read(dest)
|
|
109
|
+
newline = existing.include?("\r\n") ? "\r\n" : "\n"
|
|
110
|
+
section = section(source, newline)
|
|
111
|
+
|
|
112
|
+
if existing.match?(SECTION_PATTERN)
|
|
113
|
+
File.write(dest, existing.sub(SECTION_PATTERN) { section })
|
|
114
|
+
say "Updated the Rails Pulse section in #{dest}", :green
|
|
115
|
+
elsif existing.lstrip.start_with?(descriptor_title(source))
|
|
116
|
+
# A copy written before the section markers existed. Its end cannot
|
|
117
|
+
# be told from anything the project added after it, so it is left
|
|
118
|
+
# for a person to replace rather than guessed at or duplicated.
|
|
119
|
+
say "#{dest} starts with a Rails Pulse descriptor that has no section markers; not appending a second copy.", :yellow
|
|
120
|
+
say "Delete the Rails Pulse part of it (or the file, if nothing else is in it) and run this again."
|
|
121
|
+
exit 1
|
|
122
|
+
else
|
|
123
|
+
separator = if existing.empty? || existing.end_with?("#{newline}#{newline}")
|
|
124
|
+
""
|
|
125
|
+
elsif existing.end_with?(newline)
|
|
126
|
+
newline
|
|
127
|
+
else
|
|
128
|
+
newline * 2
|
|
129
|
+
end
|
|
130
|
+
File.write(dest, "#{existing}#{separator}#{section}")
|
|
131
|
+
say "Appended a Rails Pulse section to #{dest}", :green
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def section(source, newline)
|
|
136
|
+
body = File.read(source).strip.gsub(/\r?\n/, newline)
|
|
137
|
+
"#{SECTION_START}#{newline}#{body}#{newline}#{SECTION_END}#{newline}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def descriptor_title(source)
|
|
141
|
+
File.foreach(source).first.to_s.strip
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require_relative "base_command"
|
|
2
|
+
require_relative "formatter"
|
|
3
|
+
|
|
4
|
+
module RailsPulse
|
|
5
|
+
module CLI
|
|
6
|
+
class JobRuns < BaseCommand
|
|
7
|
+
COLUMNS = [
|
|
8
|
+
[ "ID", 8, :id ],
|
|
9
|
+
[ "Job", 35, :job_name ],
|
|
10
|
+
[ "Status", 10, :status ],
|
|
11
|
+
[ "Occurred", 25, :occurred_at ],
|
|
12
|
+
[ "Duration", 10, :duration ],
|
|
13
|
+
[ "Error", 35, :error_class ]
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
desc "list", "List individual background job runs"
|
|
17
|
+
long_desc <<~DESC
|
|
18
|
+
Returns job runs ordered by most recent first.
|
|
19
|
+
|
|
20
|
+
Filter by status:
|
|
21
|
+
--status failed failed and discarded runs
|
|
22
|
+
--status success exact status (enqueued, running, success, failed, discarded, retried)
|
|
23
|
+
|
|
24
|
+
Filter by job class:
|
|
25
|
+
--job GenerateReportJob
|
|
26
|
+
|
|
27
|
+
Filter by time window (ISO 8601):
|
|
28
|
+
--since 2026-06-01T00:00:00Z
|
|
29
|
+
--until 2026-06-01T23:59:59Z
|
|
30
|
+
|
|
31
|
+
Use --json to get error messages and the full response envelope.
|
|
32
|
+
DESC
|
|
33
|
+
option :limit, type: :numeric, default: 25, desc: "Max records to return (1–500)"
|
|
34
|
+
option :offset, type: :numeric, default: 0, desc: "Number of records to skip (for pagination)"
|
|
35
|
+
option :since, type: :string, desc: "Return runs at or after this time (ISO 8601)"
|
|
36
|
+
option :until, type: :string, desc: "Return runs at or before this time (ISO 8601)"
|
|
37
|
+
option :status, type: :string, desc: "Filter by status (failed, success, discarded, ...)"
|
|
38
|
+
option :job, type: :string, desc: "Filter by job class name"
|
|
39
|
+
option :json, type: :boolean, default: false, desc: "Output raw JSON including meta envelope"
|
|
40
|
+
def list
|
|
41
|
+
with_error_handling do
|
|
42
|
+
params = { limit: options[:limit], offset: options[:offset] }
|
|
43
|
+
params[:since] = options[:since] if options[:since]
|
|
44
|
+
params[:until] = options[:until] if options[:until]
|
|
45
|
+
params[:status] = options[:status] if options[:status]
|
|
46
|
+
params[:job] = options[:job] if options[:job]
|
|
47
|
+
result = client.get("/job_runs", params)
|
|
48
|
+
Formatter.render(result, json: options[:json], columns: COLUMNS)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require_relative "base_command"
|
|
2
|
+
require_relative "formatter"
|
|
3
|
+
|
|
4
|
+
module RailsPulse
|
|
5
|
+
module CLI
|
|
6
|
+
class Jobs < BaseCommand
|
|
7
|
+
COLUMNS = [
|
|
8
|
+
[ "Name", 35, :name ],
|
|
9
|
+
[ "Queue", 15, :queue_name ],
|
|
10
|
+
[ "Runs", 8, :runs_count ],
|
|
11
|
+
[ "Failures", 8, :failures_count ],
|
|
12
|
+
[ "Fail %", 8, :failure_rate ],
|
|
13
|
+
[ "Avg (ms)", 10, :avg_duration ]
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
# With a window each row's figures cover it, read from summaries.
|
|
17
|
+
WINDOW_COLUMNS = [
|
|
18
|
+
[ "Name", 35, :name ],
|
|
19
|
+
[ "Queue", 15, :queue_name ],
|
|
20
|
+
[ "Runs", 8, :runs_count ],
|
|
21
|
+
[ "Failures", 8, :failures_count ],
|
|
22
|
+
[ "Fail %", 8, :failure_rate ],
|
|
23
|
+
[ "Avg (ms)", 10, :avg_duration ],
|
|
24
|
+
[ "Max (ms)", 10, :max_duration ]
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
desc "list", "List background jobs with run counts and duration stats"
|
|
28
|
+
long_desc <<~DESC
|
|
29
|
+
Returns all tracked background jobs ordered by name.
|
|
30
|
+
|
|
31
|
+
Without a window each row shows lifetime stats: total runs, failure count,
|
|
32
|
+
failure rate (%), and average duration in milliseconds.
|
|
33
|
+
|
|
34
|
+
With --since and/or --until (ISO 8601) each row covers that window instead,
|
|
35
|
+
and only jobs that ran in it are listed. p95 and p99 appear in --json output
|
|
36
|
+
when one summary period covers the window:
|
|
37
|
+
--since 2026-06-01T00:00:00Z --until 2026-06-02T00:00:00Z
|
|
38
|
+
|
|
39
|
+
Filter to jobs that have failed (inside the window, when one is given):
|
|
40
|
+
--status failed
|
|
41
|
+
|
|
42
|
+
Restrict to one job class:
|
|
43
|
+
--job GenerateReportJob
|
|
44
|
+
|
|
45
|
+
Use --json to also see p95 and p99 duration percentiles and meta.window.
|
|
46
|
+
DESC
|
|
47
|
+
option :limit, type: :numeric, default: 25, desc: "Max records to return (1–500)"
|
|
48
|
+
option :offset, type: :numeric, default: 0, desc: "Number of records to skip (for pagination)"
|
|
49
|
+
option :since, type: :string, desc: "Window start (ISO 8601); rows then cover the window"
|
|
50
|
+
option :until, type: :string, desc: "Window end (ISO 8601)"
|
|
51
|
+
option :job, type: :string, desc: "Exact job class name"
|
|
52
|
+
option :status, type: :string, desc: "Filter by status — 'failed' returns only jobs with failures"
|
|
53
|
+
option :json, type: :boolean, default: false, desc: "Output raw JSON including meta envelope and p95/p99 durations"
|
|
54
|
+
def list
|
|
55
|
+
with_error_handling do
|
|
56
|
+
params = { limit: options[:limit], offset: options[:offset] }
|
|
57
|
+
%i[status since until job].each { |key| params[key] = options[key] if options[key] }
|
|
58
|
+
result = client.get("/jobs", params)
|
|
59
|
+
|
|
60
|
+
# JSON keeps lifetime counters and the window's stats apart, as the
|
|
61
|
+
# API does; the table shows whichever the rows describe.
|
|
62
|
+
next Formatter.render(result, json: true) if options[:json]
|
|
63
|
+
|
|
64
|
+
windowed = result["data"].any? { |job| job["stats"] }
|
|
65
|
+
rows = result["data"].map { |job| job.merge(job["stats"] || {}) }
|
|
66
|
+
Formatter.render(result.merge("data" => rows), columns: windowed ? WINDOW_COLUMNS : COLUMNS)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require_relative "configure"
|
|
3
|
+
require_relative "install"
|
|
4
|
+
require_relative "routes"
|
|
5
|
+
require_relative "requests"
|
|
6
|
+
require_relative "queries"
|
|
7
|
+
require_relative "jobs"
|
|
8
|
+
require_relative "job_runs"
|
|
9
|
+
require_relative "exceptions"
|
|
10
|
+
require_relative "deployments"
|
|
11
|
+
require_relative "coverage"
|
|
12
|
+
require_relative "insights"
|
|
13
|
+
require_relative "mcp"
|
|
14
|
+
|
|
15
|
+
module RailsPulse
|
|
16
|
+
module CLI
|
|
17
|
+
class Main < Thor
|
|
18
|
+
def self.exit_on_failure?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
register(Configure, "configure", "configure",
|
|
23
|
+
"Prompt for URL and API token, test the connection, and write ~/.rails-pulse")
|
|
24
|
+
register(Install, "install", "install [INTEGRATION]",
|
|
25
|
+
"Install AI agent integration files (claude, agents). Use --list to see options")
|
|
26
|
+
register(Routes, "routes", "routes SUBCOMMAND",
|
|
27
|
+
"List tracked HTTP routes and their tags")
|
|
28
|
+
register(Requests, "requests", "requests SUBCOMMAND",
|
|
29
|
+
"List recorded HTTP requests with optional status and time filters")
|
|
30
|
+
register(Queries, "queries", "queries SUBCOMMAND",
|
|
31
|
+
"List tracked SQL queries and their analysis results")
|
|
32
|
+
register(Jobs, "jobs", "jobs SUBCOMMAND",
|
|
33
|
+
"List background jobs with run counts, failure rates, and duration stats")
|
|
34
|
+
register(JobRuns, "job_runs", "job_runs SUBCOMMAND",
|
|
35
|
+
"List individual job runs with status, job, and time filters")
|
|
36
|
+
register(Exceptions, "exceptions", "exceptions SUBCOMMAND",
|
|
37
|
+
"List exception groups with status, search, and time filters")
|
|
38
|
+
register(Deployments, "deployments", "deployments SUBCOMMAND",
|
|
39
|
+
"List recorded deployments with optional time filters")
|
|
40
|
+
register(Coverage, "coverage", "coverage SUBCOMMAND",
|
|
41
|
+
"What has been recorded, how recently, and any collection gaps")
|
|
42
|
+
register(Insights, "insights", "insights SUBCOMMAND",
|
|
43
|
+
"What needs attention over one period, and whether the thresholds fit it")
|
|
44
|
+
register(Mcp, "mcp", "mcp",
|
|
45
|
+
"Start MCP server for AI coding agents (Claude Code, Codex, Cursor)")
|
|
46
|
+
|
|
47
|
+
no_commands do
|
|
48
|
+
def help(command = nil, subcommand = false)
|
|
49
|
+
super
|
|
50
|
+
return if command
|
|
51
|
+
|
|
52
|
+
say ""
|
|
53
|
+
say "Examples:"
|
|
54
|
+
say " rails-pulse configure # Set up credentials interactively"
|
|
55
|
+
say " rails-pulse routes list # List all tracked routes"
|
|
56
|
+
say " rails-pulse requests list --status 5xx --limit 10 # Last 10 server errors"
|
|
57
|
+
say " rails-pulse requests list --since 2026-06-01T00:00:00Z # Requests since a timestamp"
|
|
58
|
+
say " rails-pulse queries list --json # Queries with full analysis fields"
|
|
59
|
+
say " rails-pulse jobs list --status failed # Jobs with at least one failure"
|
|
60
|
+
say " rails-pulse jobs list --since 2026-06-01T00:00:00Z # Job stats for a window"
|
|
61
|
+
say " rails-pulse job_runs list --status failed --job ReportJob # Recent failed runs of one job"
|
|
62
|
+
say " rails-pulse exceptions list --status open # Open exception groups, most recent first"
|
|
63
|
+
say " rails-pulse exceptions show 42 # One group with backtraces and params"
|
|
64
|
+
say " rails-pulse deployments list # Recorded deploys, most recent first"
|
|
65
|
+
say " rails-pulse coverage show # What is recorded, how recently, any gaps"
|
|
66
|
+
say " rails-pulse insights show --period day # What needs attention yesterday, threshold fit"
|
|
67
|
+
say " rails-pulse install claude # Install Claude Code skill file"
|
|
68
|
+
say " rails-pulse mcp # Start MCP server for AI agents"
|
|
69
|
+
say ""
|
|
70
|
+
say "Credentials are read from RAILS_PULSE_URL / RAILS_PULSE_TOKEN env vars or ~/.rails-pulse."
|
|
71
|
+
say "Run 'rails-pulse configure' to set them up, or 'rails-pulse help COMMAND' for detailed flags."
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require_relative "base_command"
|
|
2
|
+
|
|
3
|
+
module RailsPulse
|
|
4
|
+
module CLI
|
|
5
|
+
class Mcp < BaseCommand
|
|
6
|
+
default_task :start
|
|
7
|
+
|
|
8
|
+
desc "start", "Start the Rails Pulse MCP server (stdio transport)"
|
|
9
|
+
long_desc <<~DESC
|
|
10
|
+
Starts a Model Context Protocol (MCP) server over stdio.
|
|
11
|
+
|
|
12
|
+
The MCP server exposes Rails Pulse performance data as structured tools
|
|
13
|
+
for AI coding agents (Claude Code, Codex, Cursor, etc.).
|
|
14
|
+
|
|
15
|
+
Configure your MCP client to run:
|
|
16
|
+
rails-pulse mcp
|
|
17
|
+
|
|
18
|
+
The server uses the same credentials as the CLI — set them with
|
|
19
|
+
`rails-pulse configure` or via RAILS_PULSE_URL / RAILS_PULSE_TOKEN
|
|
20
|
+
environment variables.
|
|
21
|
+
|
|
22
|
+
Available tools:
|
|
23
|
+
rails_pulse_routes — Discover endpoints with traffic and latency
|
|
24
|
+
rails_pulse_slow_requests — Slowest endpoints for a period
|
|
25
|
+
rails_pulse_errors — Recent errors grouped by endpoint
|
|
26
|
+
rails_pulse_endpoint — Deep profile of a single endpoint
|
|
27
|
+
rails_pulse_queries — Expensive and N+1 SQL queries
|
|
28
|
+
rails_pulse_jobs — Background job health and recent failures
|
|
29
|
+
rails_pulse_exceptions — Exception groups with status, count, and latest message
|
|
30
|
+
rails_pulse_exception — One group's recent occurrences with backtraces and params
|
|
31
|
+
rails_pulse_deployments — Recent deployments with revision, timing and metadata
|
|
32
|
+
rails_pulse_coverage — What has been recorded, how recently, and any collection gaps
|
|
33
|
+
DESC
|
|
34
|
+
def start
|
|
35
|
+
require_relative "../mcp/server"
|
|
36
|
+
RailsPulse::Mcp::Server.start!
|
|
37
|
+
rescue RailsPulse::CLI::Config::ConfigError => e
|
|
38
|
+
# Write errors to stderr — stdout is reserved for MCP JSON-RPC
|
|
39
|
+
$stderr.puts "Rails Pulse MCP error: #{e.message}"
|
|
40
|
+
exit 1
|
|
41
|
+
rescue RailsPulse::Mcp::Server::MissingDependencyError => e
|
|
42
|
+
$stderr.puts "Rails Pulse MCP error: #{e.message}"
|
|
43
|
+
exit 1
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require_relative "base_command"
|
|
2
|
+
require_relative "formatter"
|
|
3
|
+
|
|
4
|
+
module RailsPulse
|
|
5
|
+
module CLI
|
|
6
|
+
class Queries < BaseCommand
|
|
7
|
+
COLUMNS = [
|
|
8
|
+
[ "ID", 10, :id ],
|
|
9
|
+
[ "SQL", 60, :normalized_sql ],
|
|
10
|
+
[ "Analyzed", 25, :analyzed_at ]
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
STATS_COLUMNS = [
|
|
14
|
+
[ "ID", 8, :id ],
|
|
15
|
+
[ "SQL", 50, :normalized_sql ],
|
|
16
|
+
[ "Execs", 7, :executions ],
|
|
17
|
+
[ "Avg ms", 8, :avg_duration_ms ],
|
|
18
|
+
[ "Max ms", 8, :max_duration_ms ],
|
|
19
|
+
[ "Total ms", 10, :total_duration_ms ],
|
|
20
|
+
[ "N+1", 5, :max_repetition_count ]
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
desc "list", "List tracked SQL queries"
|
|
24
|
+
long_desc <<~DESC
|
|
25
|
+
Returns normalized SQL queries Rails Pulse has observed, ordered by ID.
|
|
26
|
+
|
|
27
|
+
Filter by the time window in which the query was executed (ISO 8601).
|
|
28
|
+
The filter matches against operation timestamps, not the query creation time,
|
|
29
|
+
and adds per-query timing stats for the window:
|
|
30
|
+
--since 2026-06-01T00:00:00Z
|
|
31
|
+
--until 2026-06-01T23:59:59Z
|
|
32
|
+
|
|
33
|
+
Order by a stat (defaults the window to the last 24 hours when --since is omitted):
|
|
34
|
+
--sort total_duration | avg_duration | executions | max_duration
|
|
35
|
+
|
|
36
|
+
Look up one query by its hashed_sql, as listed with --json or by Rails Pulse Cloud:
|
|
37
|
+
--hashed-sql e4d909c2290d0fb1ca068ffaddf22cbd
|
|
38
|
+
|
|
39
|
+
Use --json to see analysis fields (issues, suggestions, n_plus_one) in full.
|
|
40
|
+
DESC
|
|
41
|
+
option :limit, type: :numeric, default: 25, desc: "Max records to return (1–500)"
|
|
42
|
+
option :offset, type: :numeric, default: 0, desc: "Number of records to skip (for pagination)"
|
|
43
|
+
option :since, type: :string, desc: "Return queries executed at or after this time (ISO 8601)"
|
|
44
|
+
option :until, type: :string, desc: "Return queries executed at or before this time (ISO 8601)"
|
|
45
|
+
option :sort, type: :string, desc: "Order by total_duration, avg_duration, executions, or max_duration"
|
|
46
|
+
option :hashed_sql, type: :string, desc: "Exact hashed_sql of one query"
|
|
47
|
+
option :json, type: :boolean, default: false, desc: "Output raw JSON including meta envelope and analysis fields"
|
|
48
|
+
def list
|
|
49
|
+
with_error_handling do
|
|
50
|
+
params = { limit: options[:limit], offset: options[:offset] }
|
|
51
|
+
params[:since] = options[:since] if options[:since]
|
|
52
|
+
params[:until] = options[:until] if options[:until]
|
|
53
|
+
params[:sort] = options[:sort] if options[:sort]
|
|
54
|
+
params[:hashed_sql] = options[:hashed_sql] if options[:hashed_sql]
|
|
55
|
+
result = client.get("/queries", params)
|
|
56
|
+
|
|
57
|
+
if result["data"].any? { |q| q["stats"] }
|
|
58
|
+
rows = result["data"].map { |q| q.merge(q["stats"] || {}) }
|
|
59
|
+
Formatter.render(result.merge("data" => rows), json: options[:json], columns: STATS_COLUMNS)
|
|
60
|
+
else
|
|
61
|
+
Formatter.render(result, json: options[:json], columns: COLUMNS)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require_relative "base_command"
|
|
2
|
+
require_relative "formatter"
|
|
3
|
+
|
|
4
|
+
module RailsPulse
|
|
5
|
+
module CLI
|
|
6
|
+
class Requests < BaseCommand
|
|
7
|
+
COLUMNS = [
|
|
8
|
+
[ "ID", 10, :id ],
|
|
9
|
+
[ "Status", 6, :status ],
|
|
10
|
+
[ "Duration", 10, :duration ],
|
|
11
|
+
[ "Occurred", 25, :occurred_at ],
|
|
12
|
+
[ "Controller", 35, :controller_action ],
|
|
13
|
+
[ "Path", 35, :path ]
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
desc "list", "List recorded HTTP requests"
|
|
17
|
+
long_desc <<~DESC
|
|
18
|
+
Returns HTTP requests ordered by most recent first.
|
|
19
|
+
|
|
20
|
+
Filter by status code or class:
|
|
21
|
+
--status 500 exact code
|
|
22
|
+
--status 5xx any 5xx response
|
|
23
|
+
--status 4xx any 4xx response
|
|
24
|
+
|
|
25
|
+
Filter by the start of the path. A trailing * is ignored, so a Rails Pulse Cloud
|
|
26
|
+
path_prefix can be passed as it is; requests no route matched are stored under
|
|
27
|
+
their raw path:
|
|
28
|
+
--path-prefix /wp-admin/ --status 404
|
|
29
|
+
|
|
30
|
+
Filter by time window (ISO 8601):
|
|
31
|
+
--since 2026-06-01T00:00:00Z
|
|
32
|
+
--until 2026-06-01T23:59:59Z
|
|
33
|
+
|
|
34
|
+
Both --since and --until can be combined.
|
|
35
|
+
Use --json to get the full response envelope including meta.total.
|
|
36
|
+
DESC
|
|
37
|
+
option :limit, type: :numeric, default: 25, desc: "Max records to return (1–500)"
|
|
38
|
+
option :offset, type: :numeric, default: 0, desc: "Number of records to skip (for pagination)"
|
|
39
|
+
option :since, type: :string, desc: "Return requests at or after this time (ISO 8601)"
|
|
40
|
+
option :until, type: :string, desc: "Return requests at or before this time (ISO 8601)"
|
|
41
|
+
option :status, type: :string, desc: "Filter by status code or class (e.g. 500, 5xx, 4xx)"
|
|
42
|
+
option :path_prefix, type: :string, desc: "Only paths starting with this (e.g. /wp-admin/)"
|
|
43
|
+
option :json, type: :boolean, default: false, desc: "Output raw JSON including meta envelope"
|
|
44
|
+
def list
|
|
45
|
+
with_error_handling do
|
|
46
|
+
params = { limit: options[:limit], offset: options[:offset] }
|
|
47
|
+
params[:since] = options[:since] if options[:since]
|
|
48
|
+
params[:until] = options[:until] if options[:until]
|
|
49
|
+
params[:status] = options[:status] if options[:status]
|
|
50
|
+
params[:path_prefix] = options[:path_prefix] if options[:path_prefix]
|
|
51
|
+
result = client.get("/requests", params)
|
|
52
|
+
Formatter.render(result, json: options[:json], columns: COLUMNS)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require_relative "base_command"
|
|
2
|
+
require_relative "formatter"
|
|
3
|
+
|
|
4
|
+
module RailsPulse
|
|
5
|
+
module CLI
|
|
6
|
+
class Routes < BaseCommand
|
|
7
|
+
COLUMNS = [
|
|
8
|
+
[ "Method", 8, :http_methods ],
|
|
9
|
+
[ "Path", 40, :path ],
|
|
10
|
+
[ "Action", 35, :controller_action ],
|
|
11
|
+
[ "Created", 25, :created_at ]
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
STATS_COLUMNS = [
|
|
15
|
+
[ "Method", 8, :http_methods ],
|
|
16
|
+
[ "Path", 40, :path ],
|
|
17
|
+
[ "Action", 35, :controller_action ],
|
|
18
|
+
[ "Requests", 8, :request_count ],
|
|
19
|
+
[ "Avg ms", 8, :avg_duration_ms ],
|
|
20
|
+
[ "Errors", 6, :error_count ]
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
desc "list", "List all tracked HTTP routes"
|
|
24
|
+
long_desc <<~DESC
|
|
25
|
+
Returns every route Rails Pulse has recorded, ordered by path.
|
|
26
|
+
|
|
27
|
+
Search by path or controller action (case-insensitive substring):
|
|
28
|
+
--search checkout
|
|
29
|
+
|
|
30
|
+
Match one route exactly by its path and controller action:
|
|
31
|
+
--path /orders/:id --controller-action orders#show
|
|
32
|
+
|
|
33
|
+
Add request stats for a time window (ISO 8601). Only routes with traffic
|
|
34
|
+
in the window are returned, ordered by request count:
|
|
35
|
+
--since 2026-06-01T00:00:00Z
|
|
36
|
+
--until 2026-06-01T23:59:59Z
|
|
37
|
+
|
|
38
|
+
Order stats by another column (defaults the window to the last 24 hours when --since is omitted):
|
|
39
|
+
--sort request_count | avg_duration | error_count
|
|
40
|
+
|
|
41
|
+
Use --limit and --offset to paginate through large route sets.
|
|
42
|
+
Use --json to get the full response envelope including meta.total.
|
|
43
|
+
DESC
|
|
44
|
+
option :limit, type: :numeric, default: 25, desc: "Max records to return (1–500)"
|
|
45
|
+
option :offset, type: :numeric, default: 0, desc: "Number of records to skip (for pagination)"
|
|
46
|
+
option :since, type: :string, desc: "Include request stats at or after this time (ISO 8601)"
|
|
47
|
+
option :until, type: :string, desc: "Include request stats at or before this time (ISO 8601)"
|
|
48
|
+
option :search, type: :string, desc: "Filter by path or controller action substring"
|
|
49
|
+
option :path, type: :string, desc: "Exact route path"
|
|
50
|
+
option :controller_action, type: :string, desc: "Exact controller action"
|
|
51
|
+
option :sort, type: :string, desc: "Order stats by request_count, avg_duration, or error_count"
|
|
52
|
+
option :json, type: :boolean, default: false, desc: "Output raw JSON including meta envelope"
|
|
53
|
+
def list
|
|
54
|
+
with_error_handling do
|
|
55
|
+
params = { limit: options[:limit], offset: options[:offset] }
|
|
56
|
+
params[:since] = options[:since] if options[:since]
|
|
57
|
+
params[:until] = options[:until] if options[:until]
|
|
58
|
+
params[:search] = options[:search] if options[:search]
|
|
59
|
+
params[:path] = options[:path] if options[:path]
|
|
60
|
+
params[:controller_action] = options[:controller_action] if options[:controller_action]
|
|
61
|
+
params[:sort] = options[:sort] if options[:sort]
|
|
62
|
+
result = client.get("/routes", params)
|
|
63
|
+
|
|
64
|
+
rows = result["data"].map do |route|
|
|
65
|
+
route.merge("http_methods" => Array(route["http_methods"]).join("|")).merge(route["stats"] || {})
|
|
66
|
+
end
|
|
67
|
+
columns = result["data"].any? { |r| r["stats"] } ? STATS_COLUMNS : COLUMNS
|
|
68
|
+
Formatter.render(result.merge("data" => rows), json: options[:json], columns: columns)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|