rails-profiler 0.4.0 → 0.6.0

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.
@@ -18,7 +18,11 @@ module Profiler
18
18
  ]
19
19
  end
20
20
 
21
- profile = Profiler.storage.load(token)
21
+ profile = if token == "latest"
22
+ Profiler.storage.list(limit: 1).first
23
+ else
24
+ Profiler.storage.load(token)
25
+ end
22
26
  unless profile
23
27
  return [
24
28
  {
@@ -28,7 +32,7 @@ module Profiler
28
32
  ]
29
33
  end
30
34
 
31
- text = format_profile_detail(profile)
35
+ text = format_profile_detail(profile, params)
32
36
 
33
37
  [
34
38
  {
@@ -40,7 +44,29 @@ module Profiler
40
44
 
41
45
  private
42
46
 
43
- def self.format_profile_detail(profile)
47
+ def self.format_profile_detail(profile, params = {})
48
+ requested = params["sections"]&.map(&:to_s)
49
+ want = ->(name) { requested.nil? || requested.include?(name) }
50
+
51
+ lines = []
52
+ lines += section_overview(profile) if want.("overview")
53
+ lines += section_exception(profile) if want.("exception")
54
+ lines += section_job(profile) if want.("job")
55
+ lines += section_request(profile, params) if want.("request")
56
+ lines += section_response(profile, params) if want.("response")
57
+ lines += section_curl(profile) if want.("curl")
58
+ lines += section_database(profile) if want.("database")
59
+ lines += section_performance(profile) if want.("performance")
60
+ lines += section_views(profile) if want.("views")
61
+ lines += section_cache(profile) if want.("cache")
62
+ lines += section_ajax(profile) if want.("ajax")
63
+ lines += section_http(profile) if want.("http")
64
+ lines += section_routes(profile) if want.("routes")
65
+ lines += section_dumps(profile) if want.("dumps")
66
+ lines.join("\n")
67
+ end
68
+
69
+ def self.section_overview(profile)
44
70
  lines = []
45
71
  lines << "# Profile Details: #{profile.token}\n"
46
72
  lines << "**Request:** #{profile.method} #{profile.path}"
@@ -48,260 +74,316 @@ module Profiler
48
74
  lines << "**Duration:** #{profile.duration.round(2)} ms"
49
75
  lines << "**Memory:** #{(profile.memory / 1024.0 / 1024.0).round(2)} MB" if profile.memory
50
76
  lines << "**Time:** #{profile.started_at}\n"
77
+ lines
78
+ end
51
79
 
52
- # Job section
53
- job_data = profile.collector_data("job")
54
- if job_data && job_data["job_class"]
55
- lines << "## Job"
56
- lines << "- Class: #{job_data['job_class']}"
57
- lines << "- Job ID: #{job_data['job_id']}"
58
- lines << "- Queue: #{job_data['queue']}"
59
- lines << "- Executions: #{job_data['executions']}"
60
- lines << "- Status: #{job_data['status']}"
61
- lines << "- Error: #{job_data['error']}" if job_data['error']
62
- if job_data['arguments'] && !job_data['arguments'].empty?
63
- lines << "- Arguments: #{job_data['arguments'].map(&:to_s).join(', ')}"
80
+ def self.section_exception(profile)
81
+ lines = []
82
+ exception_data = profile.collector_data("exception")
83
+ return lines unless exception_data && exception_data["exception_class"]
84
+
85
+ lines << "## Exception"
86
+ lines << "**Class:** #{exception_data['exception_class']}"
87
+ lines << "**Message:** #{exception_data['message']}\n"
88
+
89
+ backtrace = exception_data["backtrace"]
90
+ if backtrace && !backtrace.empty?
91
+ lines << "### Backtrace"
92
+ backtrace.first(20).each do |frame|
93
+ marker = frame["app_frame"] ? "★ " : " "
94
+ lines << "#{marker}#{frame['location']}"
64
95
  end
65
96
  lines << ""
66
97
  end
98
+ lines
99
+ end
100
+
101
+ def self.section_job(profile)
102
+ lines = []
103
+ job_data = profile.collector_data("job")
104
+ return lines unless job_data && job_data["job_class"]
105
+
106
+ lines << "## Job"
107
+ lines << "- Class: #{job_data['job_class']}"
108
+ lines << "- Job ID: #{job_data['job_id']}"
109
+ lines << "- Queue: #{job_data['queue']}"
110
+ lines << "- Executions: #{job_data['executions']}"
111
+ lines << "- Status: #{job_data['status']}"
112
+ lines << "- Error: #{job_data['error']}" if job_data["error"]
113
+ if job_data["arguments"] && !job_data["arguments"].empty?
114
+ lines << "- Arguments: #{job_data['arguments'].map(&:to_s).join(', ')}"
115
+ end
116
+ lines << ""
117
+ lines
118
+ end
67
119
 
68
- # Request section
120
+ def self.section_request(profile, params)
121
+ lines = []
69
122
  req_data = profile.collector_data("request")
70
- if req_data
71
- params = req_data["params"]
72
- headers = req_data["headers"]
73
-
74
- if params && !params.empty?
75
- lines << "## Request Params"
76
- params.each { |k, v| lines << "- **#{k}**: #{v}" }
77
- lines << ""
78
- end
123
+ return lines unless req_data
79
124
 
80
- if headers && !headers.empty?
81
- lines << "## Request Headers"
82
- headers.each { |k, v| lines << "- **#{k}**: #{v}" }
83
- lines << ""
84
- end
125
+ request_params = req_data["params"]
126
+ headers = req_data["headers"]
85
127
 
86
- req_body = req_data["request_body"]
87
- if req_body && !req_body.empty?
88
- enc = req_data["request_body_encoding"]
89
- lines << "## Request Body"
90
- if enc == "base64"
91
- lines << "_[binary, base64-encoded]_"
92
- else
93
- lines << "```"
94
- lines << req_body
95
- lines << "```"
96
- end
97
- lines << ""
98
- end
128
+ if request_params && !request_params.empty?
129
+ lines << "## Request Params"
130
+ request_params.each { |k, v| lines << "- **#{k}**: #{v}" }
131
+ lines << ""
99
132
  end
100
133
 
101
- # Response Headers section
134
+ if headers && !headers.empty?
135
+ lines << "## Request Headers"
136
+ headers.each { |k, v| lines << "- **#{k}**: #{v}" }
137
+ lines << ""
138
+ end
139
+
140
+ req_body = req_data["request_body"]
141
+ if req_body && !req_body.empty?
142
+ lines << "## Request Body"
143
+ formatted = BodyFormatter.format_body(
144
+ profile.token,
145
+ "request_body",
146
+ req_body,
147
+ req_data["request_body_encoding"],
148
+ params
149
+ )
150
+ lines << formatted if formatted
151
+ lines << ""
152
+ end
153
+ lines
154
+ end
155
+
156
+ def self.section_response(profile, params)
157
+ lines = []
158
+
102
159
  if profile.response_headers&.any?
103
160
  lines << "## Response Headers"
104
161
  profile.response_headers.each { |k, v| lines << "- **#{k}**: #{v}" }
105
162
  lines << ""
106
163
  end
107
164
 
108
- # Response Body section
109
165
  resp_body = profile.response_body
110
166
  if resp_body && !resp_body.empty?
111
- enc = profile.response_body_encoding
112
167
  lines << "## Response Body"
113
- if enc == "base64"
114
- lines << "_[binary, base64-encoded]_"
115
- else
116
- lines << "```"
117
- lines << resp_body
118
- lines << "```"
119
- end
168
+ formatted = BodyFormatter.format_body(
169
+ profile.token,
170
+ "response_body",
171
+ resp_body,
172
+ profile.response_body_encoding,
173
+ params
174
+ )
175
+ lines << formatted if formatted
120
176
  lines << ""
121
177
  end
178
+ lines
179
+ end
122
180
 
123
- # Curl command
124
- req_data_for_curl = profile.collector_data("request")
181
+ def self.section_curl(profile)
182
+ req_data = profile.collector_data("request")
183
+ lines = []
125
184
  lines << "## Curl Command"
126
185
  lines << "```bash"
127
- lines << generate_curl(profile, req_data_for_curl)
186
+ lines << generate_curl(profile, req_data)
128
187
  lines << "```"
129
188
  lines << ""
189
+ lines
190
+ end
130
191
 
131
- # Database section
192
+ def self.section_database(profile)
193
+ lines = []
132
194
  db_data = profile.collector_data("database")
133
- if db_data && db_data["total_queries"]
134
- lines << "## Database"
135
- lines << "- Total Queries: #{db_data['total_queries']}"
136
- lines << "- Total Duration: #{db_data['total_duration'].round(2)} ms"
137
- lines << "- Slow Queries: #{db_data['slow_queries']}"
138
- lines << "- Cached Queries: #{db_data['cached_queries']}\n"
139
-
140
- if db_data["queries"] && !db_data["queries"].empty?
141
- lines << "### Query Details"
142
- db_data["queries"].each_with_index do |query, index|
143
- lines << "\n**Query #{index + 1}** (#{query['duration'].round(2)}ms):"
144
- lines << "```sql"
145
- lines << query['sql']
146
- lines << "```"
147
- if query['backtrace'] && !query['backtrace'].empty?
148
- lines << "_Backtrace:_"
149
- query['backtrace'].first(3).each { |frame| lines << " #{frame}" }
150
- end
195
+ return lines unless db_data && db_data["total_queries"]
196
+
197
+ lines << "## Database"
198
+ lines << "- Total Queries: #{db_data['total_queries']}"
199
+ lines << "- Total Duration: #{db_data['total_duration'].round(2)} ms"
200
+ lines << "- Slow Queries: #{db_data['slow_queries']}"
201
+ lines << "- Cached Queries: #{db_data['cached_queries']}\n"
202
+
203
+ if db_data["queries"] && !db_data["queries"].empty?
204
+ lines << "### Query Details"
205
+ db_data["queries"].each_with_index do |query, index|
206
+ lines << "\n**Query #{index + 1}** (#{query['duration'].round(2)}ms):"
207
+ lines << "```sql"
208
+ lines << query["sql"]
209
+ lines << "```"
210
+ if query["backtrace"] && !query["backtrace"].empty?
211
+ lines << "_Backtrace:_"
212
+ query["backtrace"].first(3).each { |frame| lines << " #{frame}" }
151
213
  end
152
214
  end
153
- lines << ""
154
215
  end
216
+ lines << ""
217
+ lines
218
+ end
155
219
 
156
- # Performance section
220
+ def self.section_performance(profile)
221
+ lines = []
157
222
  perf_data = profile.collector_data("performance")
158
- if perf_data && perf_data["total_events"]
159
- lines << "## Performance Timeline"
160
- lines << "- Total Events: #{perf_data['total_events']}"
161
- lines << "- Total Duration: #{perf_data['total_duration'].round(2)} ms\n"
162
-
163
- if perf_data["events"] && !perf_data["events"].empty?
164
- lines << "### Events"
165
- perf_data["events"].each do |event|
166
- lines << "- **#{event['name']}**: #{event['duration'].round(2)} ms"
167
- end
223
+ return lines unless perf_data && perf_data["total_events"]
224
+
225
+ lines << "## Performance Timeline"
226
+ lines << "- Total Events: #{perf_data['total_events']}"
227
+ lines << "- Total Duration: #{perf_data['total_duration'].round(2)} ms\n"
228
+
229
+ if perf_data["events"] && !perf_data["events"].empty?
230
+ lines << "### Events"
231
+ perf_data["events"].each do |event|
232
+ lines << "- **#{event['name']}**: #{event['duration'].round(2)} ms"
168
233
  end
169
- lines << ""
170
234
  end
235
+ lines << ""
236
+ lines
237
+ end
171
238
 
172
- # Views section
239
+ def self.section_views(profile)
240
+ lines = []
173
241
  view_data = profile.collector_data("view")
174
- if view_data && (view_data["total_views"] || view_data["total_partials"])
175
- lines << "## View Rendering"
176
- lines << "- Templates: #{view_data['total_views']}"
177
- lines << "- Partials: #{view_data['total_partials']}"
178
- lines << "- Total Duration: #{view_data['total_duration'].round(2)} ms\n"
179
-
180
- if view_data["views"] && !view_data["views"].empty?
181
- lines << "### Templates"
182
- view_data["views"].each do |view|
183
- lines << "- `#{view['identifier']}` #{view['duration'].round(2)} ms"
184
- end
185
- lines << ""
242
+ return lines unless view_data && (view_data["total_views"] || view_data["total_partials"])
243
+
244
+ lines << "## View Rendering"
245
+ lines << "- Templates: #{view_data['total_views']}"
246
+ lines << "- Partials: #{view_data['total_partials']}"
247
+ lines << "- Total Duration: #{view_data['total_duration'].round(2)} ms\n"
248
+
249
+ if view_data["views"] && !view_data["views"].empty?
250
+ lines << "### Templates"
251
+ view_data["views"].each do |view|
252
+ lines << "- `#{view['identifier']}` — #{view['duration'].round(2)} ms"
186
253
  end
254
+ lines << ""
255
+ end
187
256
 
188
- if view_data["partials"] && !view_data["partials"].empty?
189
- lines << "### Partials"
190
- view_data["partials"].each do |partial|
191
- lines << "- `#{partial['identifier']}` — #{partial['duration'].round(2)} ms"
192
- end
193
- lines << ""
257
+ if view_data["partials"] && !view_data["partials"].empty?
258
+ lines << "### Partials"
259
+ view_data["partials"].each do |partial|
260
+ lines << "- `#{partial['identifier']}` — #{partial['duration'].round(2)} ms"
194
261
  end
262
+ lines << ""
195
263
  end
264
+ lines
265
+ end
196
266
 
197
- # Cache section
267
+ def self.section_cache(profile)
268
+ lines = []
198
269
  cache_data = profile.collector_data("cache")
199
- if cache_data && cache_data["total_reads"]
200
- lines << "## Cache"
201
- lines << "- Reads: #{cache_data['total_reads']}"
202
- lines << "- Writes: #{cache_data['total_writes']}"
203
- lines << "- Deletes: #{cache_data['total_deletes']}"
204
- lines << "- Hit Rate: #{cache_data['hit_rate']}%\n"
205
-
206
- if cache_data["reads"] && !cache_data["reads"].empty?
207
- lines << "### Cache Reads"
208
- cache_data["reads"].each do |op|
209
- hit_label = op['hit'] ? "HIT" : "MISS"
210
- lines << "- [#{hit_label}] `#{op['key']}` #{op['duration'].round(2)} ms"
211
- end
212
- lines << ""
270
+ return lines unless cache_data && cache_data["total_reads"]
271
+
272
+ lines << "## Cache"
273
+ lines << "- Reads: #{cache_data['total_reads']}"
274
+ lines << "- Writes: #{cache_data['total_writes']}"
275
+ lines << "- Deletes: #{cache_data['total_deletes']}"
276
+ lines << "- Hit Rate: #{cache_data['hit_rate']}%\n"
277
+
278
+ if cache_data["reads"] && !cache_data["reads"].empty?
279
+ lines << "### Cache Reads"
280
+ cache_data["reads"].each do |op|
281
+ hit_label = op["hit"] ? "HIT" : "MISS"
282
+ lines << "- [#{hit_label}] `#{op['key']}` — #{op['duration'].round(2)} ms"
213
283
  end
284
+ lines << ""
285
+ end
214
286
 
215
- if cache_data["writes"] && !cache_data["writes"].empty?
216
- lines << "### Cache Writes"
217
- cache_data["writes"].each do |op|
218
- lines << "- `#{op['key']}` — #{op['duration'].round(2)} ms"
219
- end
220
- lines << ""
287
+ if cache_data["writes"] && !cache_data["writes"].empty?
288
+ lines << "### Cache Writes"
289
+ cache_data["writes"].each do |op|
290
+ lines << "- `#{op['key']}` — #{op['duration'].round(2)} ms"
221
291
  end
292
+ lines << ""
293
+ end
222
294
 
223
- if cache_data["deletes"] && !cache_data["deletes"].empty?
224
- lines << "### Cache Deletes"
225
- cache_data["deletes"].each do |op|
226
- lines << "- `#{op['key']}` — #{op['duration'].round(2)} ms"
227
- end
228
- lines << ""
295
+ if cache_data["deletes"] && !cache_data["deletes"].empty?
296
+ lines << "### Cache Deletes"
297
+ cache_data["deletes"].each do |op|
298
+ lines << "- `#{op['key']}` — #{op['duration'].round(2)} ms"
229
299
  end
300
+ lines << ""
230
301
  end
302
+ lines
303
+ end
231
304
 
232
- # Ajax section
305
+ def self.section_ajax(profile)
306
+ lines = []
233
307
  ajax_data = profile.collector_data("ajax")
234
- if ajax_data && ajax_data["total_requests"].to_i > 0
235
- lines << "## AJAX Requests"
236
- lines << "- Total: #{ajax_data['total_requests']}"
237
- lines << "- Total Duration: #{ajax_data['total_duration'].round(2)} ms\n"
238
-
239
- if ajax_data["requests"] && !ajax_data["requests"].empty?
240
- lines << "### Request List"
241
- ajax_data["requests"].each do |req|
242
- lines << "- **#{req['method']} #{req['path']}** — #{req['status']} — #{req['duration'].round(2)} ms (token: #{req['token']})"
243
- end
244
- lines << ""
308
+ return lines unless ajax_data && ajax_data["total_requests"].to_i > 0
309
+
310
+ lines << "## AJAX Requests"
311
+ lines << "- Total: #{ajax_data['total_requests']}"
312
+ lines << "- Total Duration: #{ajax_data['total_duration'].round(2)} ms\n"
313
+
314
+ if ajax_data["requests"] && !ajax_data["requests"].empty?
315
+ lines << "### Request List"
316
+ ajax_data["requests"].each do |req|
317
+ lines << "- **#{req['method']} #{req['path']}** — #{req['status']} — #{req['duration'].round(2)} ms (token: #{req['token']})"
245
318
  end
319
+ lines << ""
246
320
  end
321
+ lines
322
+ end
247
323
 
248
- # HTTP section
324
+ def self.section_http(profile)
325
+ lines = []
249
326
  http_data = profile.collector_data("http")
250
- if http_data && http_data["total_requests"].to_i > 0
251
- threshold = Profiler.configuration.slow_http_threshold
252
- lines << "## Outbound HTTP"
253
- lines << "- Total: #{http_data['total_requests']}"
254
- lines << "- Total Duration: #{http_data['total_duration'].round(2)} ms"
255
- lines << "- Slow (>#{threshold}ms): #{http_data['slow_requests']}"
256
- lines << "- Errors: #{http_data['error_requests']}\n"
257
-
258
- if http_data["requests"] && !http_data["requests"].empty?
259
- lines << "### Request List"
260
- http_data["requests"].each do |req|
261
- flag = req["duration"] >= threshold ? " [SLOW]" : ""
262
- err = req["status"] >= 400 || req["status"] == 0 ? " [ERROR]" : ""
263
- lines << "- **#{req['method']} #{req['url']}** #{req['status'] == 0 ? 'error' : req['status']} #{req['duration'].round(2)} ms#{flag}#{err}"
264
- end
265
- lines << ""
327
+ return lines unless http_data && http_data["total_requests"].to_i > 0
328
+
329
+ threshold = Profiler.configuration.slow_http_threshold
330
+ lines << "## Outbound HTTP"
331
+ lines << "- Total: #{http_data['total_requests']}"
332
+ lines << "- Total Duration: #{http_data['total_duration'].round(2)} ms"
333
+ lines << "- Slow (>#{threshold}ms): #{http_data['slow_requests']}"
334
+ lines << "- Errors: #{http_data['error_requests']}\n"
335
+
336
+ if http_data["requests"] && !http_data["requests"].empty?
337
+ lines << "### Request List"
338
+ http_data["requests"].each do |req|
339
+ flag = req["duration"] >= threshold ? " [SLOW]" : ""
340
+ err = req["status"] >= 400 || req["status"] == 0 ? " [ERROR]" : ""
341
+ lines << "- **#{req['method']} #{req['url']}** — #{req['status'] == 0 ? 'error' : req['status']} — #{req['duration'].round(2)} ms#{flag}#{err}"
266
342
  end
343
+ lines << ""
267
344
  end
345
+ lines
346
+ end
268
347
 
269
- # Routes section
348
+ def self.section_routes(profile)
349
+ lines = []
270
350
  routes_data = profile.collector_data("routes")
271
- if routes_data && routes_data["total"].to_i > 0
272
- lines << "## Routes"
273
- lines << "- Total routes: #{routes_data['total']}"
274
-
275
- matched = routes_data["matched"]
276
- if matched
277
- lines << "- **Matched:** `#{matched['verb']} #{matched['pattern']}`"
278
- lines << " - Route name: #{matched['name']}_path" if matched["name"]
279
- lines << " - Controller#Action: #{matched['controller_action']}" if matched["controller_action"]
280
- else
281
- lines << "- No route matched"
282
- end
283
- lines << ""
351
+ return lines unless routes_data && routes_data["total"].to_i > 0
352
+
353
+ lines << "## Routes"
354
+ lines << "- Total routes: #{routes_data['total']}"
355
+
356
+ matched = routes_data["matched"]
357
+ if matched
358
+ lines << "- **Matched:** `#{matched['verb']} #{matched['pattern']}`"
359
+ lines << " - Route name: #{matched['name']}_path" if matched["name"]
360
+ lines << " - Controller#Action: #{matched['controller_action']}" if matched["controller_action"]
361
+ else
362
+ lines << "- No route matched"
284
363
  end
364
+ lines << ""
365
+ lines
366
+ end
285
367
 
286
- # Dumps section
368
+ def self.section_dumps(profile)
369
+ lines = []
287
370
  dump_data = profile.collector_data("dump")
288
- if dump_data && dump_data["count"].to_i > 0
289
- lines << "## Variable Dumps"
290
- lines << "- Count: #{dump_data['count']}\n"
291
-
292
- dump_data["dumps"]&.each_with_index do |dump, index|
293
- label = dump['label'] || "Dump #{index + 1}"
294
- location = [dump['file'], dump['line']].compact.join(':')
295
- lines << "### #{label}"
296
- lines << "_Source: #{location}_" unless location.empty?
297
- lines << "```"
298
- lines << (dump['formatted'] || dump['value'].inspect)
299
- lines << "```"
300
- end
301
- lines << ""
371
+ return lines unless dump_data && dump_data["count"].to_i > 0
372
+
373
+ lines << "## Variable Dumps"
374
+ lines << "- Count: #{dump_data['count']}\n"
375
+
376
+ dump_data["dumps"]&.each_with_index do |dump, index|
377
+ label = dump["label"] || "Dump #{index + 1}"
378
+ location = [dump["file"], dump["line"]].compact.join(":")
379
+ lines << "### #{label}"
380
+ lines << "_Source: #{location}_" unless location.empty?
381
+ lines << "```"
382
+ lines << (dump["formatted"] || dump["value"].inspect)
383
+ lines << "```"
302
384
  end
303
-
304
- lines.join("\n")
385
+ lines << ""
386
+ lines
305
387
  end
306
388
 
307
389
  def self.generate_curl(profile, req_data)
@@ -10,7 +10,11 @@ module Profiler
10
10
  return [{ type: "text", text: "Error: token parameter is required" }]
11
11
  end
12
12
 
13
- profile = Profiler.storage.load(token)
13
+ profile = if token == "latest"
14
+ Profiler.storage.list(limit: 1).first
15
+ else
16
+ Profiler.storage.load(token)
17
+ end
14
18
  unless profile
15
19
  return [{ type: "text", text: "Profile not found: #{token}" }]
16
20
  end