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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
class QueueInsightsPrinter < BasePrinter
|
|
6
|
+
def print
|
|
7
|
+
print_header
|
|
8
|
+
print_meta
|
|
9
|
+
print_warnings
|
|
10
|
+
print_section("๐ฌ Enqueued Jobs", value.enqueued_jobs) { |job| format_job(job) }
|
|
11
|
+
print_section("๐ Retry Set", value.retry_jobs) { |job| format_job(job, include_attempts: true) }
|
|
12
|
+
print_section("โ๏ธ Recent Executions", value.recent_executions) { |execution| format_execution(execution) }
|
|
13
|
+
print_footer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def print_header
|
|
19
|
+
header_color = config.get_color(:header)
|
|
20
|
+
output.puts bold_color(header_color, "โ" * config.header_width)
|
|
21
|
+
output.puts bold_color(header_color, "๐งต QUEUE INSIGHTS: #{display_label}")
|
|
22
|
+
output.puts bold_color(header_color, "โ" * config.header_width)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def print_meta
|
|
26
|
+
return if value.meta.empty?
|
|
27
|
+
|
|
28
|
+
output.puts color(config.get_color(:info), "\nโน๏ธ Adapter Stats:")
|
|
29
|
+
value.meta.each do |key, data|
|
|
30
|
+
formatted_key = key.to_s.tr('_', ' ').capitalize
|
|
31
|
+
line = "#{formatted_key.ljust(18)} #{color(config.get_color(:attribute_value_numeric), data.to_s)}"
|
|
32
|
+
output.puts " #{line}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def print_warnings
|
|
37
|
+
return unless value.warnings?
|
|
38
|
+
|
|
39
|
+
warning_color = config.get_color(:warning)
|
|
40
|
+
value.warnings.each do |warning|
|
|
41
|
+
output.puts bold_color(warning_color, "โ ๏ธ #{warning}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def print_section(title, collection)
|
|
46
|
+
output.puts bold_color(config.get_color(:warning), "\n#{title}:")
|
|
47
|
+
if collection.empty?
|
|
48
|
+
output.puts color(:dim, " (none)")
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
collection.each_with_index do |entry, index|
|
|
53
|
+
output.puts color(:dim, " #{index + 1}. ")
|
|
54
|
+
formatted = yield(entry)
|
|
55
|
+
formatted.each { |line| output.puts " #{line}" }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def print_footer
|
|
60
|
+
footer_color = config.get_color(:footer)
|
|
61
|
+
output.puts bold_color(footer_color, "\n" + "โ" * config.header_width)
|
|
62
|
+
output.puts color(:dim, "Captured at: #{format_time(value.captured_at)}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def format_job(job, include_attempts: false)
|
|
66
|
+
lines = []
|
|
67
|
+
lines << "#{bold_color(config.get_color(:attribute_key), job.job_class || job.queue || 'Job')} (#{job.id || 'unknown'})"
|
|
68
|
+
lines << "Queue: #{color(config.get_color(:info), job.queue || 'default')}"
|
|
69
|
+
lines << "Enqueued: #{color(config.get_color(:attribute_value_time), format_time(job.enqueued_at) || 'n/a')}"
|
|
70
|
+
if job.scheduled_at
|
|
71
|
+
lines << "Scheduled: #{color(config.get_color(:attribute_value_time), format_time(job.scheduled_at))}"
|
|
72
|
+
end
|
|
73
|
+
if include_attempts && job.attempts
|
|
74
|
+
lines << "Attempts: #{color(config.get_color(:attribute_value_numeric), job.attempts.to_s)}"
|
|
75
|
+
end
|
|
76
|
+
if job.error
|
|
77
|
+
lines << "Error: #{color(config.get_color(:error), job.error)}"
|
|
78
|
+
end
|
|
79
|
+
if job.args && !job.args.empty?
|
|
80
|
+
serialized_args = safe_truncate(job.args.inspect)
|
|
81
|
+
lines << "Args: #{color(config.get_color(:attribute_value_string), serialized_args)}"
|
|
82
|
+
end
|
|
83
|
+
if present?(job.metadata)
|
|
84
|
+
job.metadata.each do |key, value|
|
|
85
|
+
lines << "#{key.to_s.tr('_', ' ').capitalize}: #{color(config.get_color(:attribute_value_string), value.to_s)}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
lines
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def format_execution(execution)
|
|
92
|
+
lines = []
|
|
93
|
+
lines << "#{bold_color(config.get_color(:attribute_key), execution.job_class || 'Execution')} (#{execution.id || 'unknown'})"
|
|
94
|
+
lines << "Queue: #{color(config.get_color(:info), execution.queue || 'default')}"
|
|
95
|
+
lines << "Started: #{color(config.get_color(:attribute_value_time), format_time(execution.started_at) || 'n/a')}"
|
|
96
|
+
if execution.runtime_ms
|
|
97
|
+
lines << "Runtime: #{color(config.get_color(:attribute_value_numeric), "#{execution.runtime_ms.to_f.round(2)} ms")}"
|
|
98
|
+
end
|
|
99
|
+
if execution.worker || execution.hostname
|
|
100
|
+
lines << "Worker: #{color(config.get_color(:attribute_value_string), [execution.worker, execution.hostname].compact.join('@'))}"
|
|
101
|
+
end
|
|
102
|
+
if present?(execution.metadata)
|
|
103
|
+
execution.metadata.each do |key, value|
|
|
104
|
+
lines << "#{key.to_s.tr('_', ' ').capitalize}: #{color(config.get_color(:attribute_value_string), value.to_s)}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
lines
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def format_time(time)
|
|
111
|
+
return unless time
|
|
112
|
+
|
|
113
|
+
case time
|
|
114
|
+
when Time
|
|
115
|
+
time.strftime("%Y-%m-%d %H:%M:%S")
|
|
116
|
+
when Integer, Float
|
|
117
|
+
Time.at(time).strftime("%Y-%m-%d %H:%M:%S")
|
|
118
|
+
else
|
|
119
|
+
time.to_s
|
|
120
|
+
end
|
|
121
|
+
rescue
|
|
122
|
+
time.to_s
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def safe_truncate(text, max = 120)
|
|
126
|
+
return text if text.length <= max
|
|
127
|
+
|
|
128
|
+
"#{text[0, max]}โฆ"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def display_label
|
|
132
|
+
label = value.adapter_label.to_s.strip
|
|
133
|
+
label.empty? ? "ActiveJob" : label
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def present?(value)
|
|
137
|
+
case value
|
|
138
|
+
when nil
|
|
139
|
+
false
|
|
140
|
+
when String
|
|
141
|
+
!value.empty?
|
|
142
|
+
else
|
|
143
|
+
value.respond_to?(:empty?) ? !value.empty? : true
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
# Printer for snippet collection results
|
|
6
|
+
class SnippetCollectionPrinter < BasePrinter
|
|
7
|
+
PREVIEW_WIDTH = 80
|
|
8
|
+
|
|
9
|
+
def print
|
|
10
|
+
collection = value
|
|
11
|
+
print_header(collection)
|
|
12
|
+
|
|
13
|
+
if collection.empty?
|
|
14
|
+
output.puts color(config.get_color(:warning), "No snippets yet. Capture one with snippets(:add, \"User.count\")")
|
|
15
|
+
else
|
|
16
|
+
collection.each_with_index do |snippet, index|
|
|
17
|
+
print_row(snippet, index: index)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
print_footer(collection)
|
|
22
|
+
collection
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def print_header(collection)
|
|
28
|
+
border
|
|
29
|
+
title = "๐ SNIPPETS"
|
|
30
|
+
filters = []
|
|
31
|
+
filters << "query: #{collection.query.inspect}" if collection.query
|
|
32
|
+
filters << "tags: #{collection.tags.join(', ')}" if collection.tags.any?
|
|
33
|
+
filters << "limit: #{collection.limit}" if collection.limit
|
|
34
|
+
|
|
35
|
+
header_text = filters.any? ? "#{title} (#{filters.join(' ยท ')})" : title
|
|
36
|
+
output.puts bold_color(config.get_color(:header), header_text)
|
|
37
|
+
border
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def print_row(snippet, index:)
|
|
41
|
+
index_label = color(config.get_color(:info), (index + 1).to_s.rjust(2))
|
|
42
|
+
id_label = bold_color(config.get_color(:attribute_key), snippet.id)
|
|
43
|
+
tags_label = snippet.tags.any? ? color(config.get_color(:info), "[#{snippet.tags.join(', ')}]") : nil
|
|
44
|
+
favorite_marker = snippet.favorite? ? color(config.get_color(:success), "โ
") : " "
|
|
45
|
+
summary = truncate(snippet.summary)
|
|
46
|
+
|
|
47
|
+
output.puts "#{index_label} #{favorite_marker} #{id_label} #{summary}"
|
|
48
|
+
if tags_label
|
|
49
|
+
output.puts color(:dim, " #{tags_label}")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def print_footer(collection)
|
|
54
|
+
border
|
|
55
|
+
output.puts color(:dim, "Showing #{collection.size} #{collection.size == 1 ? 'snippet' : 'snippets'}")
|
|
56
|
+
border
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def truncate(text)
|
|
60
|
+
return '' unless text
|
|
61
|
+
return text if text.length <= PREVIEW_WIDTH
|
|
62
|
+
|
|
63
|
+
"#{text[0, PREVIEW_WIDTH - 1]}โฆ"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
# Printer for individual snippet results
|
|
6
|
+
class SnippetPrinter < BasePrinter
|
|
7
|
+
def print
|
|
8
|
+
result = value
|
|
9
|
+
snippet = result.snippet
|
|
10
|
+
|
|
11
|
+
border
|
|
12
|
+
header_text = header_title(result)
|
|
13
|
+
output.puts bold_color(config.get_color(:header), header_text)
|
|
14
|
+
border
|
|
15
|
+
|
|
16
|
+
output.puts format_metadata(snippet, result)
|
|
17
|
+
output.puts
|
|
18
|
+
|
|
19
|
+
snippet.body.each_line.with_index(1) do |line, number|
|
|
20
|
+
output.puts format_line(number, line)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
border
|
|
24
|
+
output.puts result.message if result.message
|
|
25
|
+
border
|
|
26
|
+
|
|
27
|
+
snippet
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def header_title(result)
|
|
33
|
+
case result.action
|
|
34
|
+
when :add
|
|
35
|
+
"โจ Captured snippet #{result.snippet.id}"
|
|
36
|
+
when :favorite
|
|
37
|
+
"โญ Favorite snippet #{result.snippet.id}"
|
|
38
|
+
when :unfavorite
|
|
39
|
+
"โ Snippet #{result.snippet.id}"
|
|
40
|
+
else
|
|
41
|
+
"๐ Snippet #{result.snippet.id}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_metadata(snippet, result)
|
|
46
|
+
tags = snippet.tags.any? ? "tags: #{snippet.tags.join(', ')}" : nil
|
|
47
|
+
details = []
|
|
48
|
+
details << "description: #{snippet.description}" if snippet.description
|
|
49
|
+
details << tags if tags
|
|
50
|
+
details << "favorite: #{snippet.favorite?}"
|
|
51
|
+
details << "created: #{snippet.created_at.strftime('%Y-%m-%d %H:%M')}"
|
|
52
|
+
details << "updated: #{snippet.updated_at.strftime('%Y-%m-%d %H:%M')}"
|
|
53
|
+
|
|
54
|
+
color(config.get_color(:info), details.compact.join(' ยท '))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def format_line(number, line)
|
|
58
|
+
number_label = color(config.get_color(:attribute_key), number.to_s.rjust(3))
|
|
59
|
+
"#{number_label} โ #{line.rstrip}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Value object representing a profiling session summary
|
|
5
|
+
class ProfileResult
|
|
6
|
+
QuerySample = Struct.new(
|
|
7
|
+
:sql,
|
|
8
|
+
:duration_ms,
|
|
9
|
+
:cached,
|
|
10
|
+
:name,
|
|
11
|
+
:binds,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
DuplicateQuery = Struct.new(
|
|
16
|
+
:fingerprint,
|
|
17
|
+
:sql,
|
|
18
|
+
:count,
|
|
19
|
+
:total_duration_ms,
|
|
20
|
+
keyword_init: true
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
attr_reader :label,
|
|
24
|
+
:duration_ms,
|
|
25
|
+
:result,
|
|
26
|
+
:error,
|
|
27
|
+
:query_count,
|
|
28
|
+
:cached_query_count,
|
|
29
|
+
:write_query_count,
|
|
30
|
+
:total_sql_duration_ms,
|
|
31
|
+
:slow_queries,
|
|
32
|
+
:duplicate_queries,
|
|
33
|
+
:query_samples,
|
|
34
|
+
:instantiation_count,
|
|
35
|
+
:cache_hits,
|
|
36
|
+
:cache_misses,
|
|
37
|
+
:cache_writes,
|
|
38
|
+
:started_at,
|
|
39
|
+
:finished_at
|
|
40
|
+
|
|
41
|
+
def initialize(label:, duration_ms:, result:, error:, query_count:, cached_query_count:,
|
|
42
|
+
write_query_count:, total_sql_duration_ms:, slow_queries:, duplicate_queries:,
|
|
43
|
+
query_samples:, instantiation_count:, cache_hits:, cache_misses:,
|
|
44
|
+
cache_writes:, started_at:, finished_at:)
|
|
45
|
+
@label = label
|
|
46
|
+
@duration_ms = duration_ms
|
|
47
|
+
@result = result
|
|
48
|
+
@error = error
|
|
49
|
+
@query_count = query_count
|
|
50
|
+
@cached_query_count = cached_query_count
|
|
51
|
+
@write_query_count = write_query_count
|
|
52
|
+
@total_sql_duration_ms = total_sql_duration_ms
|
|
53
|
+
@slow_queries = Array(slow_queries)
|
|
54
|
+
@duplicate_queries = Array(duplicate_queries)
|
|
55
|
+
@query_samples = Array(query_samples)
|
|
56
|
+
@instantiation_count = instantiation_count
|
|
57
|
+
@cache_hits = cache_hits
|
|
58
|
+
@cache_misses = cache_misses
|
|
59
|
+
@cache_writes = cache_writes
|
|
60
|
+
@started_at = started_at
|
|
61
|
+
@finished_at = finished_at
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def label?
|
|
65
|
+
!(label.nil? || (label.respond_to?(:empty?) && label.empty?))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def error?
|
|
69
|
+
!error.nil?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cache_activity?
|
|
73
|
+
cache_hits.positive? || cache_misses.positive? || cache_writes.positive?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def slow_queries?
|
|
77
|
+
slow_queries.any?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def duplicate_queries?
|
|
81
|
+
duplicate_queries.any?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def query_samples?
|
|
85
|
+
query_samples.any?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read_query_count
|
|
89
|
+
query_count - write_query_count
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_json(pretty: true)
|
|
93
|
+
FormatExporter.to_json(self, pretty: pretty)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def to_yaml
|
|
97
|
+
FormatExporter.to_yaml(self)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_html(style: :default)
|
|
101
|
+
FormatExporter.to_html(self, title: "Profile: #{label || 'Session'}", style: style)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def export_to_file(file_path, format: nil)
|
|
105
|
+
FormatExporter.export_to_file(self, file_path, format: format)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
@@ -32,6 +32,47 @@ if defined?(Pry)
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
Pry::Commands.create_command "profile" do
|
|
36
|
+
description "Profile a block, callable, or relation and report query stats"
|
|
37
|
+
|
|
38
|
+
def process
|
|
39
|
+
pastel = RailsConsolePro::ColorHelper.pastel
|
|
40
|
+
unless RailsConsolePro.config.profile_command_enabled
|
|
41
|
+
output.puts pastel.yellow("Profile command is disabled. Enable it with: RailsConsolePro.configure { |c| c.profile_command_enabled = true }")
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if args.empty?
|
|
46
|
+
show_usage
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
expression = args.join(' ')
|
|
52
|
+
profile_target = eval(expression, target)
|
|
53
|
+
result = RailsConsolePro::Commands.profile(profile_target)
|
|
54
|
+
RailsConsolePro.call(output, result, pry_instance) if result
|
|
55
|
+
rescue SyntaxError => e
|
|
56
|
+
output.puts pastel.red("Syntax Error: #{e.message}")
|
|
57
|
+
show_usage
|
|
58
|
+
rescue => e
|
|
59
|
+
output.puts pastel.red("Error: #{e.message}")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def show_usage
|
|
66
|
+
pastel = RailsConsolePro::ColorHelper.pastel
|
|
67
|
+
output.puts pastel.red("Usage: profile expression")
|
|
68
|
+
output.puts pastel.yellow("Examples:")
|
|
69
|
+
output.puts pastel.cyan(" profile User.active.limit(10)")
|
|
70
|
+
output.puts pastel.cyan(" profile -> { User.includes(:posts).each { |u| u.posts.load } }")
|
|
71
|
+
output.puts pastel.yellow("")
|
|
72
|
+
output.puts pastel.yellow("Tip: For blocks, call the helper method directly: profile('Load') { User.limit(5).to_a }")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
35
76
|
Pry::Commands.create_command "explain" do
|
|
36
77
|
description "Analyze SQL query execution plan"
|
|
37
78
|
|
|
@@ -186,6 +227,71 @@ if defined?(Pry)
|
|
|
186
227
|
end
|
|
187
228
|
end
|
|
188
229
|
|
|
230
|
+
Pry::Commands.create_command "jobs" do
|
|
231
|
+
description "Inspect ActiveJob queue insights (enqueued, retries, recent executions)"
|
|
232
|
+
|
|
233
|
+
def process
|
|
234
|
+
pastel = RailsConsolePro::ColorHelper.pastel
|
|
235
|
+
unless RailsConsolePro.config.queue_command_enabled
|
|
236
|
+
output.puts pastel.yellow("Jobs command is disabled. Enable it with: RailsConsolePro.configure { |c| c.queue_command_enabled = true }")
|
|
237
|
+
return
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
begin
|
|
241
|
+
options = parse_options
|
|
242
|
+
result = RailsConsolePro::Commands.jobs(options)
|
|
243
|
+
if result
|
|
244
|
+
RailsConsolePro.call(output, result, pry_instance)
|
|
245
|
+
else
|
|
246
|
+
output.puts pastel.yellow("No queue insights available.")
|
|
247
|
+
end
|
|
248
|
+
rescue => e
|
|
249
|
+
output.puts pastel.red("Error: #{e.message}")
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
def parse_options
|
|
256
|
+
return {} if args.empty?
|
|
257
|
+
|
|
258
|
+
options = {}
|
|
259
|
+
|
|
260
|
+
args.each do |token|
|
|
261
|
+
case token
|
|
262
|
+
when /\A(limit|queue)=(.+)\z/i
|
|
263
|
+
key = Regexp.last_match(1).downcase.to_sym
|
|
264
|
+
value = Regexp.last_match(2)
|
|
265
|
+
value = value.to_i if key == :limit
|
|
266
|
+
options[key] = value
|
|
267
|
+
when /\Astatus=(.+)\z/i, /\A--status=(.+)\z/i
|
|
268
|
+
statuses = Regexp.last_match(1).split(',').map(&:strip).reject(&:empty?)
|
|
269
|
+
options[:status] = Array(options[:status]) + statuses
|
|
270
|
+
when /\A(class|job_class)=(.+)\z/i, /\A--class=(.+)\z/i
|
|
271
|
+
options[:job_class] = Regexp.last_match(2)
|
|
272
|
+
when /\Aretry=(.+)\z/i, /\A--retry=(.+)\z/i
|
|
273
|
+
options[:retry] = Regexp.last_match(1)
|
|
274
|
+
when /\Adelete=(.+)\z/i, /\A--delete=(.+)\z/i
|
|
275
|
+
options[:delete] = Regexp.last_match(1)
|
|
276
|
+
when /\Adetails?=(.+)\z/i, /\A--details?=(.+)\z/i
|
|
277
|
+
options[:details] = Regexp.last_match(1)
|
|
278
|
+
when '--retry-only'
|
|
279
|
+
options[:status] = Array(options[:status]) + ['retry']
|
|
280
|
+
when '--enqueued-only'
|
|
281
|
+
options[:status] = Array(options[:status]) + ['enqueued']
|
|
282
|
+
when '--recent-only', '--executing-only', '--running-only'
|
|
283
|
+
options[:status] = Array(options[:status]) + ['recent']
|
|
284
|
+
when /\A\d+\z/
|
|
285
|
+
options[:limit] = token.to_i
|
|
286
|
+
else
|
|
287
|
+
options[:queue] ||= token
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
options
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
189
295
|
Pry::Commands.create_command "diff" do
|
|
190
296
|
description "Compare two objects and highlight differences"
|
|
191
297
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
class QueueInsightsResult
|
|
5
|
+
JobSummary = Struct.new(
|
|
6
|
+
:id,
|
|
7
|
+
:job_class,
|
|
8
|
+
:queue,
|
|
9
|
+
:args,
|
|
10
|
+
:enqueued_at,
|
|
11
|
+
:scheduled_at,
|
|
12
|
+
:attempts,
|
|
13
|
+
:error,
|
|
14
|
+
:metadata,
|
|
15
|
+
keyword_init: true
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
ExecutionSummary = Struct.new(
|
|
19
|
+
:id,
|
|
20
|
+
:job_class,
|
|
21
|
+
:queue,
|
|
22
|
+
:started_at,
|
|
23
|
+
:runtime_ms,
|
|
24
|
+
:worker,
|
|
25
|
+
:hostname,
|
|
26
|
+
:metadata,
|
|
27
|
+
keyword_init: true
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
attr_reader :adapter_name,
|
|
31
|
+
:adapter_type,
|
|
32
|
+
:enqueued_jobs,
|
|
33
|
+
:retry_jobs,
|
|
34
|
+
:recent_executions,
|
|
35
|
+
:meta,
|
|
36
|
+
:warnings,
|
|
37
|
+
:captured_at
|
|
38
|
+
|
|
39
|
+
def initialize(adapter_name:, adapter_type:, enqueued_jobs:, retry_jobs:, recent_executions:, meta: {}, warnings: [], captured_at: Time.current)
|
|
40
|
+
@adapter_name = adapter_name
|
|
41
|
+
@adapter_type = adapter_type
|
|
42
|
+
@enqueued_jobs = Array(enqueued_jobs)
|
|
43
|
+
@retry_jobs = Array(retry_jobs)
|
|
44
|
+
@recent_executions = Array(recent_executions)
|
|
45
|
+
@meta = meta || {}
|
|
46
|
+
@warnings = Array(warnings).compact
|
|
47
|
+
@captured_at = captured_at || Time.current
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def adapter_label
|
|
51
|
+
[adapter_name, adapter_type].compact.uniq.join(" ")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def has_enqueued?
|
|
55
|
+
enqueued_jobs.any?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def has_retry_jobs?
|
|
59
|
+
retry_jobs.any?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def has_recent_executions?
|
|
63
|
+
recent_executions.any?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def empty?
|
|
67
|
+
!has_enqueued? && !has_retry_jobs? && !has_recent_executions?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def warnings?
|
|
71
|
+
warnings.any?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def total_enqueued
|
|
75
|
+
enqueued_jobs.size
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def total_retry
|
|
79
|
+
retry_jobs.size
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def total_recent
|
|
83
|
+
recent_executions.size
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def totals
|
|
87
|
+
{
|
|
88
|
+
enqueued: total_enqueued,
|
|
89
|
+
retry: total_retry,
|
|
90
|
+
recent: total_recent
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def with_overrides(overrides = {})
|
|
95
|
+
self.class.new(
|
|
96
|
+
adapter_name: overrides.fetch(:adapter_name, adapter_name),
|
|
97
|
+
adapter_type: overrides.fetch(:adapter_type, adapter_type),
|
|
98
|
+
enqueued_jobs: overrides.fetch(:enqueued_jobs, enqueued_jobs),
|
|
99
|
+
retry_jobs: overrides.fetch(:retry_jobs, retry_jobs),
|
|
100
|
+
recent_executions: overrides.fetch(:recent_executions, recent_executions),
|
|
101
|
+
meta: overrides.fetch(:meta, meta),
|
|
102
|
+
warnings: overrides.fetch(:warnings, warnings),
|
|
103
|
+
captured_at: overrides.fetch(:captured_at, captured_at)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Serializers
|
|
5
|
+
# Serializer for ProfileResult objects
|
|
6
|
+
class ProfileSerializer < BaseSerializer
|
|
7
|
+
def serialize(profile)
|
|
8
|
+
{
|
|
9
|
+
label: profile.label,
|
|
10
|
+
duration_ms: profile.duration_ms,
|
|
11
|
+
total_sql_duration_ms: profile.total_sql_duration_ms,
|
|
12
|
+
query_count: profile.query_count,
|
|
13
|
+
read_query_count: profile.read_query_count,
|
|
14
|
+
write_query_count: profile.write_query_count,
|
|
15
|
+
cached_query_count: profile.cached_query_count,
|
|
16
|
+
instantiation_count: profile.instantiation_count,
|
|
17
|
+
cache_stats: serialize_cache(profile),
|
|
18
|
+
slow_queries: serialize_queries(profile.slow_queries),
|
|
19
|
+
duplicate_queries: serialize_duplicates(profile.duplicate_queries),
|
|
20
|
+
query_samples: serialize_queries(profile.query_samples),
|
|
21
|
+
error: serialize_error(profile.error),
|
|
22
|
+
started_at: profile.started_at&.iso8601,
|
|
23
|
+
finished_at: profile.finished_at&.iso8601,
|
|
24
|
+
result: serialize_data(profile.result)
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def serialize_cache(profile)
|
|
31
|
+
{
|
|
32
|
+
hits: profile.cache_hits,
|
|
33
|
+
misses: profile.cache_misses,
|
|
34
|
+
writes: profile.cache_writes
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def serialize_queries(queries)
|
|
39
|
+
Array(queries).map do |query|
|
|
40
|
+
{
|
|
41
|
+
sql: query.sql,
|
|
42
|
+
duration_ms: query.duration_ms,
|
|
43
|
+
cached: query.cached,
|
|
44
|
+
name: query.name,
|
|
45
|
+
binds: query.binds
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def serialize_duplicates(duplicates)
|
|
51
|
+
Array(duplicates).map do |duplicate|
|
|
52
|
+
{
|
|
53
|
+
fingerprint: duplicate.fingerprint,
|
|
54
|
+
sql: duplicate.sql,
|
|
55
|
+
count: duplicate.count,
|
|
56
|
+
total_duration_ms: duplicate.total_duration_ms
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def serialize_error(error)
|
|
62
|
+
return nil unless error
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
class: error.class.name,
|
|
66
|
+
message: error.message,
|
|
67
|
+
backtrace: Array(error.backtrace).first(10)
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|