rails_console_pro 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec_status +261 -240
- data/CHANGELOG.md +4 -0
- data/QUICK_START.md +8 -0
- data/README.md +16 -0
- data/docs/FORMATTING.md +5 -0
- data/docs/MODEL_STATISTICS.md +4 -0
- data/docs/OBJECT_DIFFING.md +6 -0
- data/docs/PROFILING.md +91 -0
- data/docs/QUEUE_INSIGHTS.md +82 -0
- data/docs/SCHEMA_INSPECTION.md +5 -0
- data/docs/SNIPPETS.md +71 -0
- data/lib/rails_console_pro/commands/jobs_command.rb +212 -0
- data/lib/rails_console_pro/commands/profile_command.rb +84 -0
- data/lib/rails_console_pro/commands/snippets_command.rb +141 -0
- data/lib/rails_console_pro/commands.rb +15 -0
- data/lib/rails_console_pro/configuration.rb +39 -0
- data/lib/rails_console_pro/format_exporter.rb +8 -0
- data/lib/rails_console_pro/global_methods.rb +12 -0
- data/lib/rails_console_pro/initializer.rb +23 -0
- data/lib/rails_console_pro/model_validator.rb +1 -1
- data/lib/rails_console_pro/printers/profile_printer.rb +180 -0
- data/lib/rails_console_pro/printers/queue_insights_printer.rb +150 -0
- data/lib/rails_console_pro/printers/snippet_collection_printer.rb +68 -0
- data/lib/rails_console_pro/printers/snippet_printer.rb +64 -0
- data/lib/rails_console_pro/profile_result.rb +109 -0
- data/lib/rails_console_pro/pry_commands.rb +106 -0
- data/lib/rails_console_pro/queue_insights_result.rb +110 -0
- data/lib/rails_console_pro/serializers/profile_serializer.rb +73 -0
- data/lib/rails_console_pro/services/profile_collector.rb +245 -0
- data/lib/rails_console_pro/services/queue_action_service.rb +176 -0
- data/lib/rails_console_pro/services/queue_insight_fetcher.rb +600 -0
- data/lib/rails_console_pro/services/snippet_repository.rb +191 -0
- data/lib/rails_console_pro/snippets/collection_result.rb +44 -0
- data/lib/rails_console_pro/snippets/single_result.rb +30 -0
- data/lib/rails_console_pro/snippets/snippet.rb +112 -0
- data/lib/rails_console_pro/snippets.rb +12 -0
- data/lib/rails_console_pro/version.rb +1 -1
- data/rails_console_pro.gemspec +1 -1
- metadata +26 -8
data/QUICK_START.md
CHANGED
|
@@ -59,6 +59,13 @@ export schema(User) user_schema.json
|
|
|
59
59
|
```
|
|
60
60
|
[Learn more →](docs/EXPORT.md)
|
|
61
61
|
|
|
62
|
+
### Snippet Library
|
|
63
|
+
```ruby
|
|
64
|
+
snippets(:add, "User.where(active: true).count", description: "Active users", tags: %w[users metrics])
|
|
65
|
+
snippets(:list)
|
|
66
|
+
```
|
|
67
|
+
[Learn more →](docs/SNIPPETS.md)
|
|
68
|
+
|
|
62
69
|
### Beautiful Formatting
|
|
63
70
|
```ruby
|
|
64
71
|
User.first # Automatically formatted with colors
|
|
@@ -103,6 +110,7 @@ rails generate rails_console_pro:install
|
|
|
103
110
|
- [Association Navigation](docs/ASSOCIATION_NAVIGATION.md)
|
|
104
111
|
- [Object Diffing](docs/OBJECT_DIFFING.md)
|
|
105
112
|
- [Export](docs/EXPORT.md)
|
|
113
|
+
- [Snippets](docs/SNIPPETS.md)
|
|
106
114
|
- [Formatting](docs/FORMATTING.md)
|
|
107
115
|
|
|
108
116
|
## Need Help?
|
data/README.md
CHANGED
|
@@ -14,9 +14,12 @@ Rails Console Pro transforms your Rails console into a powerful debugging enviro
|
|
|
14
14
|
- 🔍 **SQL Explain** - Analyze query execution plans with performance recommendations
|
|
15
15
|
- 🧭 **Association Navigator** - Interactive navigation through model associations
|
|
16
16
|
- 📈 **Model Statistics** - Record counts, growth rates, table sizes, and index usage
|
|
17
|
+
- 🔬 **Adaptive Profiling** - Profile blocks or relations with query, cache, and performance metrics
|
|
18
|
+
- 🧵 **ActiveJob Insights** - Inspect and manage queues across adapters (Sidekiq, SolidQueue, Test, Async) with filters and inline actions
|
|
17
19
|
- 🔄 **Object Diffing** - Compare ActiveRecord objects and highlight differences
|
|
18
20
|
- 💾 **Export Capabilities** - Export to JSON, YAML, and HTML formats
|
|
19
21
|
- 📄 **Smart Pagination** - Automatic pagination for large collections
|
|
22
|
+
- 📝 **Snippet Library** - Capture, search, and reuse console snippets across sessions
|
|
20
23
|
|
|
21
24
|
## 🚀 Installation
|
|
22
25
|
|
|
@@ -62,6 +65,15 @@ diff user1, user2
|
|
|
62
65
|
|
|
63
66
|
# Export
|
|
64
67
|
export schema(User) user_schema.json
|
|
68
|
+
|
|
69
|
+
# Profiling
|
|
70
|
+
profile('Load users') { User.active.includes(:posts).limit(10).to_a }
|
|
71
|
+
|
|
72
|
+
# Queue insights
|
|
73
|
+
jobs(limit: 10, queue: 'mailers')
|
|
74
|
+
jobs status=retry class=ReminderJob
|
|
75
|
+
jobs retry=abcdef123456
|
|
76
|
+
jobs details=abcdef123456
|
|
65
77
|
```
|
|
66
78
|
|
|
67
79
|
See [QUICK_START.md](QUICK_START.md) for more examples and detailed documentation for each feature.
|
|
@@ -78,6 +90,7 @@ RailsConsolePro.configure do |config|
|
|
|
78
90
|
config.schema_command_enabled = true
|
|
79
91
|
config.explain_command_enabled = true
|
|
80
92
|
config.stats_command_enabled = true
|
|
93
|
+
config.queue_command_enabled = true
|
|
81
94
|
|
|
82
95
|
# Color scheme
|
|
83
96
|
config.color_scheme = :dark # or :light
|
|
@@ -107,7 +120,10 @@ end
|
|
|
107
120
|
- [Association Navigation](docs/ASSOCIATION_NAVIGATION.md) - Navigate model associations
|
|
108
121
|
- [Object Diffing](docs/OBJECT_DIFFING.md) - Compare objects
|
|
109
122
|
- [Export](docs/EXPORT.md) - Export to JSON, YAML, HTML
|
|
123
|
+
- [Snippets](docs/SNIPPETS.md) - Build a reusable console snippet library
|
|
110
124
|
- [Formatting](docs/FORMATTING.md) - Beautiful console output
|
|
125
|
+
- [Adaptive Profiling](docs/PROFILING.md) - Measure queries, cache hits, and potential N+1 issues
|
|
126
|
+
- [Queue Insights](docs/QUEUE_INSIGHTS.md) - Inspect jobs across ActiveJob adapters
|
|
111
127
|
|
|
112
128
|
## 🤝 Contributing
|
|
113
129
|
|
data/docs/FORMATTING.md
CHANGED
|
@@ -84,3 +84,8 @@ end
|
|
|
84
84
|
- **Clean Layout**: Well-organized, readable output
|
|
85
85
|
- **Dark/Light Themes**: Choose your preferred color scheme
|
|
86
86
|
|
|
87
|
+
## Screenshots
|
|
88
|
+
|
|
89
|
+
<img width="1117" height="551" alt="Screenshot 2025-11-07 at 11 42 52 AM" src="https://github.com/user-attachments/assets/2e77e0d5-8fa1-4249-8b0f-f8d16e829d99" />
|
|
90
|
+
|
|
91
|
+
|
data/docs/MODEL_STATISTICS.md
CHANGED
|
@@ -70,3 +70,7 @@ export stats(User) user_stats.json
|
|
|
70
70
|
- **Index Usage**: Which indexes are used frequently
|
|
71
71
|
- **Column Statistics**: Unique values, distributions, ranges (for smaller tables)
|
|
72
72
|
|
|
73
|
+
## Screenshots
|
|
74
|
+
|
|
75
|
+
<img width="1119" height="714" alt="Screenshot 2025-11-07 at 11 44 01 AM" src="https://github.com/user-attachments/assets/a175cb8c-1bea-4819-a80b-3f0bbb0d1a75" />
|
|
76
|
+
|
data/docs/OBJECT_DIFFING.md
CHANGED
|
@@ -85,3 +85,9 @@ diff(user1, user2).to_json
|
|
|
85
85
|
- **Multiple Object Types**: Works with ActiveRecord, Hash, and more
|
|
86
86
|
- **Export Support**: Export diff results to JSON/YAML/HTML
|
|
87
87
|
|
|
88
|
+
## Screenshots
|
|
89
|
+
|
|
90
|
+
<img width="1057" height="650" alt="Screenshot 2025-11-07 at 11 27 23 AM" src="https://github.com/user-attachments/assets/ca9fce82-8ed8-4506-907b-75e07735ec66" />
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
data/docs/PROFILING.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Adaptive Profiling
|
|
2
|
+
|
|
3
|
+
Rails Console Pro includes an adaptive profiler that wraps any block, callable, or ActiveRecord relation and reports real-time performance metrics without leaving the console.
|
|
4
|
+
|
|
5
|
+
## Why Use It?
|
|
6
|
+
|
|
7
|
+
- Understand how long a console experiment takes end-to-end
|
|
8
|
+
- See how much time is spent inside SQL queries
|
|
9
|
+
- Detect cache hits/misses and duplicated queries (potential N+1s)
|
|
10
|
+
- Keep a sample of executed SQL statements with bind values
|
|
11
|
+
- Capture errors while still getting timing information
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# Profile a block
|
|
17
|
+
profile { User.active.limit(25).to_a }
|
|
18
|
+
|
|
19
|
+
# Add a label for the session
|
|
20
|
+
profile('Load active users') { User.active.includes(:posts).limit(25).to_a }
|
|
21
|
+
|
|
22
|
+
# Profile a relation (loads it automatically)
|
|
23
|
+
relation = User.includes(:posts).limit(10)
|
|
24
|
+
profile relation
|
|
25
|
+
|
|
26
|
+
# Profile any callable object
|
|
27
|
+
profile -> { HeavyService.call(user) }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`profile` returns a `RailsConsolePro::ProfileResult` instance, so you can further inspect or export the collected metrics.
|
|
31
|
+
|
|
32
|
+
## Sample Output
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
🧪 PROFILE: Load active users
|
|
36
|
+
⏱ Execution Summary:
|
|
37
|
+
Total time 35.42 ms
|
|
38
|
+
SQL time 28.12 ms
|
|
39
|
+
(79.36% of total time spent in SQL)
|
|
40
|
+
|
|
41
|
+
🗂 Query Breakdown:
|
|
42
|
+
Total queries 4
|
|
43
|
+
Read queries 4
|
|
44
|
+
Write queries 0
|
|
45
|
+
Cached queries 1
|
|
46
|
+
|
|
47
|
+
🐢 Slow Queries (100.0ms+):
|
|
48
|
+
1. 120.44 ms SELECT "users".* FROM ...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The printer also highlights cache activity, sample queries, potential N+1 issues, and any error raised during execution.
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
Tune profiling behaviour via the initializer:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
RailsConsolePro.configure do |config|
|
|
59
|
+
# Enable/disable the profile command
|
|
60
|
+
config.profile_command_enabled = true
|
|
61
|
+
|
|
62
|
+
# Flag queries above this threshold (milliseconds)
|
|
63
|
+
config.profile_slow_query_threshold = 120.0
|
|
64
|
+
|
|
65
|
+
# Minimum occurrences before a query is treated as a possible N+1
|
|
66
|
+
config.profile_duplicate_query_threshold = 3
|
|
67
|
+
|
|
68
|
+
# Number of query samples to keep in memory for reporting
|
|
69
|
+
config.profile_max_saved_queries = 10
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Exporting
|
|
74
|
+
|
|
75
|
+
Profile results can be exported like any other value object:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
result = profile { User.active.limit(10).to_a }
|
|
79
|
+
|
|
80
|
+
result.to_json
|
|
81
|
+
result.to_yaml
|
|
82
|
+
result.export_to_file('profile.html', format: :html)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tips
|
|
86
|
+
|
|
87
|
+
- Combine profiling with Rails scopes to analyse real data paths
|
|
88
|
+
- Lower `profile_slow_query_threshold` when testing on local databases
|
|
89
|
+
- Use labels to differentiate multiple runs in the same console session
|
|
90
|
+
- Errors are captured and displayed; you still get timings even when the block fails
|
|
91
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Queue Insights
|
|
2
|
+
|
|
3
|
+
Rails Console Pro introduces a `jobs` command that surfaces ActiveJob activity without leaving the console. It aggregates state from the underlying queue adapter so you can quickly review what is enqueued, retrying, or currently executing.
|
|
4
|
+
|
|
5
|
+
## Supported Adapters
|
|
6
|
+
|
|
7
|
+
The command automatically detects the adapter backing `ActiveJob::Base.queue_adapter` and falls back gracefully when a feature is unavailable.
|
|
8
|
+
|
|
9
|
+
| Adapter | Enqueued | Retries | Recent Executions | Notes |
|
|
10
|
+
| ------- | -------- | ------- | ----------------- | ----- |
|
|
11
|
+
| Sidekiq | ✅ | ✅ (`RetrySet`) | ✅ (`Workers`) | Requires Redis connection |
|
|
12
|
+
| SolidQueue | ✅ (`ready`) | ✅ (`retryable`/`failed`) | ✅ (`Execution`/`CompletedExecution`) | Works with default SolidQueue tables |
|
|
13
|
+
| Test / Inline / Async | ✅ (`enqueued_jobs`) | ❌ | ✅ (`performed_jobs` when available) | Primarily for development/test |
|
|
14
|
+
|
|
15
|
+
> **Tip:** When an adapter does not expose a given dataset, the section is omitted and a warning is printed where appropriate.
|
|
16
|
+
>
|
|
17
|
+
> When Sidekiq or SolidQueue are loaded but ActiveJob is still using the default async adapter, Rails Console Pro automatically falls back to the native queue APIs so you still see those jobs. For Sidekiq, ensure `sidekiq/api` is required in the console (most Rails apps do this automatically). Inline actions (`retry=`, `delete=`, `details=`) currently target Sidekiq queues.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# Everything (auto-detected adapter)
|
|
23
|
+
jobs
|
|
24
|
+
|
|
25
|
+
# Limit the number of entries
|
|
26
|
+
jobs(limit: 10)
|
|
27
|
+
|
|
28
|
+
# Focus on a specific queue (if the adapter supports it)
|
|
29
|
+
jobs(queue: "mailers")
|
|
30
|
+
|
|
31
|
+
# Combine options
|
|
32
|
+
jobs(limit: 5, queue: "critical")
|
|
33
|
+
|
|
34
|
+
# Filter by status or job class
|
|
35
|
+
jobs status=retry
|
|
36
|
+
jobs status=enqueued,retry class=ReminderJob
|
|
37
|
+
|
|
38
|
+
# Inline queue actions
|
|
39
|
+
jobs retry=abcdef123456
|
|
40
|
+
jobs delete=abcdef123456
|
|
41
|
+
jobs details=abcdef123456
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
In Pry you can use CLI-style arguments:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
jobs limit=5 queue=critical
|
|
48
|
+
jobs 10 mailers
|
|
49
|
+
jobs status=retry class=ReminderJob
|
|
50
|
+
jobs --retry-only
|
|
51
|
+
jobs retry=abcdef123456
|
|
52
|
+
jobs delete=abcdef123456
|
|
53
|
+
jobs details=abcdef123456
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Output
|
|
57
|
+
|
|
58
|
+
The printer renders three sections when data is available:
|
|
59
|
+
|
|
60
|
+
1. **📬 Enqueued Jobs** – Most recent enqueued jobs with timestamps and arguments.
|
|
61
|
+
2. **🔁 Retry Set** – Jobs scheduled for retry or marked as failed.
|
|
62
|
+
3. **⚙️ Recent Executions** – Currently running jobs (Sidekiq) or recently performed jobs (other adapters).
|
|
63
|
+
|
|
64
|
+
Adapter statistics (e.g., processed, failed) appear under **ℹ️ Adapter Stats** when provided by the backend.
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
The feature is enabled by default. Toggle it in your initializer if needed:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
RailsConsolePro.configure do |config|
|
|
72
|
+
config.queue_command_enabled = true # or false to disable
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Troubleshooting
|
|
77
|
+
|
|
78
|
+
- **Missing data:** Ensure the backing queue system is reachable (e.g., Redis for Sidekiq).
|
|
79
|
+
- **Adapter-specific warnings:** These indicate the adapter API is unavailable or the command lacks sufficient access. The command continues with available sections.
|
|
80
|
+
- **Custom adapters:** The command falls back to ActiveJob's generic `enqueued_jobs` / `performed_jobs` APIs when present.
|
|
81
|
+
|
|
82
|
+
|
data/docs/SCHEMA_INSPECTION.md
CHANGED
|
@@ -57,4 +57,9 @@ schema(User).to_json
|
|
|
57
57
|
# Export to file
|
|
58
58
|
export schema(User) user_schema.json
|
|
59
59
|
```
|
|
60
|
+
## Screenshots
|
|
60
61
|
|
|
62
|
+
|
|
63
|
+
<img width="1114" height="722" alt="Screenshot 2025-11-07 at 11 27 52 AM" src="https://github.com/user-attachments/assets/ab93a9b9-4b59-4032-b15e-06fe1ef069c6" />
|
|
64
|
+
|
|
65
|
+
<img width="1107" height="498" alt="Screenshot 2025-11-07 at 11 28 01 AM" src="https://github.com/user-attachments/assets/27f9e85a-8e5d-48f6-9a75-b69fde3dbb5c" />
|
data/docs/SNIPPETS.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Snippet Library
|
|
2
|
+
|
|
3
|
+
Rails Console Pro ships with a developer-friendly snippet library that helps you capture, search, and reuse the console commands you rely on every day.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
- Store frequently used console expressions with a short description and optional tags
|
|
8
|
+
- Persist snippets across console sessions (default path: `tmp/rails_console_pro/snippets.yml`)
|
|
9
|
+
- Fuzzy search by text or tags to quickly locate prior commands
|
|
10
|
+
- Mark favorites for even faster recall
|
|
11
|
+
- Use Ruby blocks to add multi-line snippets with nice formatting
|
|
12
|
+
|
|
13
|
+
## Quick Actions
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# Capture a snippet
|
|
17
|
+
snippets(:add, "User.where(active: true).count", description: "Active users", tags: %w[users metrics])
|
|
18
|
+
|
|
19
|
+
# Capture multiline snippet with a block
|
|
20
|
+
snippets(:add, description: "Backfill user slugs") do
|
|
21
|
+
<<~RUBY
|
|
22
|
+
User.where(slug: nil).find_each do |user|
|
|
23
|
+
user.update!(slug: user.name.parameterize)
|
|
24
|
+
end
|
|
25
|
+
RUBY
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# List (defaults to the 10 most recent)
|
|
29
|
+
snippets(:list)
|
|
30
|
+
|
|
31
|
+
# Search by keyword
|
|
32
|
+
snippets(:search, "active users")
|
|
33
|
+
|
|
34
|
+
# Search by tags
|
|
35
|
+
snippets(:search, tags: %w[metrics])
|
|
36
|
+
|
|
37
|
+
# Mark a favorite and list favorites
|
|
38
|
+
snippets(:favorite, "active-users")
|
|
39
|
+
snippets(:list, favorites: true)
|
|
40
|
+
|
|
41
|
+
# Show a specific snippet with full body
|
|
42
|
+
snippets(:show, "active-users")
|
|
43
|
+
|
|
44
|
+
# Remove a snippet
|
|
45
|
+
snippets(:delete, "active-users")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
You can adjust snippet behavior via the standard configuration block:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
RailsConsolePro.configure do |config|
|
|
54
|
+
# Disable / enable the snippet commands
|
|
55
|
+
config.snippets_command_enabled = true
|
|
56
|
+
|
|
57
|
+
# Override the storage path if you prefer a shared location
|
|
58
|
+
config.snippet_store_path = Rails.root.join("tmp", "rails_console_pro", "snippets.yml")
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If you want to share snippets across a team, point `snippet_store_path` to a shared directory (for example within your repo) and commit the file if appropriate.
|
|
63
|
+
|
|
64
|
+
## Tips
|
|
65
|
+
|
|
66
|
+
- Use meaningful IDs (`id:` option) to recall snippets by name (`snippets(:show, "backfill-slugs")`)
|
|
67
|
+
- Tag by model or domain (`tags: %w[user data-migrations]`) to quickly filter the list
|
|
68
|
+
- Keep destructive snippets safe by adding warnings to the description or tags (`tags: %w[danger prod-only]`)
|
|
69
|
+
- Since snippets are plain Ruby strings, you can paste them directly back into the console when you need them
|
|
70
|
+
|
|
71
|
+
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
class JobsCommand < BaseCommand
|
|
6
|
+
DEFAULT_LIMIT = 20
|
|
7
|
+
|
|
8
|
+
def execute(options = nil)
|
|
9
|
+
unless feature_enabled?
|
|
10
|
+
puts pastel.yellow("Jobs command is disabled. Enable it with: RailsConsolePro.configure { |c| c.queue_command_enabled = true }")
|
|
11
|
+
return nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
unless active_job_available?
|
|
15
|
+
puts pastel.red("ActiveJob is not loaded. Queue insights require ActiveJob to be available.")
|
|
16
|
+
return nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
normalized_options = normalize_options(options)
|
|
20
|
+
|
|
21
|
+
fetch_options, filter_options, action_options = split_options(normalized_options)
|
|
22
|
+
action_results = perform_actions(action_options, fetch_options)
|
|
23
|
+
action_results.each { |result| print_action_result(result) }
|
|
24
|
+
|
|
25
|
+
result = fetcher.fetch(**fetch_options)
|
|
26
|
+
return result unless result
|
|
27
|
+
|
|
28
|
+
apply_filters(result, filter_options)
|
|
29
|
+
rescue => e
|
|
30
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :jobs)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def feature_enabled?
|
|
36
|
+
RailsConsolePro.config.queue_command_enabled
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def active_job_available?
|
|
40
|
+
defined?(ActiveJob::Base)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fetcher
|
|
44
|
+
@fetcher ||= Services::QueueInsightFetcher.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def action_service
|
|
48
|
+
@action_service ||= Services::QueueActionService.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def perform_actions(action_options, fetch_options)
|
|
52
|
+
return [] if action_options.empty?
|
|
53
|
+
|
|
54
|
+
action_options.each_with_object([]) do |(action_key, value), results|
|
|
55
|
+
next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
56
|
+
|
|
57
|
+
result = action_service.perform(
|
|
58
|
+
action: action_key,
|
|
59
|
+
jid: value,
|
|
60
|
+
queue: fetch_options[:queue]
|
|
61
|
+
)
|
|
62
|
+
results << result if result
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply_filters(result, filter_options)
|
|
67
|
+
filters = prepare_filters(filter_options)
|
|
68
|
+
filtered_enqueued = result.enqueued_jobs
|
|
69
|
+
filtered_retry = result.retry_jobs
|
|
70
|
+
filtered_recent = result.recent_executions
|
|
71
|
+
additional_warnings = []
|
|
72
|
+
|
|
73
|
+
statuses = filters[:statuses]
|
|
74
|
+
unless statuses.empty?
|
|
75
|
+
show_enqueued = statuses.include?(:enqueued) || statuses.include?(:scheduled) || statuses.include?(:all)
|
|
76
|
+
show_retry = statuses.include?(:retry) || statuses.include?(:retries) || statuses.include?(:all)
|
|
77
|
+
show_recent = statuses.include?(:recent) || statuses.include?(:executing) || statuses.include?(:running) || statuses.include?(:all)
|
|
78
|
+
|
|
79
|
+
filtered_enqueued = [] unless show_enqueued
|
|
80
|
+
filtered_retry = [] unless show_retry
|
|
81
|
+
filtered_recent = [] unless show_recent
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if filters[:job_class]
|
|
85
|
+
matcher = filters[:job_class]
|
|
86
|
+
filtered_enqueued = filtered_enqueued.select { |job| job_class_matches?(job, matcher) }
|
|
87
|
+
filtered_retry = filtered_retry.select { |job| job_class_matches?(job, matcher) }
|
|
88
|
+
filtered_recent = filtered_recent.select { |job| job_class_matches?(job, matcher) }
|
|
89
|
+
if filtered_enqueued.empty? && filtered_retry.empty? && filtered_recent.empty?
|
|
90
|
+
additional_warnings << "No jobs matching class filter '#{matcher}'."
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
filtered_result = result.with_overrides(
|
|
95
|
+
enqueued_jobs: filtered_enqueued,
|
|
96
|
+
retry_jobs: filtered_retry,
|
|
97
|
+
recent_executions: filtered_recent,
|
|
98
|
+
warnings: result.warnings + additional_warnings
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
filtered_result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def normalize_options(options)
|
|
105
|
+
case options
|
|
106
|
+
when Numeric
|
|
107
|
+
{ limit: options.to_i }
|
|
108
|
+
when Hash
|
|
109
|
+
symbolized = options.each_with_object({}) do |(key, value), acc|
|
|
110
|
+
sym_key = key.to_sym
|
|
111
|
+
sym_key = :job_class if sym_key == :class
|
|
112
|
+
acc[sym_key] = value
|
|
113
|
+
end
|
|
114
|
+
symbolized
|
|
115
|
+
when nil
|
|
116
|
+
{}
|
|
117
|
+
else
|
|
118
|
+
{}
|
|
119
|
+
end.then do |opts|
|
|
120
|
+
limit = opts.fetch(:limit, DEFAULT_LIMIT)
|
|
121
|
+
limit = limit.to_i
|
|
122
|
+
limit = DEFAULT_LIMIT if limit <= 0
|
|
123
|
+
opts[:limit] = limit
|
|
124
|
+
opts[:job_class] = opts[:job_class].to_s.strip if opts.key?(:job_class) && opts[:job_class]
|
|
125
|
+
[:retry, :delete, :details].each do |key|
|
|
126
|
+
opts[key] = opts[key].to_s.strip if opts.key?(key) && opts[key]
|
|
127
|
+
end
|
|
128
|
+
opts
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def split_options(options)
|
|
133
|
+
fetch_options = {
|
|
134
|
+
limit: options[:limit],
|
|
135
|
+
queue: options[:queue]
|
|
136
|
+
}.compact
|
|
137
|
+
|
|
138
|
+
filter_options = {
|
|
139
|
+
statuses: options[:statuses] || options[:status],
|
|
140
|
+
job_class: options[:job_class]
|
|
141
|
+
}.compact
|
|
142
|
+
|
|
143
|
+
action_options = {
|
|
144
|
+
retry: options[:retry],
|
|
145
|
+
delete: options[:delete],
|
|
146
|
+
details: options[:details]
|
|
147
|
+
}.compact
|
|
148
|
+
|
|
149
|
+
[fetch_options, filter_options, action_options]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def prepare_filters(filter_options)
|
|
153
|
+
statuses =
|
|
154
|
+
Array(filter_options[:statuses]).flat_map { |s| s.to_s.split(',') }
|
|
155
|
+
.map(&:strip)
|
|
156
|
+
.reject(&:empty?)
|
|
157
|
+
.map { |s| s.downcase.to_sym }
|
|
158
|
+
.uniq
|
|
159
|
+
|
|
160
|
+
{
|
|
161
|
+
statuses: statuses,
|
|
162
|
+
job_class: filter_options[:job_class],
|
|
163
|
+
limit: filter_options[:limit]
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def job_class_matches?(job, matcher)
|
|
168
|
+
job_class = job.job_class.to_s
|
|
169
|
+
return false if job_class.empty?
|
|
170
|
+
|
|
171
|
+
matcher_str = matcher.to_s
|
|
172
|
+
job_class.casecmp?(matcher_str) ||
|
|
173
|
+
job_class.split('::').last.casecmp?(matcher_str) ||
|
|
174
|
+
job_class.include?(matcher_str)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def print_action_result(result)
|
|
178
|
+
return unless result
|
|
179
|
+
|
|
180
|
+
if result.warning
|
|
181
|
+
puts pastel.yellow("⚠️ #{result.warning}")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if result.message
|
|
185
|
+
color_method = result.success ? :green : :cyan
|
|
186
|
+
puts pastel.public_send(color_method, result.message)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if result.details
|
|
190
|
+
puts pastel.cyan("Details:")
|
|
191
|
+
formatted = format_details(result.details)
|
|
192
|
+
puts formatted
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def format_details(details)
|
|
197
|
+
case details
|
|
198
|
+
when String
|
|
199
|
+
" #{details}"
|
|
200
|
+
when Hash
|
|
201
|
+
details.map { |key, value| " #{key}: #{value.inspect}" }.join("\n")
|
|
202
|
+
when Array
|
|
203
|
+
details.map { |value| " - #{value.inspect}" }.join("\n")
|
|
204
|
+
else
|
|
205
|
+
details.inspect
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
# Command for profiling arbitrary blocks, callables, or ActiveRecord relations
|
|
6
|
+
class ProfileCommand < BaseCommand
|
|
7
|
+
def execute(target = nil, *args, label: nil, &block)
|
|
8
|
+
return disabled_message unless enabled?
|
|
9
|
+
|
|
10
|
+
label, target, block = normalize_arguments(label, target, block)
|
|
11
|
+
execution = build_execution(target, args, block)
|
|
12
|
+
return execution if execution.nil? || execution.is_a?(String)
|
|
13
|
+
|
|
14
|
+
collector = Services::ProfileCollector.new(config)
|
|
15
|
+
collector.profile(label: label || execution[:label]) do
|
|
16
|
+
execution[:callable].call
|
|
17
|
+
end
|
|
18
|
+
rescue => e
|
|
19
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :profile)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def enabled?
|
|
25
|
+
RailsConsolePro.config.enabled && RailsConsolePro.config.profile_command_enabled
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def disabled_message
|
|
29
|
+
pastel.yellow('Profile command is disabled. Enable it via RailsConsolePro.configure { |c| c.profile_command_enabled = true }')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def config
|
|
33
|
+
RailsConsolePro.config
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def normalize_arguments(label, target, block)
|
|
37
|
+
if block_provided?(block) && (target.is_a?(String) || target.is_a?(Symbol))
|
|
38
|
+
label ||= target.to_s
|
|
39
|
+
target = nil
|
|
40
|
+
end
|
|
41
|
+
[label, target, block]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def block_provided?(block)
|
|
45
|
+
!block.nil?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_execution(target, args, block)
|
|
49
|
+
if block_provided?(block)
|
|
50
|
+
{ callable: block, label: nil }
|
|
51
|
+
elsif relation?(target)
|
|
52
|
+
relation = target
|
|
53
|
+
{ callable: -> { relation.load }, label: "#{relation.klass.name} relation" }
|
|
54
|
+
elsif callable?(target)
|
|
55
|
+
callable = target
|
|
56
|
+
{ callable: -> { callable.call(*args) }, label: callable_label(callable) }
|
|
57
|
+
elsif target.nil?
|
|
58
|
+
pastel.red('Nothing to profile. Provide a block, callable, or ActiveRecord relation.')
|
|
59
|
+
else
|
|
60
|
+
{ callable: -> { target }, label: target.class.name }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def relation?(object)
|
|
65
|
+
defined?(ActiveRecord::Relation) && object.is_a?(ActiveRecord::Relation)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def callable?(object)
|
|
69
|
+
object.respond_to?(:call)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def callable_label(callable)
|
|
73
|
+
if callable.respond_to?(:name) && callable.name
|
|
74
|
+
callable.name
|
|
75
|
+
elsif callable.respond_to?(:receiver) && callable.respond_to?(:name)
|
|
76
|
+
"#{callable.receiver.class}##{callable.name}"
|
|
77
|
+
else
|
|
78
|
+
callable.class.name
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|