catpm 0.6.1 → 0.6.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/README.md +1 -1
- data/app/controllers/catpm/system_controller.rb +1 -0
- data/app/views/catpm/system/index.html.erb +47 -0
- data/app/views/layouts/catpm/application.html.erb +3 -0
- data/lib/catpm/adapter/base.rb +4 -0
- data/lib/catpm/adapter/postgresql.rb +17 -0
- data/lib/catpm/adapter/sqlite.rb +29 -0
- data/lib/catpm/collector.rb +51 -0
- data/lib/catpm/engine.rb +3 -3
- data/lib/catpm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b36a94d20679108a187a682cc578a0b6a90afc6f4c42a0d645ae02c517206ce8
|
|
4
|
+
data.tar.gz: a493544c554ef3bb3fce48a430660f5cebdff7322cbdb553a71a7b573062a442
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c34e1daeb528987a6d90d4343a2f274db4024714891af8ad6a5e568e061b80335c41406cfa6f16e2545a9bc69aab83c7408f658de6c43b2b0aa0fdb5357897d4
|
|
7
|
+
data.tar.gz: 4a954b7676aebe80843f8c56695e4ed73ea9706132c009b0366c2f7c2ccb3f3c826670e12b3ec2c564465464f422f664bf808ded1decabe95b88632fe490cbab
|
data/README.md
CHANGED
|
@@ -43,6 +43,53 @@
|
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
45
|
|
|
46
|
+
<%# ─── Storage ─── %>
|
|
47
|
+
<h2>Database Storage</h2>
|
|
48
|
+
<%= section_description("Disk space used by catpm tables in your database.") %>
|
|
49
|
+
|
|
50
|
+
<% total_bytes = @table_sizes.sum { |t| t[:total_bytes].to_i } %>
|
|
51
|
+
<% has_bytes = @table_sizes.any? { |t| t[:total_bytes] } %>
|
|
52
|
+
|
|
53
|
+
<% if has_bytes %>
|
|
54
|
+
<div class="storage-total">
|
|
55
|
+
Total: <strong><%= number_to_human_size(total_bytes) %></strong>
|
|
56
|
+
</div>
|
|
57
|
+
<% end %>
|
|
58
|
+
|
|
59
|
+
<div class="config-table">
|
|
60
|
+
<div class="table-scroll">
|
|
61
|
+
<table>
|
|
62
|
+
<thead>
|
|
63
|
+
<tr>
|
|
64
|
+
<th>Table</th>
|
|
65
|
+
<th style="text-align:right">Rows</th>
|
|
66
|
+
<% if has_bytes %>
|
|
67
|
+
<th style="text-align:right">Size</th>
|
|
68
|
+
<% end %>
|
|
69
|
+
</tr>
|
|
70
|
+
</thead>
|
|
71
|
+
<tbody>
|
|
72
|
+
<% @table_sizes.each do |t| %>
|
|
73
|
+
<tr>
|
|
74
|
+
<td class="mono"><%= t[:name] %></td>
|
|
75
|
+
<td class="mono" style="text-align:right"><%= number_with_delimiter(t[:row_estimate].to_i) %></td>
|
|
76
|
+
<% if has_bytes %>
|
|
77
|
+
<td class="mono" style="text-align:right"><%= number_to_human_size(t[:total_bytes].to_i) %></td>
|
|
78
|
+
<% end %>
|
|
79
|
+
</tr>
|
|
80
|
+
<% end %>
|
|
81
|
+
<% if has_bytes %>
|
|
82
|
+
<tr style="font-weight:600;border-top:2px solid var(--border)">
|
|
83
|
+
<td>Total</td>
|
|
84
|
+
<td class="mono" style="text-align:right"><%= number_with_delimiter(@table_sizes.sum { |t| t[:row_estimate].to_i }) %></td>
|
|
85
|
+
<td class="mono" style="text-align:right"><%= number_to_human_size(total_bytes) %></td>
|
|
86
|
+
</tr>
|
|
87
|
+
<% end %>
|
|
88
|
+
</tbody>
|
|
89
|
+
</table>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
46
93
|
<%# ─── Configuration ─── %>
|
|
47
94
|
<h2>Configuration</h2>
|
|
48
95
|
<%= section_description("Current catpm settings. Configure via initializer.") %>
|
|
@@ -163,6 +163,9 @@
|
|
|
163
163
|
.sample-sidebar { flex: 0 0 320px; }
|
|
164
164
|
@media (max-width: 900px) { .sample-layout { flex-direction: column; } .sample-sidebar { flex: auto; width: 100%; } }
|
|
165
165
|
|
|
166
|
+
/* ─── Storage ─── */
|
|
167
|
+
.storage-total { font-size: 14px; color: var(--text-1); margin-bottom: 12px; }
|
|
168
|
+
|
|
166
169
|
/* ─── Config Table ─── */
|
|
167
170
|
.config-table { background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
168
171
|
.config-table table { margin: 0; }
|
data/lib/catpm/adapter/base.rb
CHANGED
|
@@ -182,6 +182,23 @@ module Catpm
|
|
|
182
182
|
"EXTRACT(EPOCH FROM bucket_start)::integer % #{interval.to_i} = 0"
|
|
183
183
|
end
|
|
184
184
|
|
|
185
|
+
def table_sizes
|
|
186
|
+
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
|
187
|
+
rows = conn.select_all(<<~SQL)
|
|
188
|
+
SELECT c.relname AS name,
|
|
189
|
+
pg_total_relation_size(c.oid) AS total_bytes,
|
|
190
|
+
pg_table_size(c.oid) AS table_bytes,
|
|
191
|
+
pg_indexes_size(c.oid) AS index_bytes,
|
|
192
|
+
c.reltuples::bigint AS row_estimate
|
|
193
|
+
FROM pg_class c
|
|
194
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
195
|
+
WHERE c.relname LIKE 'catpm_%' AND c.relkind = 'r' AND n.nspname = 'public'
|
|
196
|
+
ORDER BY pg_total_relation_size(c.oid) DESC
|
|
197
|
+
SQL
|
|
198
|
+
rows.map(&:symbolize_keys)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
185
202
|
private
|
|
186
203
|
|
|
187
204
|
def advisory_lock_key(identifier)
|
data/lib/catpm/adapter/sqlite.rb
CHANGED
|
@@ -152,6 +152,35 @@ module Catpm
|
|
|
152
152
|
"CAST(strftime('%s', bucket_start) AS INTEGER) % #{interval.to_i} = 0"
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
def table_sizes
|
|
156
|
+
conn = ActiveRecord::Base.connection
|
|
157
|
+
|
|
158
|
+
tables = conn.select_values(
|
|
159
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'catpm_%' ORDER BY name"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
has_dbstat = begin
|
|
163
|
+
conn.select_value("SELECT 1 FROM dbstat LIMIT 1")
|
|
164
|
+
true
|
|
165
|
+
rescue StandardError
|
|
166
|
+
false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
tables.map do |table|
|
|
170
|
+
row_count = conn.select_value("SELECT COUNT(*) FROM \"#{table}\"").to_i
|
|
171
|
+
|
|
172
|
+
total_bytes = if has_dbstat
|
|
173
|
+
objects = [table] + conn.select_values(
|
|
174
|
+
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=#{conn.quote(table)}"
|
|
175
|
+
)
|
|
176
|
+
placeholders = objects.map { |n| conn.quote(n) }.join(',')
|
|
177
|
+
conn.select_value("SELECT SUM(pgsize) FROM dbstat WHERE name IN (#{placeholders})").to_i
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
{ name: table, total_bytes: total_bytes, row_estimate: row_count }
|
|
181
|
+
end.sort_by { |r| -(r[:total_bytes] || 0) }
|
|
182
|
+
end
|
|
183
|
+
|
|
155
184
|
private
|
|
156
185
|
|
|
157
186
|
def with_write_lock(&block)
|
data/lib/catpm/collector.rb
CHANGED
|
@@ -45,6 +45,7 @@ module Catpm
|
|
|
45
45
|
|
|
46
46
|
if req_segments
|
|
47
47
|
segments = segment_data[:segments]
|
|
48
|
+
collapse_code_wrappers(segments)
|
|
48
49
|
|
|
49
50
|
# Inject root request segment with full duration
|
|
50
51
|
root_segment = {
|
|
@@ -227,6 +228,7 @@ module Catpm
|
|
|
227
228
|
|
|
228
229
|
if req_segments && segment_data
|
|
229
230
|
segments = segment_data[:segments]
|
|
231
|
+
collapse_code_wrappers(segments)
|
|
230
232
|
|
|
231
233
|
# Inject root request segment
|
|
232
234
|
root_segment = {
|
|
@@ -331,6 +333,55 @@ module Catpm
|
|
|
331
333
|
|
|
332
334
|
private
|
|
333
335
|
|
|
336
|
+
# Remove near-zero-duration "code" spans that merely wrap a "controller" span.
|
|
337
|
+
# This happens when CallTracer (TracePoint) captures a thin dispatch method
|
|
338
|
+
# (e.g. Telegram::WebhookController#process) whose :return fires before the
|
|
339
|
+
# ActiveSupport controller notification finishes.
|
|
340
|
+
# Mutates segments in place: removes the wrapper and re-indexes parent references.
|
|
341
|
+
def collapse_code_wrappers(segments)
|
|
342
|
+
# Identify code spans to collapse: near-zero duration wrapping a controller child
|
|
343
|
+
collapse = {}
|
|
344
|
+
segments.each_with_index do |seg, i|
|
|
345
|
+
next unless seg[:type] == 'code'
|
|
346
|
+
next unless (seg[:duration] || 0).to_f < 1.0
|
|
347
|
+
|
|
348
|
+
has_controller_child = segments.any? { |s| s[:parent_index] == i && s[:type] == 'controller' }
|
|
349
|
+
next unless has_controller_child
|
|
350
|
+
|
|
351
|
+
collapse[i] = seg[:parent_index]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
return if collapse.empty?
|
|
355
|
+
|
|
356
|
+
# Reparent children of collapsed spans
|
|
357
|
+
segments.each do |seg|
|
|
358
|
+
pi = seg[:parent_index]
|
|
359
|
+
next unless pi && collapse.key?(pi)
|
|
360
|
+
new_parent = collapse[pi]
|
|
361
|
+
if new_parent.nil?
|
|
362
|
+
seg.delete(:parent_index)
|
|
363
|
+
else
|
|
364
|
+
seg[:parent_index] = new_parent
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Build old→new index mapping, remove collapsed spans
|
|
369
|
+
old_to_new = {}
|
|
370
|
+
kept = []
|
|
371
|
+
segments.each_with_index do |seg, i|
|
|
372
|
+
next if collapse.key?(i)
|
|
373
|
+
old_to_new[i] = kept.size
|
|
374
|
+
kept << seg
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Rewrite parent references to new indices
|
|
378
|
+
kept.each do |seg|
|
|
379
|
+
seg[:parent_index] = old_to_new[seg[:parent_index]] if seg[:parent_index]
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
segments.replace(kept)
|
|
383
|
+
end
|
|
384
|
+
|
|
334
385
|
# Determine sample type at event creation time so only sampled events
|
|
335
386
|
# carry full context in the buffer. Includes filling phase via
|
|
336
387
|
# process-level counter (resets on restart — acceptable approximation).
|
data/lib/catpm/engine.rb
CHANGED
|
@@ -16,9 +16,9 @@ module Catpm
|
|
|
16
16
|
|
|
17
17
|
if Catpm.config.instrument_middleware_stack
|
|
18
18
|
app = Rails.application
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
app.middleware.insert_before(
|
|
19
|
+
middlewares = app.middleware.reject { |m| m.name&.start_with?('Catpm::') }
|
|
20
|
+
middlewares.reverse_each do |middleware|
|
|
21
|
+
app.middleware.insert_before(middleware, Catpm::MiddlewareProbe, middleware.name)
|
|
22
22
|
rescue ArgumentError, RuntimeError
|
|
23
23
|
# Middleware not found in stack — skip
|
|
24
24
|
end
|
data/lib/catpm/version.rb
CHANGED