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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77d3e9a9e80a64c61ba1cbc9f400f9b53cb774c2eee0cf1fc807952bf1b81c39
4
- data.tar.gz: 3364e29476e9a6f77494f86c0fb00932c70a84eb90067df86e8455a00608930b
3
+ metadata.gz: b36a94d20679108a187a682cc578a0b6a90afc6f4c42a0d645ae02c517206ce8
4
+ data.tar.gz: a493544c554ef3bb3fce48a430660f5cebdff7322cbdb553a71a7b573062a442
5
5
  SHA512:
6
- metadata.gz: 52c5cb2542feabdb2a493390e1fd0c42bef5e5ea8c8d89bb71115f774b790e88b8c8f253402e47835da8190a4113d3ed7302398f59a2ce2841df2576018a20cb
7
- data.tar.gz: be688f7c4ce207e04d09fb1ac30b390bc7f2ea8b050120bc46929a952a632d17e63ffc110950907ee37e60aa62f8f7400a3d982acfc568dd2392d01fe9841c91
6
+ metadata.gz: c34e1daeb528987a6d90d4343a2f274db4024714891af8ad6a5e568e061b80335c41406cfa6f16e2545a9bc69aab83c7408f658de6c43b2b0aa0fdb5357897d4
7
+ data.tar.gz: 4a954b7676aebe80843f8c56695e4ed73ea9706132c009b0366c2f7c2ccb3f3c826670e12b3ec2c564465464f422f664bf808ded1decabe95b88632fe490cbab
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  gem build catpm.gemspec
2
- gem push catpm-0.5.0.gem
2
+ gem push catpm-0.6.0.gem
3
3
 
4
4
  # Catpm
5
5
 
@@ -9,6 +9,7 @@ module Catpm
9
9
  @config = Catpm.config
10
10
  @oldest_bucket = Catpm::Bucket.minimum(:bucket_start)
11
11
  @active_error_count = Catpm::ErrorRecord.unresolved.count
12
+ @table_sizes = Catpm::Adapter.current.table_sizes
12
13
  end
13
14
  end
14
15
  end
@@ -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; }
@@ -49,6 +49,10 @@ module Catpm
49
49
  raise NotImplementedError
50
50
  end
51
51
 
52
+ def table_sizes
53
+ raise NotImplementedError
54
+ end
55
+
52
56
  def merge_metadata_sum(existing, incoming)
53
57
  existing = parse_json(existing)
54
58
  incoming = parse_json(incoming)
@@ -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)
@@ -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)
@@ -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
- names = app.middleware.filter_map { |m| m.name }.reject { |n| n.start_with?('Catpm::') }
20
- names.reverse_each do |name|
21
- app.middleware.insert_before(name, Catpm::MiddlewareProbe, name)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Catpm
4
- VERSION = '0.6.1'
4
+ VERSION = '0.6.3'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: catpm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''