catpm 0.9.4 → 0.9.6
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 +2 -21
- data/app/controllers/catpm/system_controller.rb +2 -1
- data/app/helpers/catpm/application_helper.rb +2 -4
- data/app/models/catpm/sample.rb +0 -4
- data/app/views/catpm/samples/show.html.erb +2 -2
- data/app/views/catpm/shared/_segments_waterfall.html.erb +7 -1
- data/app/views/catpm/system/index.html.erb +1 -1
- data/lib/catpm/adapter/base.rb +0 -1
- data/lib/catpm/buffer.rb +7 -5
- data/lib/catpm/collector.rb +83 -70
- data/lib/catpm/configuration.rb +30 -10
- data/lib/catpm/engine.rb +0 -4
- data/lib/catpm/event.rb +2 -3
- data/lib/catpm/fingerprint.rb +1 -1
- data/lib/catpm/flusher.rb +77 -202
- data/lib/catpm/lifecycle.rb +8 -1
- data/lib/catpm/middleware.rb +3 -4
- data/lib/catpm/patches/httpclient.rb +1 -1
- data/lib/catpm/patches/net_http.rb +1 -1
- data/lib/catpm/request_segments.rb +18 -79
- data/lib/catpm/segment_subscribers.rb +5 -5
- data/lib/catpm/trace.rb +3 -16
- data/lib/catpm/version.rb +1 -1
- data/lib/catpm.rb +7 -1
- data/lib/generators/catpm/templates/initializer.rb.tt +4 -6
- metadata +1 -2
- data/lib/catpm/auto_instrument.rb +0 -145
data/lib/catpm/flusher.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
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
|
|
7
6
|
|
|
8
7
|
attr_reader :running
|
|
9
8
|
|
|
@@ -182,8 +181,7 @@ module Catpm
|
|
|
182
181
|
sample_type: sample_type,
|
|
183
182
|
recorded_at: event.started_at,
|
|
184
183
|
duration: event.duration,
|
|
185
|
-
context: event.context || {}
|
|
186
|
-
request_id: event.request_id
|
|
184
|
+
context: event.context || {}
|
|
187
185
|
}
|
|
188
186
|
sample_hash[:error_fingerprint] = error_fp if error_fp
|
|
189
187
|
samples << sample_hash
|
|
@@ -221,8 +219,6 @@ module Catpm
|
|
|
221
219
|
b
|
|
222
220
|
end
|
|
223
221
|
|
|
224
|
-
samples = merge_request_samples(samples)
|
|
225
|
-
|
|
226
222
|
[ buckets, samples, error_groups.values ]
|
|
227
223
|
end
|
|
228
224
|
|
|
@@ -350,7 +346,6 @@ module Catpm
|
|
|
350
346
|
@last_cleanup_at = Time.now
|
|
351
347
|
downsample_buckets
|
|
352
348
|
cleanup_expired_data if Catpm.config.retention_period
|
|
353
|
-
cleanup_orphaned_partials
|
|
354
349
|
Collector.reset_sample_counts!
|
|
355
350
|
end
|
|
356
351
|
|
|
@@ -398,83 +393,93 @@ module Catpm
|
|
|
398
393
|
cutoff = age_threshold.ago
|
|
399
394
|
target_seconds = target_interval.to_i
|
|
400
395
|
|
|
401
|
-
#
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
.
|
|
405
|
-
.each do |(_kind, _target, _operation), endpoint_buckets|
|
|
406
|
-
groups = endpoint_buckets.group_by do |bucket|
|
|
407
|
-
epoch = bucket.bucket_start.to_i
|
|
408
|
-
aligned_epoch = epoch - (epoch % target_seconds)
|
|
409
|
-
Time.at(aligned_epoch).utc
|
|
410
|
-
end
|
|
396
|
+
# Get unique endpoint keys first (small set), then process per-endpoint
|
|
397
|
+
# to avoid loading all old buckets into memory at once
|
|
398
|
+
endpoint_keys = Catpm::Bucket.where(bucket_start: ...cutoff)
|
|
399
|
+
.distinct.pluck(:kind, :target, :operation)
|
|
411
400
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
401
|
+
endpoint_keys.each do |kind, target, operation|
|
|
402
|
+
endpoint_buckets = Catpm::Bucket
|
|
403
|
+
.where(kind: kind, target: target, operation: operation, bucket_start: ...cutoff)
|
|
404
|
+
.select(:id, :bucket_start).to_a
|
|
405
|
+
|
|
406
|
+
groups = endpoint_buckets.group_by do |bucket|
|
|
407
|
+
epoch = bucket.bucket_start.to_i
|
|
408
|
+
aligned_epoch = epoch - (epoch % target_seconds)
|
|
409
|
+
Time.at(aligned_epoch).utc
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
groups.each do |aligned_start, stub_buckets|
|
|
413
|
+
next if stub_buckets.size == 1 && stub_buckets.first.bucket_start.to_i % target_seconds == 0
|
|
414
|
+
|
|
415
|
+
# Load full records only for groups that need merging
|
|
416
|
+
bucket_ids = stub_buckets.map(&:id)
|
|
417
|
+
buckets = Catpm::Bucket.where(id: bucket_ids).to_a
|
|
418
|
+
|
|
419
|
+
merged = {
|
|
420
|
+
kind: buckets.first.kind,
|
|
421
|
+
target: buckets.first.target,
|
|
422
|
+
operation: buckets.first.operation,
|
|
423
|
+
bucket_start: aligned_start,
|
|
424
|
+
count: buckets.sum(&:count),
|
|
425
|
+
success_count: buckets.sum(&:success_count),
|
|
426
|
+
failure_count: buckets.sum(&:failure_count),
|
|
427
|
+
duration_sum: buckets.sum(&:duration_sum),
|
|
428
|
+
duration_max: buckets.map(&:duration_max).max,
|
|
429
|
+
duration_min: buckets.map(&:duration_min).min,
|
|
430
|
+
metadata_sum: merge_bucket_metadata(buckets, adapter),
|
|
431
|
+
p95_digest: merge_bucket_digests(buckets)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
survivor = buckets.first
|
|
435
|
+
|
|
436
|
+
# Reassign all samples to the survivor bucket
|
|
437
|
+
Catpm::Sample.where(bucket_id: bucket_ids).update_all(bucket_id: survivor.id)
|
|
438
|
+
|
|
439
|
+
# Delete non-survivor source buckets (now sample-free)
|
|
440
|
+
Catpm::Bucket.where(id: bucket_ids - [survivor.id]).delete_all
|
|
441
|
+
|
|
442
|
+
# Overwrite survivor with merged data
|
|
443
|
+
survivor.update!(
|
|
444
|
+
bucket_start: aligned_start,
|
|
445
|
+
count: merged[:count],
|
|
446
|
+
success_count: merged[:success_count],
|
|
447
|
+
failure_count: merged[:failure_count],
|
|
448
|
+
duration_sum: merged[:duration_sum],
|
|
449
|
+
duration_max: merged[:duration_max],
|
|
450
|
+
duration_min: merged[:duration_min],
|
|
451
|
+
metadata_sum: merged[:metadata_sum],
|
|
452
|
+
p95_digest: merged[:p95_digest]
|
|
453
|
+
)
|
|
455
454
|
end
|
|
455
|
+
end
|
|
456
456
|
end
|
|
457
457
|
|
|
458
458
|
def downsample_event_tier(target_interval:, age_threshold:, adapter:)
|
|
459
459
|
cutoff = age_threshold.ago
|
|
460
460
|
target_seconds = target_interval.to_i
|
|
461
461
|
|
|
462
|
-
|
|
463
|
-
|
|
462
|
+
# Process per-name to avoid loading all event buckets into memory
|
|
463
|
+
names = Catpm::EventBucket.where(bucket_start: ...cutoff).distinct.pluck(:name)
|
|
464
|
+
return if names.empty?
|
|
464
465
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
aligned_start = Time.at(aligned_epoch).utc
|
|
469
|
-
[bucket.name, aligned_start]
|
|
470
|
-
end
|
|
466
|
+
names.each do |name|
|
|
467
|
+
buckets = Catpm::EventBucket.where(name: name, bucket_start: ...cutoff).to_a
|
|
468
|
+
next if buckets.empty?
|
|
471
469
|
|
|
472
|
-
|
|
473
|
-
|
|
470
|
+
groups = buckets.group_by do |bucket|
|
|
471
|
+
epoch = bucket.bucket_start.to_i
|
|
472
|
+
aligned_epoch = epoch - (epoch % target_seconds)
|
|
473
|
+
Time.at(aligned_epoch).utc
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
groups.each do |aligned_start, group_buckets|
|
|
477
|
+
next if group_buckets.size == 1 && group_buckets.first.bucket_start.to_i % target_seconds == 0
|
|
474
478
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
479
|
+
merged = { name: name, bucket_start: aligned_start, count: group_buckets.sum(&:count) }
|
|
480
|
+
Catpm::EventBucket.where(id: group_buckets.map(&:id)).delete_all
|
|
481
|
+
adapter.persist_event_buckets([merged])
|
|
482
|
+
end
|
|
478
483
|
end
|
|
479
484
|
end
|
|
480
485
|
|
|
@@ -494,136 +499,6 @@ module Catpm
|
|
|
494
499
|
combined.empty? ? nil : combined.serialize
|
|
495
500
|
end
|
|
496
501
|
|
|
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
|
-
|
|
627
502
|
def cleanup_expired_data
|
|
628
503
|
cutoff = Catpm.config.retention_period.ago
|
|
629
504
|
batch_size = Catpm.config.cleanup_batch_size
|
data/lib/catpm/lifecycle.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Catpm
|
|
|
8
8
|
|
|
9
9
|
initialize_buffer
|
|
10
10
|
initialize_flusher
|
|
11
|
+
load_sample_counts
|
|
11
12
|
apply_patches
|
|
12
13
|
|
|
13
14
|
# Start the flusher in the current process.
|
|
@@ -24,6 +25,12 @@ module Catpm
|
|
|
24
25
|
|
|
25
26
|
private
|
|
26
27
|
|
|
28
|
+
def load_sample_counts
|
|
29
|
+
Collector.load_sample_counts_eagerly!
|
|
30
|
+
rescue => e
|
|
31
|
+
Catpm.config.error_handler&.call(e)
|
|
32
|
+
end
|
|
33
|
+
|
|
27
34
|
def apply_patches
|
|
28
35
|
if Catpm.config.instrument_net_http
|
|
29
36
|
if defined?(::Net::HTTP)
|
|
@@ -39,7 +46,7 @@ module Catpm
|
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def initialize_buffer
|
|
42
|
-
Catpm.buffer ||= Buffer.new(max_bytes: Catpm.config.
|
|
49
|
+
Catpm.buffer ||= Buffer.new(max_bytes: Catpm.config.effective_max_buffer_memory)
|
|
43
50
|
end
|
|
44
51
|
|
|
45
52
|
def initialize_flusher
|
data/lib/catpm/middleware.rb
CHANGED
|
@@ -20,8 +20,7 @@ module Catpm
|
|
|
20
20
|
max_segments: Catpm.config.max_segments_per_request,
|
|
21
21
|
request_start: env['catpm.request_start'],
|
|
22
22
|
stack_sample: use_sampler,
|
|
23
|
-
call_tree: Catpm.config.instrument_call_tree
|
|
24
|
-
memory_limit: Catpm.config.max_request_memory
|
|
23
|
+
call_tree: Catpm.config.instrument_call_tree
|
|
25
24
|
)
|
|
26
25
|
env['catpm.segments'] = req_segments
|
|
27
26
|
Thread.current[:catpm_request_segments] = req_segments
|
|
@@ -47,10 +46,10 @@ module Catpm
|
|
|
47
46
|
ev = Event.new(
|
|
48
47
|
kind: :http,
|
|
49
48
|
target: target_from_env(env),
|
|
50
|
-
operation: env['REQUEST_METHOD'] || '
|
|
49
|
+
operation: env['REQUEST_METHOD'] || '',
|
|
51
50
|
duration: elapsed_ms(env),
|
|
52
51
|
started_at: Time.current,
|
|
53
|
-
status:
|
|
52
|
+
status: Collector::DEFAULT_ERROR_STATUS,
|
|
54
53
|
sample_type: 'error',
|
|
55
54
|
error_class: exception.class.name,
|
|
56
55
|
error_message: exception.message,
|
|
@@ -17,7 +17,7 @@ module Catpm
|
|
|
17
17
|
status = response.status rescue nil
|
|
18
18
|
detail = "#{http_method} #{uri.host}#{uri.path}"
|
|
19
19
|
detail += " (#{status})" if status
|
|
20
|
-
source =
|
|
20
|
+
source = Catpm.segment_storable?(duration) ? extract_catpm_source : nil
|
|
21
21
|
|
|
22
22
|
segments.add(
|
|
23
23
|
type: :http, duration: duration, detail: detail,
|
|
@@ -12,7 +12,7 @@ module Catpm
|
|
|
12
12
|
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0
|
|
13
13
|
|
|
14
14
|
detail = "#{req.method} #{@address}#{req.path} (#{response.code})"
|
|
15
|
-
source =
|
|
15
|
+
source = Catpm.segment_storable?(duration) ? extract_catpm_source : nil
|
|
16
16
|
|
|
17
17
|
segments.add(
|
|
18
18
|
type: :http, duration: duration, detail: detail,
|
|
@@ -9,9 +9,9 @@ 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, :
|
|
12
|
+
attr_reader :segments, :summary, :request_start, :estimated_bytes, :segments_filtered
|
|
13
13
|
|
|
14
|
-
def initialize(max_segments:, request_start: nil, stack_sample: false, call_tree: false
|
|
14
|
+
def initialize(max_segments:, request_start: nil, stack_sample: false, call_tree: false)
|
|
15
15
|
@max_segments = max_segments
|
|
16
16
|
@request_start = request_start || Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
17
17
|
@segments = []
|
|
@@ -20,11 +20,8 @@ module Catpm
|
|
|
20
20
|
@span_stack = []
|
|
21
21
|
@tracked_ranges = []
|
|
22
22
|
@call_tree = call_tree
|
|
23
|
-
@memory_limit = memory_limit
|
|
24
23
|
@estimated_bytes = 0
|
|
25
|
-
@
|
|
26
|
-
@checkpoint_count = 0
|
|
27
|
-
@request_id = memory_limit ? SecureRandom.hex(8) : nil
|
|
24
|
+
@segments_filtered = 0
|
|
28
25
|
|
|
29
26
|
if stack_sample
|
|
30
27
|
@sampler = StackSampler.new(target_thread: Thread.current, request_start: @request_start, call_tree: call_tree)
|
|
@@ -32,16 +29,24 @@ module Catpm
|
|
|
32
29
|
end
|
|
33
30
|
end
|
|
34
31
|
|
|
35
|
-
def on_checkpoint(&block)
|
|
36
|
-
@checkpoint_callback = block
|
|
37
|
-
end
|
|
38
|
-
|
|
39
32
|
def add(type:, duration:, detail:, source: nil, started_at: nil)
|
|
40
33
|
type_key = type.to_sym
|
|
41
34
|
count_key, dur_key = SUMMARY_KEYS[type_key]
|
|
42
35
|
@summary[count_key] += 1
|
|
43
36
|
@summary[dur_key] += duration
|
|
44
37
|
|
|
38
|
+
# Record time range so sampler can skip already-tracked periods
|
|
39
|
+
if started_at && duration > 0
|
|
40
|
+
@tracked_ranges << [started_at, started_at + duration / 1000.0]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Skip storing segment below minimum duration threshold —
|
|
44
|
+
# summary counters above are still updated for accurate Time Breakdown.
|
|
45
|
+
unless Catpm.segment_storable?(duration)
|
|
46
|
+
@segments_filtered += 1
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
45
50
|
offset = started_at ? ((started_at - @request_start) * 1000.0).round(2) : nil
|
|
46
51
|
|
|
47
52
|
segment = { type: type.to_s, duration: duration.round(2), detail: detail }
|
|
@@ -49,11 +54,6 @@ module Catpm
|
|
|
49
54
|
segment[:source] = source if source
|
|
50
55
|
segment[:parent_index] = @span_stack.last if @span_stack.any?
|
|
51
56
|
|
|
52
|
-
# Record time range so sampler can skip already-tracked periods
|
|
53
|
-
if started_at && duration > 0
|
|
54
|
-
@tracked_ranges << [started_at, started_at + duration / 1000.0]
|
|
55
|
-
end
|
|
56
|
-
|
|
57
57
|
if @max_segments.nil? || @segments.size < @max_segments
|
|
58
58
|
@segments << segment
|
|
59
59
|
else
|
|
@@ -65,7 +65,6 @@ module Catpm
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
@estimated_bytes += estimate_segment_bytes(segment)
|
|
68
|
-
maybe_checkpoint
|
|
69
68
|
end
|
|
70
69
|
|
|
71
70
|
def push_span(type:, detail:, started_at: nil)
|
|
@@ -132,6 +131,7 @@ module Catpm
|
|
|
132
131
|
@tracked_ranges = []
|
|
133
132
|
@sampler = nil
|
|
134
133
|
@estimated_bytes = 0
|
|
134
|
+
@segments_filtered = 0
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def overflowed?
|
|
@@ -142,7 +142,8 @@ module Catpm
|
|
|
142
142
|
{
|
|
143
143
|
segments: @segments,
|
|
144
144
|
segment_summary: @summary,
|
|
145
|
-
segments_capped: @overflow
|
|
145
|
+
segments_capped: @overflow,
|
|
146
|
+
segments_filtered: @segments_filtered
|
|
146
147
|
}
|
|
147
148
|
end
|
|
148
149
|
|
|
@@ -156,67 +157,5 @@ module Catpm
|
|
|
156
157
|
bytes
|
|
157
158
|
end
|
|
158
159
|
|
|
159
|
-
def maybe_checkpoint
|
|
160
|
-
return unless @memory_limit && @estimated_bytes > @memory_limit && @checkpoint_callback
|
|
161
|
-
|
|
162
|
-
checkpoint_data = {
|
|
163
|
-
segments: @segments,
|
|
164
|
-
summary: @summary,
|
|
165
|
-
overflow: @overflow,
|
|
166
|
-
sampler_segments: @sampler ? sampler_segments_for_checkpoint : [],
|
|
167
|
-
checkpoint_number: @checkpoint_count
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
@checkpoint_count += 1
|
|
171
|
-
rebuild_after_checkpoint
|
|
172
|
-
@checkpoint_callback.call(checkpoint_data)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def sampler_segments_for_checkpoint
|
|
176
|
-
if @call_tree
|
|
177
|
-
result = @sampler&.to_call_tree(tracked_ranges: @tracked_ranges) || []
|
|
178
|
-
else
|
|
179
|
-
result = @sampler&.to_segments(tracked_ranges: @tracked_ranges) || []
|
|
180
|
-
end
|
|
181
|
-
@sampler&.clear_samples!
|
|
182
|
-
result
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# After checkpoint: keep only active spans from @span_stack, reset everything else.
|
|
186
|
-
def rebuild_after_checkpoint
|
|
187
|
-
if @span_stack.any?
|
|
188
|
-
# Clone active spans with corrected indices
|
|
189
|
-
new_segments = []
|
|
190
|
-
old_to_new = {}
|
|
191
|
-
|
|
192
|
-
@span_stack.each do |old_idx|
|
|
193
|
-
seg = @segments[old_idx]
|
|
194
|
-
next unless seg
|
|
195
|
-
|
|
196
|
-
new_idx = new_segments.size
|
|
197
|
-
old_to_new[old_idx] = new_idx
|
|
198
|
-
new_segments << seg.dup
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# Fix parent_index references in cloned spans
|
|
202
|
-
new_segments.each do |seg|
|
|
203
|
-
if seg.key?(:parent_index) && old_to_new.key?(seg[:parent_index])
|
|
204
|
-
seg[:parent_index] = old_to_new[seg[:parent_index]]
|
|
205
|
-
else
|
|
206
|
-
seg.delete(:parent_index)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
@span_stack = @span_stack.filter_map { |old_idx| old_to_new[old_idx] }
|
|
211
|
-
@segments = new_segments
|
|
212
|
-
else
|
|
213
|
-
@segments = []
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
@summary = Hash.new(0)
|
|
217
|
-
@tracked_ranges = []
|
|
218
|
-
@overflow = false
|
|
219
|
-
@estimated_bytes = 0
|
|
220
|
-
end
|
|
221
160
|
end
|
|
222
161
|
end
|
|
@@ -155,7 +155,7 @@ module Catpm
|
|
|
155
155
|
record_count = payload[:record_count] || 0
|
|
156
156
|
class_name = payload[:class_name] || 'ActiveRecord'
|
|
157
157
|
detail = "#{class_name} x#{record_count}"
|
|
158
|
-
source =
|
|
158
|
+
source = Catpm.segment_storable?(duration) ? extract_source_location : nil
|
|
159
159
|
|
|
160
160
|
# Fold into sql summary for cleaner breakdown
|
|
161
161
|
req_segments.add(
|
|
@@ -176,7 +176,7 @@ module Catpm
|
|
|
176
176
|
sql = payload[:sql].to_s
|
|
177
177
|
max_len = Catpm.config.max_sql_length
|
|
178
178
|
sql = sql.truncate(max_len) if max_len && sql.length > max_len
|
|
179
|
-
source =
|
|
179
|
+
source = Catpm.segment_storable?(duration) ? extract_source_location : nil
|
|
180
180
|
|
|
181
181
|
req_segments.add(
|
|
182
182
|
type: :sql, duration: duration, detail: sql,
|
|
@@ -193,7 +193,7 @@ module Catpm
|
|
|
193
193
|
hit = event.payload[:hit]
|
|
194
194
|
detail = "cache.#{operation} #{key}"
|
|
195
195
|
detail += hit ? ' (hit)' : ' (miss)' if operation == 'read' && !hit.nil?
|
|
196
|
-
source =
|
|
196
|
+
source = Catpm.segment_storable?(duration) ? extract_source_location : nil
|
|
197
197
|
|
|
198
198
|
req_segments.add(
|
|
199
199
|
type: :cache, duration: duration, detail: detail,
|
|
@@ -209,7 +209,7 @@ module Catpm
|
|
|
209
209
|
mailer = payload[:mailer].to_s
|
|
210
210
|
to = Array(payload[:to]).first.to_s
|
|
211
211
|
detail = to.empty? ? mailer : "#{mailer} to #{to}"
|
|
212
|
-
source = event.duration
|
|
212
|
+
source = Catpm.segment_storable?(event.duration) ? extract_source_location : nil
|
|
213
213
|
|
|
214
214
|
req_segments.add(
|
|
215
215
|
type: :mailer, duration: event.duration, detail: detail,
|
|
@@ -224,7 +224,7 @@ module Catpm
|
|
|
224
224
|
payload = event.payload
|
|
225
225
|
key = payload[:key].to_s
|
|
226
226
|
detail = "#{operation} #{key}"
|
|
227
|
-
source = event.duration
|
|
227
|
+
source = Catpm.segment_storable?(event.duration) ? extract_source_location : nil
|
|
228
228
|
|
|
229
229
|
req_segments.add(
|
|
230
230
|
type: :storage, duration: event.duration, detail: detail,
|
data/lib/catpm/trace.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Catpm
|
|
|
47
47
|
|
|
48
48
|
req_segments = Thread.current[:catpm_request_segments]
|
|
49
49
|
if req_segments
|
|
50
|
-
source = duration_ms
|
|
50
|
+
source = segment_storable?(duration_ms) ? extract_trace_source : nil
|
|
51
51
|
req_segments.add(
|
|
52
52
|
type: :custom, duration: duration_ms, detail: name,
|
|
53
53
|
source: source, started_at: start_time
|
|
@@ -89,23 +89,10 @@ module Catpm
|
|
|
89
89
|
max_segments: config.max_segments_per_request,
|
|
90
90
|
request_start: start_time,
|
|
91
91
|
stack_sample: use_sampler,
|
|
92
|
-
call_tree: config.instrument_call_tree
|
|
93
|
-
memory_limit: config.max_request_memory
|
|
92
|
+
call_tree: config.instrument_call_tree
|
|
94
93
|
)
|
|
95
94
|
Thread.current[:catpm_request_segments] = req_segments
|
|
96
95
|
owns_segments = true
|
|
97
|
-
|
|
98
|
-
if config.max_request_memory
|
|
99
|
-
req_segments.on_checkpoint do |checkpoint_data|
|
|
100
|
-
Collector.process_checkpoint(
|
|
101
|
-
kind: kind, target: target, operation: operation,
|
|
102
|
-
context: context, metadata: metadata,
|
|
103
|
-
checkpoint_data: checkpoint_data,
|
|
104
|
-
request_start: start_time,
|
|
105
|
-
request_id: req_segments.request_id
|
|
106
|
-
)
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
96
|
end
|
|
110
97
|
end
|
|
111
98
|
|
|
@@ -162,7 +149,7 @@ module Catpm
|
|
|
162
149
|
|
|
163
150
|
req_segments = Thread.current[:catpm_request_segments]
|
|
164
151
|
if req_segments
|
|
165
|
-
source =
|
|
152
|
+
source = Catpm.segment_storable?(duration_ms) ? Catpm.send(:extract_trace_source) : nil
|
|
166
153
|
req_segments.add(
|
|
167
154
|
type: :custom, duration: duration_ms, detail: @name,
|
|
168
155
|
source: source, started_at: @start_time
|
data/lib/catpm/version.rb
CHANGED