catpm 0.9.2 → 0.9.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/models/catpm/sample.rb +4 -0
- data/db/migrate/20250601000001_create_catpm_tables.rb +2 -0
- data/lib/catpm/adapter/base.rb +3 -1
- data/lib/catpm/collector.rb +29 -13
- data/lib/catpm/event.rb +3 -2
- data/lib/catpm/flusher.rb +136 -1
- data/lib/catpm/request_segments.rb +2 -1
- data/lib/catpm/trace.rb +2 -1
- data/lib/catpm/version.rb +1 -1
- data/lib/tasks/catpm_tasks.rake +8 -0
- 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: 10d81d0f093dc28f0f22a3bd9eda141ca89dfa44daacabf4fda1190c549681e2
|
|
4
|
+
data.tar.gz: 80c8cd25d825e3dc3eae16bf1fc58adddee4c5cfe5bf420ef1f663d0ca295d2e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aafdfdedafaa9558c06da29ce3fc6c7af9e4f3120b7c1556b8e5162cbd781e72b1d8613720078e2ae2eb33e2d24d0304922c3de04cc622aa6df17602299e8ad5
|
|
7
|
+
data.tar.gz: 9f9eb35a16e182155b215323beafa593af8617de530dc36a993187ba8d2783b1b324855a783b27c7d998d97dd6e59d40b4449df852a4f1b70563ffa8009190b7
|
data/README.md
CHANGED
data/app/models/catpm/sample.rb
CHANGED
|
@@ -14,6 +14,10 @@ module Catpm
|
|
|
14
14
|
scope :recent, ->(period = 1.hour) { where(recorded_at: period.ago..) }
|
|
15
15
|
scope :for_error, ->(fingerprint) { where(error_fingerprint: fingerprint) }
|
|
16
16
|
|
|
17
|
+
def self.request_id_supported?
|
|
18
|
+
column_names.include?('request_id')
|
|
19
|
+
end
|
|
20
|
+
|
|
17
21
|
def parsed_context
|
|
18
22
|
case context
|
|
19
23
|
when Hash then context
|
|
@@ -33,11 +33,13 @@ class CreateCatpmTables < ActiveRecord::Migration[8.0]
|
|
|
33
33
|
t.float :duration, null: false
|
|
34
34
|
t.json :context
|
|
35
35
|
t.string :error_fingerprint, limit: 64
|
|
36
|
+
t.string :request_id, limit: 16
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
add_index :catpm_samples, :recorded_at, name: 'idx_catpm_samples_time'
|
|
39
40
|
add_index :catpm_samples, [:kind, :recorded_at], name: 'idx_catpm_samples_kind_time'
|
|
40
41
|
add_index :catpm_samples, :error_fingerprint, name: 'idx_catpm_samples_error_fp'
|
|
42
|
+
add_index :catpm_samples, :request_id, name: 'idx_catpm_samples_request_id'
|
|
41
43
|
|
|
42
44
|
create_table :catpm_errors do |t|
|
|
43
45
|
t.string :fingerprint, null: false, limit: 64
|
data/lib/catpm/adapter/base.rb
CHANGED
|
@@ -29,7 +29,7 @@ module Catpm
|
|
|
29
29
|
bucket = bucket_map[sample_data[:bucket_key]]
|
|
30
30
|
next unless bucket
|
|
31
31
|
|
|
32
|
-
{
|
|
32
|
+
record = {
|
|
33
33
|
bucket_id: bucket.id,
|
|
34
34
|
kind: sample_data[:kind],
|
|
35
35
|
sample_type: sample_data[:sample_type],
|
|
@@ -38,6 +38,8 @@ module Catpm
|
|
|
38
38
|
context: sample_data[:context],
|
|
39
39
|
error_fingerprint: sample_data[:error_fingerprint]
|
|
40
40
|
}
|
|
41
|
+
record[:request_id] = sample_data[:request_id] if Catpm::Sample.request_id_supported?
|
|
42
|
+
record
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
Catpm::Sample.insert_all(records) if records.any?
|
data/lib/catpm/collector.rb
CHANGED
|
@@ -60,11 +60,19 @@ module Catpm
|
|
|
60
60
|
instrumented: instrumented
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
if !instrumented
|
|
67
|
-
|
|
63
|
+
# Force the NEXT HTTP request to be fully instrumented when this one
|
|
64
|
+
# wasn't. Covers slow/error spikes and filling phase (new endpoints that
|
|
65
|
+
# haven't collected enough instrumented samples yet).
|
|
66
|
+
if !instrumented
|
|
67
|
+
if payload[:exception] || duration >= Catpm.config.slow_threshold_for(:http)
|
|
68
|
+
trigger_force_instrument
|
|
69
|
+
else
|
|
70
|
+
max = Catpm.config.max_random_samples_per_endpoint
|
|
71
|
+
if max
|
|
72
|
+
endpoint_key = ['http', target, operation]
|
|
73
|
+
trigger_force_instrument if instrumented_sample_counts[endpoint_key] < max
|
|
74
|
+
end
|
|
75
|
+
end
|
|
68
76
|
end
|
|
69
77
|
|
|
70
78
|
if sample_type
|
|
@@ -360,6 +368,8 @@ module Catpm
|
|
|
360
368
|
context = nil
|
|
361
369
|
end
|
|
362
370
|
|
|
371
|
+
request_id = req_segments&.request_id
|
|
372
|
+
|
|
363
373
|
ev = Event.new(
|
|
364
374
|
kind: kind,
|
|
365
375
|
target: target,
|
|
@@ -372,13 +382,14 @@ module Catpm
|
|
|
372
382
|
metadata: metadata,
|
|
373
383
|
error_class: error&.class&.name,
|
|
374
384
|
error_message: error&.message,
|
|
375
|
-
backtrace: error&.backtrace
|
|
385
|
+
backtrace: error&.backtrace,
|
|
386
|
+
request_id: request_id
|
|
376
387
|
)
|
|
377
388
|
|
|
378
389
|
Catpm.buffer&.push(ev)
|
|
379
390
|
end
|
|
380
391
|
|
|
381
|
-
def process_checkpoint(kind:, target:, operation:, context:, metadata:, checkpoint_data:, request_start:)
|
|
392
|
+
def process_checkpoint(kind:, target:, operation:, context:, metadata:, checkpoint_data:, request_start:, request_id: nil)
|
|
382
393
|
return unless Catpm.enabled?
|
|
383
394
|
|
|
384
395
|
segments = checkpoint_data[:segments].dup
|
|
@@ -419,7 +430,8 @@ module Catpm
|
|
|
419
430
|
status: 200,
|
|
420
431
|
context: checkpoint_context,
|
|
421
432
|
sample_type: 'random',
|
|
422
|
-
metadata: (metadata || {}).dup.merge(checkpoint_data[:summary] || {})
|
|
433
|
+
metadata: (metadata || {}).dup.merge(checkpoint_data[:summary] || {}),
|
|
434
|
+
request_id: request_id
|
|
423
435
|
)
|
|
424
436
|
|
|
425
437
|
Catpm.buffer&.push(ev)
|
|
@@ -600,10 +612,12 @@ module Catpm
|
|
|
600
612
|
# Determine sample type at event creation time so only sampled events
|
|
601
613
|
# carry full context in the buffer.
|
|
602
614
|
#
|
|
603
|
-
#
|
|
604
|
-
#
|
|
605
|
-
#
|
|
606
|
-
#
|
|
615
|
+
# Non-instrumented requests never get a sample (they have no segments).
|
|
616
|
+
# Filling phase is handled by the caller via trigger_force_instrument,
|
|
617
|
+
# so the NEXT request gets full instrumentation with segments.
|
|
618
|
+
#
|
|
619
|
+
# Post-filling: non-instrumented requests just contribute duration/count
|
|
620
|
+
# to the bucket, no sample created.
|
|
607
621
|
def early_sample_type(error:, duration:, kind:, target:, operation:, instrumented: true)
|
|
608
622
|
# Errors: only create sample for instrumented requests (with segments).
|
|
609
623
|
# Non-instrumented errors are still tracked in error_groups via
|
|
@@ -617,7 +631,9 @@ module Catpm
|
|
|
617
631
|
# don't count towards filling phase (they have no segments).
|
|
618
632
|
return 'slow' if is_slow && !instrumented
|
|
619
633
|
|
|
620
|
-
# Non-instrumented requests have no segments — skip sample creation
|
|
634
|
+
# Non-instrumented requests have no segments — skip sample creation.
|
|
635
|
+
# Filling phase is handled by the caller via trigger_force_instrument
|
|
636
|
+
# so the NEXT request gets full instrumentation with segments.
|
|
621
637
|
return nil unless instrumented
|
|
622
638
|
|
|
623
639
|
# Count this instrumented request towards filling phase completion.
|
data/lib/catpm/event.rb
CHANGED
|
@@ -9,14 +9,14 @@ module Catpm
|
|
|
9
9
|
|
|
10
10
|
attr_accessor :kind, :target, :operation, :duration, :started_at,
|
|
11
11
|
:metadata, :error_class, :error_message, :backtrace,
|
|
12
|
-
:sample_type, :context, :status
|
|
12
|
+
:sample_type, :context, :status, :request_id
|
|
13
13
|
|
|
14
14
|
EMPTY_HASH = {}.freeze
|
|
15
15
|
private_constant :EMPTY_HASH
|
|
16
16
|
|
|
17
17
|
def initialize(kind:, target:, operation: '', duration: 0.0, started_at: nil,
|
|
18
18
|
metadata: nil, error_class: nil, error_message: nil, backtrace: nil,
|
|
19
|
-
sample_type: nil, context: nil, status: nil)
|
|
19
|
+
sample_type: nil, context: nil, status: nil, request_id: nil)
|
|
20
20
|
@kind = kind.to_s
|
|
21
21
|
@target = target.to_s
|
|
22
22
|
@operation = (operation || '').to_s
|
|
@@ -32,6 +32,7 @@ module Catpm
|
|
|
32
32
|
@sample_type = sample_type
|
|
33
33
|
@context = context
|
|
34
34
|
@status = status
|
|
35
|
+
@request_id = request_id
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def estimated_bytes
|
data/lib/catpm/flusher.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Catpm
|
|
4
4
|
class Flusher
|
|
5
5
|
ERROR_LOG_BACKTRACE_LINES = 5
|
|
6
|
+
PARTIAL_STALE_TIMEOUT = 600 # seconds — orphaned partial samples cleaned after 10 minutes
|
|
6
7
|
|
|
7
8
|
attr_reader :running
|
|
8
9
|
|
|
@@ -181,7 +182,8 @@ module Catpm
|
|
|
181
182
|
sample_type: sample_type,
|
|
182
183
|
recorded_at: event.started_at,
|
|
183
184
|
duration: event.duration,
|
|
184
|
-
context: event.context || {}
|
|
185
|
+
context: event.context || {},
|
|
186
|
+
request_id: event.request_id
|
|
185
187
|
}
|
|
186
188
|
sample_hash[:error_fingerprint] = error_fp if error_fp
|
|
187
189
|
samples << sample_hash
|
|
@@ -219,6 +221,8 @@ module Catpm
|
|
|
219
221
|
b
|
|
220
222
|
end
|
|
221
223
|
|
|
224
|
+
samples = merge_request_samples(samples)
|
|
225
|
+
|
|
222
226
|
[ buckets, samples, error_groups.values ]
|
|
223
227
|
end
|
|
224
228
|
|
|
@@ -346,6 +350,7 @@ module Catpm
|
|
|
346
350
|
@last_cleanup_at = Time.now
|
|
347
351
|
downsample_buckets
|
|
348
352
|
cleanup_expired_data if Catpm.config.retention_period
|
|
353
|
+
cleanup_orphaned_partials
|
|
349
354
|
Collector.reset_sample_counts!
|
|
350
355
|
end
|
|
351
356
|
|
|
@@ -489,6 +494,136 @@ module Catpm
|
|
|
489
494
|
combined.empty? ? nil : combined.serialize
|
|
490
495
|
end
|
|
491
496
|
|
|
497
|
+
def merge_request_samples(samples)
|
|
498
|
+
return samples unless Catpm::Sample.request_id_supported?
|
|
499
|
+
|
|
500
|
+
by_request = {} # request_id => { partials: [], final: nil }
|
|
501
|
+
regular = []
|
|
502
|
+
|
|
503
|
+
samples.each do |s|
|
|
504
|
+
rid = s[:request_id]
|
|
505
|
+
if rid
|
|
506
|
+
entry = (by_request[rid] ||= { partials: [], final: nil })
|
|
507
|
+
if s[:context].is_a?(Hash) && s[:context][:partial]
|
|
508
|
+
entry[:partials] << s
|
|
509
|
+
else
|
|
510
|
+
entry[:final] = s
|
|
511
|
+
end
|
|
512
|
+
else
|
|
513
|
+
regular << s
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
merged = []
|
|
518
|
+
by_request.each do |rid, entry|
|
|
519
|
+
if entry[:final]
|
|
520
|
+
# Merge in-batch partials
|
|
521
|
+
if entry[:partials].any?
|
|
522
|
+
merge_checkpoint_contexts(
|
|
523
|
+
entry[:final][:context],
|
|
524
|
+
entry[:partials].map { |p| p[:context] }
|
|
525
|
+
)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Merge cross-batch partials from DB
|
|
529
|
+
db_partials = Catpm::Sample.where(request_id: rid)
|
|
530
|
+
if db_partials.exists?
|
|
531
|
+
merge_checkpoint_contexts(
|
|
532
|
+
entry[:final][:context],
|
|
533
|
+
db_partials.map(&:parsed_context)
|
|
534
|
+
)
|
|
535
|
+
db_partials.delete_all
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Clear request_id so persisted final sample won't be treated as orphan
|
|
539
|
+
entry[:final][:request_id] = nil
|
|
540
|
+
merged << entry[:final]
|
|
541
|
+
else
|
|
542
|
+
# Only partials, no final yet — persist as-is
|
|
543
|
+
merged.concat(entry[:partials])
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
regular + merged
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def merge_checkpoint_contexts(final_ctx, checkpoint_ctxs)
|
|
551
|
+
final_segments = final_ctx[:segments] || final_ctx['segments']
|
|
552
|
+
return unless final_segments
|
|
553
|
+
|
|
554
|
+
final_ctrl_idx = final_segments.index { |s|
|
|
555
|
+
(s[:type] || s['type']) == 'controller'
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
sorted = checkpoint_ctxs.sort_by { |c|
|
|
559
|
+
c[:checkpoint_number] || c['checkpoint_number'] || 0
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
sorted.each do |cp_ctx|
|
|
563
|
+
cp_segments = cp_ctx[:segments] || cp_ctx['segments'] || []
|
|
564
|
+
|
|
565
|
+
old_to_new = {}
|
|
566
|
+
kept = []
|
|
567
|
+
|
|
568
|
+
cp_segments.each_with_index do |seg, i|
|
|
569
|
+
seg_type = seg[:type] || seg['type']
|
|
570
|
+
next if seg_type == 'request'
|
|
571
|
+
next if seg_type == 'controller'
|
|
572
|
+
old_to_new[i] = final_segments.size + kept.size
|
|
573
|
+
kept << seg.dup
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
kept.each do |seg|
|
|
577
|
+
pi_key = seg.key?(:parent_index) ? :parent_index : 'parent_index'
|
|
578
|
+
pi = seg[pi_key]
|
|
579
|
+
next unless pi
|
|
580
|
+
|
|
581
|
+
if old_to_new.key?(pi)
|
|
582
|
+
seg[pi_key] = old_to_new[pi]
|
|
583
|
+
else
|
|
584
|
+
seg[pi_key] = final_ctrl_idx || 0
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
final_segments.concat(kept)
|
|
589
|
+
|
|
590
|
+
# Merge summary
|
|
591
|
+
cp_summary = cp_ctx[:segment_summary] || cp_ctx['segment_summary']
|
|
592
|
+
if cp_summary
|
|
593
|
+
use_symbols = final_ctx.key?(:segment_summary)
|
|
594
|
+
summary_key = use_symbols ? :segment_summary : 'segment_summary'
|
|
595
|
+
final_summary = final_ctx[summary_key] ||= {}
|
|
596
|
+
cp_summary.each do |k, v|
|
|
597
|
+
nk = use_symbols ? k.to_sym : k.to_s
|
|
598
|
+
final_summary[nk] = (final_summary[nk] || 0) + v.to_f
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# Merge capped flag
|
|
603
|
+
capped_key = final_ctx.key?(:segments_capped) ? :segments_capped : 'segments_capped'
|
|
604
|
+
cp_capped = cp_ctx[:segments_capped] || cp_ctx['segments_capped']
|
|
605
|
+
final_ctx[capped_key] = true if cp_capped
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# Clean up checkpoint markers
|
|
609
|
+
final_ctx.delete(:partial)
|
|
610
|
+
final_ctx.delete('partial')
|
|
611
|
+
final_ctx.delete(:request_id)
|
|
612
|
+
final_ctx.delete('request_id')
|
|
613
|
+
final_ctx.delete(:checkpoint_number)
|
|
614
|
+
final_ctx.delete('checkpoint_number')
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def cleanup_orphaned_partials
|
|
618
|
+
return unless Catpm::Sample.request_id_supported?
|
|
619
|
+
|
|
620
|
+
Catpm::Sample.where.not(request_id: nil)
|
|
621
|
+
.where(recorded_at: ..PARTIAL_STALE_TIMEOUT.seconds.ago)
|
|
622
|
+
.delete_all
|
|
623
|
+
rescue => e
|
|
624
|
+
Catpm.config.error_handler&.call(e)
|
|
625
|
+
end
|
|
626
|
+
|
|
492
627
|
def cleanup_expired_data
|
|
493
628
|
cutoff = Catpm.config.retention_period.ago
|
|
494
629
|
batch_size = Catpm.config.cleanup_batch_size
|
|
@@ -9,7 +9,7 @@ module Catpm
|
|
|
9
9
|
SEGMENT_BASE_BYTES = Event::OBJECT_OVERHEAD + (6 * Event::HASH_ENTRY_SIZE)
|
|
10
10
|
SEGMENT_STRING_OVERHEAD = Event::OBJECT_OVERHEAD # per-string overhead in segment values
|
|
11
11
|
|
|
12
|
-
attr_reader :segments, :summary, :request_start, :estimated_bytes, :checkpoint_count
|
|
12
|
+
attr_reader :segments, :summary, :request_start, :estimated_bytes, :checkpoint_count, :request_id
|
|
13
13
|
|
|
14
14
|
def initialize(max_segments:, request_start: nil, stack_sample: false, call_tree: false, memory_limit: nil)
|
|
15
15
|
@max_segments = max_segments
|
|
@@ -24,6 +24,7 @@ module Catpm
|
|
|
24
24
|
@estimated_bytes = 0
|
|
25
25
|
@checkpoint_callback = nil
|
|
26
26
|
@checkpoint_count = 0
|
|
27
|
+
@request_id = memory_limit ? SecureRandom.hex(8) : nil
|
|
27
28
|
|
|
28
29
|
if stack_sample
|
|
29
30
|
@sampler = StackSampler.new(target_thread: Thread.current, request_start: @request_start, call_tree: call_tree)
|
data/lib/catpm/trace.rb
CHANGED
|
@@ -101,7 +101,8 @@ module Catpm
|
|
|
101
101
|
kind: kind, target: target, operation: operation,
|
|
102
102
|
context: context, metadata: metadata,
|
|
103
103
|
checkpoint_data: checkpoint_data,
|
|
104
|
-
request_start: start_time
|
|
104
|
+
request_start: start_time,
|
|
105
|
+
request_id: req_segments.request_id
|
|
105
106
|
)
|
|
106
107
|
end
|
|
107
108
|
end
|
data/lib/catpm/version.rb
CHANGED
data/lib/tasks/catpm_tasks.rake
CHANGED
|
@@ -42,6 +42,14 @@ namespace :catpm do
|
|
|
42
42
|
puts '[catpm] catpm_errors.pinned already exists, skipping'
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
unless connection.column_exists?(:catpm_samples, :request_id)
|
|
46
|
+
connection.add_column :catpm_samples, :request_id, :string, limit: 16
|
|
47
|
+
connection.add_index :catpm_samples, :request_id, name: 'idx_catpm_samples_request_id'
|
|
48
|
+
puts '[catpm] Added request_id column to catpm_samples'
|
|
49
|
+
else
|
|
50
|
+
puts '[catpm] catpm_samples.request_id already exists, skipping'
|
|
51
|
+
end
|
|
52
|
+
|
|
45
53
|
unless connection.table_exists?(:catpm_event_prefs)
|
|
46
54
|
connection.create_table :catpm_event_prefs do |t|
|
|
47
55
|
t.string :name, null: false
|