catpm 0.8.4 → 0.9.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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/views/catpm/endpoints/show.html.erb +4 -2
- data/app/views/catpm/errors/index.html.erb +1 -1
- data/app/views/catpm/samples/show.html.erb +1 -2
- data/lib/catpm/collector.rb +149 -19
- data/lib/catpm/configuration.rb +2 -0
- data/lib/catpm/middleware.rb +8 -7
- data/lib/catpm/request_segments.rb +92 -2
- data/lib/catpm/trace.rb +24 -9
- 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: f73d14442e21e40a399d3df0635a640cf6067dc10f34e17a74054ab855ac212a
|
|
4
|
+
data.tar.gz: db34666075f0f5e2c2506a6e7ddad705673f8328ae22edcf1264ae0b94fe999a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16edf4257962cc5b52ee5207604d6f4643fd93b3219ef15aaf4fbaac14779fa09f49b22aff387a97796d9435b741820f4e9380e2e5276df4939b47af9202f01d
|
|
7
|
+
data.tar.gz: c5e5b2de2aa7994d827760f77ff7e30fa313d77a51b58573a5129a6a09010815ba30deade3603c15482076b6a4e3cd888123cea7dd78ba026db6fbeab6bc7bc3
|
data/README.md
CHANGED
|
@@ -72,11 +72,13 @@
|
|
|
72
72
|
<% end %>
|
|
73
73
|
|
|
74
74
|
<%
|
|
75
|
+
instrumented_count = (@metadata["_instrumented"] || @metadata[:"_instrumented"] || 0).to_f
|
|
76
|
+
instrumented_count = @count.to_f if instrumented_count == 0 # backward compat with pre-sampling data
|
|
75
77
|
type_data = segment_colors.map { |type, color|
|
|
76
78
|
count = (@metadata["#{type}_count"] || @metadata[:"#{type}_count"] || 0).to_f
|
|
77
79
|
dur = (@metadata["#{type}_duration"] || @metadata[:"#{type}_duration"] || 0).to_f
|
|
78
|
-
avg_dur =
|
|
79
|
-
avg_count =
|
|
80
|
+
avg_dur = instrumented_count > 0 ? dur / instrumented_count : 0
|
|
81
|
+
avg_count = instrumented_count > 0 ? count / instrumented_count : 0
|
|
80
82
|
text_color = segment_text_colors[type] || "#4b5563"
|
|
81
83
|
{ type: type, label: segment_labels[type] || type.capitalize, bg: color, text: text_color, count: count, dur: dur, avg_dur: avg_dur, avg_count: avg_count }
|
|
82
84
|
}.select { |d| d[:count] > 0 }
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<% end %>
|
|
16
16
|
<input type="text" class="search-input" id="error-search" placeholder="Search errors... (/)" oninput="filterByText('error-search','errors-table')">
|
|
17
17
|
<% if @tab == "active" && @active_count > 0 %>
|
|
18
|
-
<span style="display:inline; margin-left:auto"><%= button_to "Resolve all", catpm.resolve_all_errors_path, method: :post, class: "btn"
|
|
18
|
+
<span style="display:inline; margin-left:auto"><%= button_to "Resolve all", catpm.resolve_all_errors_path, method: :post, class: "btn" %></span>
|
|
19
19
|
<% end %>
|
|
20
20
|
</div>
|
|
21
21
|
<% end %>
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
<span>Sample #<%= @sample.id %></span>
|
|
18
18
|
</div>
|
|
19
19
|
<%= button_to "Delete Sample", catpm.sample_path(@sample),
|
|
20
|
-
method: :delete, class: "btn btn-danger"
|
|
21
|
-
data: { confirm: "Delete this sample? This cannot be undone." } %>
|
|
20
|
+
method: :delete, class: "btn btn-danger" %>
|
|
22
21
|
</div>
|
|
23
22
|
|
|
24
23
|
<%# ─── Request Info Bar ─── %>
|
data/lib/catpm/collector.rb
CHANGED
|
@@ -19,6 +19,8 @@ module Catpm
|
|
|
19
19
|
metadata = build_http_metadata(payload)
|
|
20
20
|
|
|
21
21
|
req_segments = Thread.current[:catpm_request_segments]
|
|
22
|
+
instrumented = !req_segments.nil?
|
|
23
|
+
|
|
22
24
|
if req_segments
|
|
23
25
|
segment_data = req_segments.to_h
|
|
24
26
|
|
|
@@ -28,8 +30,17 @@ module Catpm
|
|
|
28
30
|
|
|
29
31
|
# Segment summary is always needed for bucket metadata aggregation
|
|
30
32
|
segment_data[:segment_summary].each { |k, v| metadata[k] = v }
|
|
33
|
+
else
|
|
34
|
+
# Non-instrumented request — compute duration from thread-local start time
|
|
35
|
+
request_start = Thread.current[:catpm_request_start]
|
|
36
|
+
if request_start
|
|
37
|
+
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start) * 1000.0
|
|
38
|
+
end
|
|
31
39
|
end
|
|
32
40
|
|
|
41
|
+
# Track instrumented count for correct dashboard averaging
|
|
42
|
+
metadata[:_instrumented] = 1 if instrumented
|
|
43
|
+
|
|
33
44
|
# Early sampling decision — only build heavy context for sampled events
|
|
34
45
|
operation = payload[:method] || 'GET'
|
|
35
46
|
sample_type = early_sample_type(
|
|
@@ -37,9 +48,15 @@ module Catpm
|
|
|
37
48
|
duration: duration,
|
|
38
49
|
kind: :http,
|
|
39
50
|
target: target,
|
|
40
|
-
operation: operation
|
|
51
|
+
operation: operation,
|
|
52
|
+
instrumented: instrumented
|
|
41
53
|
)
|
|
42
54
|
|
|
55
|
+
# Slow spike detection: force instrument next request for this endpoint
|
|
56
|
+
if !instrumented && (payload[:exception] || duration >= Catpm.config.slow_threshold_for(:http))
|
|
57
|
+
trigger_force_instrument(kind: :http, target: target, operation: operation)
|
|
58
|
+
end
|
|
59
|
+
|
|
43
60
|
if sample_type
|
|
44
61
|
context = build_http_context(payload)
|
|
45
62
|
|
|
@@ -224,20 +241,30 @@ module Catpm
|
|
|
224
241
|
return if Catpm.config.ignored?(target)
|
|
225
242
|
|
|
226
243
|
metadata = (metadata || {}).dup
|
|
244
|
+
instrumented = !req_segments.nil?
|
|
227
245
|
|
|
228
246
|
if req_segments
|
|
229
247
|
segment_data = req_segments.to_h
|
|
230
248
|
segment_data[:segment_summary]&.each { |k, v| metadata[k] = v }
|
|
231
249
|
end
|
|
232
250
|
|
|
251
|
+
# Track instrumented count for correct dashboard averaging
|
|
252
|
+
metadata[:_instrumented] = 1 if instrumented
|
|
253
|
+
|
|
233
254
|
sample_type = early_sample_type(
|
|
234
255
|
error: error,
|
|
235
256
|
duration: duration,
|
|
236
257
|
kind: kind,
|
|
237
258
|
target: target,
|
|
238
|
-
operation: operation
|
|
259
|
+
operation: operation,
|
|
260
|
+
instrumented: instrumented
|
|
239
261
|
)
|
|
240
262
|
|
|
263
|
+
# Slow spike detection: force instrument next request for this endpoint
|
|
264
|
+
if !instrumented && (error || duration >= Catpm.config.slow_threshold_for(kind.to_sym))
|
|
265
|
+
trigger_force_instrument(kind: kind, target: target, operation: operation)
|
|
266
|
+
end
|
|
267
|
+
|
|
241
268
|
if sample_type
|
|
242
269
|
context = (context || {}).dup
|
|
243
270
|
|
|
@@ -341,6 +368,53 @@ module Catpm
|
|
|
341
368
|
Catpm.buffer&.push(ev)
|
|
342
369
|
end
|
|
343
370
|
|
|
371
|
+
def process_checkpoint(kind:, target:, operation:, context:, metadata:, checkpoint_data:, request_start:)
|
|
372
|
+
return unless Catpm.enabled?
|
|
373
|
+
|
|
374
|
+
segments = checkpoint_data[:segments].dup
|
|
375
|
+
collapse_code_wrappers(segments)
|
|
376
|
+
|
|
377
|
+
duration_so_far = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start) * 1000.0
|
|
378
|
+
|
|
379
|
+
# Inject root request segment
|
|
380
|
+
root_segment = {
|
|
381
|
+
type: 'request',
|
|
382
|
+
detail: "#{operation.presence || kind} #{target}",
|
|
383
|
+
duration: duration_so_far.round(2),
|
|
384
|
+
offset: 0.0
|
|
385
|
+
}
|
|
386
|
+
segments.each do |seg|
|
|
387
|
+
if seg.key?(:parent_index)
|
|
388
|
+
seg[:parent_index] += 1
|
|
389
|
+
else
|
|
390
|
+
seg[:parent_index] = 0
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
segments.unshift(root_segment)
|
|
394
|
+
|
|
395
|
+
checkpoint_context = (context || {}).dup
|
|
396
|
+
checkpoint_context[:segments] = segments
|
|
397
|
+
checkpoint_context[:segment_summary] = checkpoint_data[:summary]
|
|
398
|
+
checkpoint_context[:segments_capped] = checkpoint_data[:overflow]
|
|
399
|
+
checkpoint_context[:partial] = true
|
|
400
|
+
checkpoint_context[:checkpoint_number] = checkpoint_data[:checkpoint_number]
|
|
401
|
+
checkpoint_context = scrub(checkpoint_context)
|
|
402
|
+
|
|
403
|
+
ev = Event.new(
|
|
404
|
+
kind: kind,
|
|
405
|
+
target: target,
|
|
406
|
+
operation: operation.to_s,
|
|
407
|
+
duration: duration_so_far,
|
|
408
|
+
started_at: Time.current,
|
|
409
|
+
status: 200,
|
|
410
|
+
context: checkpoint_context,
|
|
411
|
+
sample_type: 'random',
|
|
412
|
+
metadata: (metadata || {}).dup.merge(checkpoint_data[:summary] || {})
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
Catpm.buffer&.push(ev)
|
|
416
|
+
end
|
|
417
|
+
|
|
344
418
|
def process_custom(name:, duration:, metadata: {}, error: nil, context: {})
|
|
345
419
|
return unless Catpm.enabled?
|
|
346
420
|
return if Catpm.config.ignored?(name)
|
|
@@ -361,7 +435,64 @@ module Catpm
|
|
|
361
435
|
Catpm.buffer&.push(ev)
|
|
362
436
|
end
|
|
363
437
|
|
|
364
|
-
|
|
438
|
+
# --- Pre-sampling: decide BEFORE request whether to instrument ---
|
|
439
|
+
|
|
440
|
+
# For HTTP middleware where endpoint is unknown at start.
|
|
441
|
+
# Returns true if this request should get full instrumentation.
|
|
442
|
+
def should_instrument_request?
|
|
443
|
+
# Force after slow spike detection
|
|
444
|
+
if (@force_instrument_count || 0) > 0
|
|
445
|
+
@force_instrument_count -= 1
|
|
446
|
+
return true
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
rand(Catpm.config.random_sample_rate) == 0
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# For track_request where endpoint is known at start.
|
|
453
|
+
# Filling phase ensures new endpoints get instrumented samples quickly.
|
|
454
|
+
def should_instrument?(kind, target, operation)
|
|
455
|
+
endpoint_key = [kind.to_s, target.to_s, (operation || '').to_s]
|
|
456
|
+
|
|
457
|
+
# Force after slow spike
|
|
458
|
+
if force_instrument_endpoints.delete(endpoint_key)
|
|
459
|
+
return true
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Filling phase — endpoint hasn't collected enough instrumented samples yet
|
|
463
|
+
max = Catpm.config.max_random_samples_per_endpoint
|
|
464
|
+
if max.nil? || instrumented_sample_counts[endpoint_key] < max
|
|
465
|
+
return true
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
rand(Catpm.config.random_sample_rate) == 0
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Called when a slow/error request had no instrumentation —
|
|
472
|
+
# forces the NEXT request(s) to be fully instrumented.
|
|
473
|
+
def trigger_force_instrument(kind: nil, target: nil, operation: nil)
|
|
474
|
+
if kind && target
|
|
475
|
+
endpoint_key = [kind.to_s, target.to_s, (operation || '').to_s]
|
|
476
|
+
force_instrument_endpoints[endpoint_key] = true
|
|
477
|
+
end
|
|
478
|
+
@force_instrument_count = (@force_instrument_count || 0) + 1
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def reset_sample_counts!
|
|
482
|
+
@instrumented_sample_counts = nil
|
|
483
|
+
@force_instrument_endpoints = nil
|
|
484
|
+
@force_instrument_count = nil
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
private
|
|
488
|
+
|
|
489
|
+
def force_instrument_endpoints
|
|
490
|
+
@force_instrument_endpoints ||= {}
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def instrumented_sample_counts
|
|
494
|
+
@instrumented_sample_counts ||= Hash.new(0)
|
|
495
|
+
end
|
|
365
496
|
|
|
366
497
|
# Remove near-zero-duration "code" spans that merely wrap a "controller" span.
|
|
367
498
|
# This happens when CallTracer (TracePoint) captures a thin dispatch method
|
|
@@ -417,31 +548,30 @@ module Catpm
|
|
|
417
548
|
end
|
|
418
549
|
|
|
419
550
|
# Determine sample type at event creation time so only sampled events
|
|
420
|
-
# carry full context in the buffer.
|
|
421
|
-
#
|
|
422
|
-
|
|
551
|
+
# carry full context in the buffer.
|
|
552
|
+
#
|
|
553
|
+
# When instrumented: false, only error/slow get a sample_type —
|
|
554
|
+
# non-instrumented normal requests just contribute duration/count.
|
|
555
|
+
# Filling counter only increments for instrumented requests so
|
|
556
|
+
# non-instrumented requests don't waste filling slots.
|
|
557
|
+
def early_sample_type(error:, duration:, kind:, target:, operation:, instrumented: true)
|
|
423
558
|
return 'error' if error
|
|
424
559
|
return 'slow' if duration >= Catpm.config.slow_threshold_for(kind.to_sym)
|
|
425
560
|
|
|
426
|
-
#
|
|
561
|
+
# Non-instrumented requests have no segments — skip sample creation
|
|
562
|
+
return nil unless instrumented
|
|
563
|
+
|
|
564
|
+
# Filling phase: always sample until endpoint has enough instrumented samples
|
|
427
565
|
endpoint_key = [kind.to_s, target, operation.to_s]
|
|
428
|
-
count =
|
|
566
|
+
count = instrumented_sample_counts[endpoint_key]
|
|
429
567
|
max_random = Catpm.config.max_random_samples_per_endpoint
|
|
430
568
|
if max_random.nil? || count < max_random
|
|
431
|
-
|
|
569
|
+
instrumented_sample_counts[endpoint_key] = count + 1
|
|
432
570
|
return 'random'
|
|
433
571
|
end
|
|
434
572
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def random_sample_counts
|
|
440
|
-
@random_sample_counts ||= Hash.new(0)
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
def reset_sample_counts!
|
|
444
|
-
@random_sample_counts = nil
|
|
573
|
+
# Instrumented request was already chosen by dice roll at start — always sample
|
|
574
|
+
'random'
|
|
445
575
|
end
|
|
446
576
|
|
|
447
577
|
def inject_gap_segments(segments, req_segments, gap, ctrl_idx, ctrl_seg)
|
data/lib/catpm/configuration.rb
CHANGED
|
@@ -43,6 +43,7 @@ module Catpm
|
|
|
43
43
|
events_max_samples_per_name max_stack_samples_per_request
|
|
44
44
|
max_error_detail_length max_fingerprint_app_frames
|
|
45
45
|
max_fingerprint_gem_frames cleanup_batch_size caller_scan_depth
|
|
46
|
+
max_request_memory
|
|
46
47
|
].freeze
|
|
47
48
|
|
|
48
49
|
(REQUIRED_NUMERIC + OPTIONAL_NUMERIC).each do |attr|
|
|
@@ -116,6 +117,7 @@ module Catpm
|
|
|
116
117
|
@max_fingerprint_gem_frames = 3
|
|
117
118
|
@cleanup_batch_size = 1_000
|
|
118
119
|
@caller_scan_depth = 50
|
|
120
|
+
@max_request_memory = 2.megabytes
|
|
119
121
|
@instrument_call_tree = false
|
|
120
122
|
@show_untracked_segments = false
|
|
121
123
|
end
|
data/lib/catpm/middleware.rb
CHANGED
|
@@ -12,14 +12,16 @@ module Catpm
|
|
|
12
12
|
Catpm.flusher&.ensure_running!
|
|
13
13
|
|
|
14
14
|
env['catpm.request_start'] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
15
|
+
Thread.current[:catpm_request_start] = env['catpm.request_start']
|
|
15
16
|
|
|
16
|
-
if Catpm.config.instrument_segments
|
|
17
|
+
if Catpm.config.instrument_segments && Collector.should_instrument_request?
|
|
17
18
|
use_sampler = Catpm.config.instrument_stack_sampler || Catpm.config.instrument_call_tree
|
|
18
19
|
req_segments = RequestSegments.new(
|
|
19
20
|
max_segments: Catpm.config.max_segments_per_request,
|
|
20
21
|
request_start: env['catpm.request_start'],
|
|
21
22
|
stack_sample: use_sampler,
|
|
22
|
-
call_tree: Catpm.config.instrument_call_tree
|
|
23
|
+
call_tree: Catpm.config.instrument_call_tree,
|
|
24
|
+
memory_limit: Catpm.config.max_request_memory
|
|
23
25
|
)
|
|
24
26
|
env['catpm.segments'] = req_segments
|
|
25
27
|
Thread.current[:catpm_request_segments] = req_segments
|
|
@@ -30,11 +32,10 @@ module Catpm
|
|
|
30
32
|
record_exception(env, e)
|
|
31
33
|
raise
|
|
32
34
|
ensure
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
35
|
+
req_segments&.stop_sampler
|
|
36
|
+
req_segments&.release!
|
|
37
|
+
Thread.current[:catpm_request_segments] = nil
|
|
38
|
+
Thread.current[:catpm_request_start] = nil
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
private
|
|
@@ -5,9 +5,13 @@ module Catpm
|
|
|
5
5
|
# Pre-computed symbol pairs — each type computed once per process lifetime.
|
|
6
6
|
SUMMARY_KEYS = Hash.new { |h, k| h[k] = [:"#{k}_count", :"#{k}_duration"] }
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# Per-segment byte estimate: Hash overhead + typical keys (type, duration, detail, offset, source, parent_index)
|
|
9
|
+
SEGMENT_BASE_BYTES = Event::OBJECT_OVERHEAD + (6 * Event::HASH_ENTRY_SIZE)
|
|
10
|
+
SEGMENT_STRING_OVERHEAD = Event::OBJECT_OVERHEAD # per-string overhead in segment values
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
attr_reader :segments, :summary, :request_start, :estimated_bytes, :checkpoint_count
|
|
13
|
+
|
|
14
|
+
def initialize(max_segments:, request_start: nil, stack_sample: false, call_tree: false, memory_limit: nil)
|
|
11
15
|
@max_segments = max_segments
|
|
12
16
|
@request_start = request_start || Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
13
17
|
@segments = []
|
|
@@ -16,6 +20,10 @@ module Catpm
|
|
|
16
20
|
@span_stack = []
|
|
17
21
|
@tracked_ranges = []
|
|
18
22
|
@call_tree = call_tree
|
|
23
|
+
@memory_limit = memory_limit
|
|
24
|
+
@estimated_bytes = 0
|
|
25
|
+
@checkpoint_callback = nil
|
|
26
|
+
@checkpoint_count = 0
|
|
19
27
|
|
|
20
28
|
if stack_sample
|
|
21
29
|
@sampler = StackSampler.new(target_thread: Thread.current, request_start: @request_start, call_tree: call_tree)
|
|
@@ -23,6 +31,10 @@ module Catpm
|
|
|
23
31
|
end
|
|
24
32
|
end
|
|
25
33
|
|
|
34
|
+
def on_checkpoint(&block)
|
|
35
|
+
@checkpoint_callback = block
|
|
36
|
+
end
|
|
37
|
+
|
|
26
38
|
def add(type:, duration:, detail:, source: nil, started_at: nil)
|
|
27
39
|
type_key = type.to_sym
|
|
28
40
|
count_key, dur_key = SUMMARY_KEYS[type_key]
|
|
@@ -50,6 +62,9 @@ module Catpm
|
|
|
50
62
|
@segments[min_idx] = segment
|
|
51
63
|
end
|
|
52
64
|
end
|
|
65
|
+
|
|
66
|
+
@estimated_bytes += estimate_segment_bytes(segment)
|
|
67
|
+
maybe_checkpoint
|
|
53
68
|
end
|
|
54
69
|
|
|
55
70
|
def push_span(type:, detail:, started_at: nil)
|
|
@@ -64,6 +79,7 @@ module Catpm
|
|
|
64
79
|
index = @segments.size
|
|
65
80
|
@segments << segment
|
|
66
81
|
@span_stack.push(index)
|
|
82
|
+
@estimated_bytes += estimate_segment_bytes(segment)
|
|
67
83
|
index
|
|
68
84
|
end
|
|
69
85
|
|
|
@@ -114,6 +130,7 @@ module Catpm
|
|
|
114
130
|
@summary = {}
|
|
115
131
|
@tracked_ranges = []
|
|
116
132
|
@sampler = nil
|
|
133
|
+
@estimated_bytes = 0
|
|
117
134
|
end
|
|
118
135
|
|
|
119
136
|
def overflowed?
|
|
@@ -127,5 +144,78 @@ module Catpm
|
|
|
127
144
|
segments_capped: @overflow
|
|
128
145
|
}
|
|
129
146
|
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def estimate_segment_bytes(segment)
|
|
151
|
+
bytes = SEGMENT_BASE_BYTES
|
|
152
|
+
bytes += segment[:detail].bytesize + SEGMENT_STRING_OVERHEAD if segment[:detail]
|
|
153
|
+
bytes += segment[:type].bytesize + SEGMENT_STRING_OVERHEAD if segment[:type]
|
|
154
|
+
bytes += segment[:source].bytesize + SEGMENT_STRING_OVERHEAD if segment[:source]
|
|
155
|
+
bytes
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def maybe_checkpoint
|
|
159
|
+
return unless @memory_limit && @estimated_bytes > @memory_limit && @checkpoint_callback
|
|
160
|
+
|
|
161
|
+
checkpoint_data = {
|
|
162
|
+
segments: @segments,
|
|
163
|
+
summary: @summary,
|
|
164
|
+
overflow: @overflow,
|
|
165
|
+
sampler_segments: @sampler ? sampler_segments_for_checkpoint : [],
|
|
166
|
+
checkpoint_number: @checkpoint_count
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@checkpoint_count += 1
|
|
170
|
+
rebuild_after_checkpoint
|
|
171
|
+
@checkpoint_callback.call(checkpoint_data)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def sampler_segments_for_checkpoint
|
|
175
|
+
if @call_tree
|
|
176
|
+
result = @sampler&.to_call_tree(tracked_ranges: @tracked_ranges) || []
|
|
177
|
+
else
|
|
178
|
+
result = @sampler&.to_segments(tracked_ranges: @tracked_ranges) || []
|
|
179
|
+
end
|
|
180
|
+
@sampler&.clear_samples!
|
|
181
|
+
result
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# After checkpoint: keep only active spans from @span_stack, reset everything else.
|
|
185
|
+
def rebuild_after_checkpoint
|
|
186
|
+
if @span_stack.any?
|
|
187
|
+
# Clone active spans with corrected indices
|
|
188
|
+
new_segments = []
|
|
189
|
+
old_to_new = {}
|
|
190
|
+
|
|
191
|
+
@span_stack.each do |old_idx|
|
|
192
|
+
seg = @segments[old_idx]
|
|
193
|
+
next unless seg
|
|
194
|
+
|
|
195
|
+
new_idx = new_segments.size
|
|
196
|
+
old_to_new[old_idx] = new_idx
|
|
197
|
+
new_segments << seg.dup
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Fix parent_index references in cloned spans
|
|
201
|
+
new_segments.each do |seg|
|
|
202
|
+
if seg.key?(:parent_index) && old_to_new.key?(seg[:parent_index])
|
|
203
|
+
seg[:parent_index] = old_to_new[seg[:parent_index]]
|
|
204
|
+
else
|
|
205
|
+
seg.delete(:parent_index)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
@span_stack = @span_stack.filter_map { |old_idx| old_to_new[old_idx] }
|
|
210
|
+
@segments = new_segments
|
|
211
|
+
else
|
|
212
|
+
@segments = []
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
@summary = Hash.new(0)
|
|
216
|
+
@tracked_ranges = []
|
|
217
|
+
@overflow = false
|
|
218
|
+
@estimated_bytes = 0
|
|
219
|
+
end
|
|
130
220
|
end
|
|
131
221
|
end
|
data/lib/catpm/trace.rb
CHANGED
|
@@ -76,15 +76,30 @@ module Catpm
|
|
|
76
76
|
owns_segments = false
|
|
77
77
|
|
|
78
78
|
if req_segments.nil? && config.instrument_segments
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
if Collector.should_instrument?(kind, target, operation)
|
|
80
|
+
use_sampler = config.instrument_stack_sampler || config.instrument_call_tree
|
|
81
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
82
|
+
req_segments = RequestSegments.new(
|
|
83
|
+
max_segments: config.max_segments_per_request,
|
|
84
|
+
request_start: start_time,
|
|
85
|
+
stack_sample: use_sampler,
|
|
86
|
+
call_tree: config.instrument_call_tree,
|
|
87
|
+
memory_limit: config.max_request_memory
|
|
88
|
+
)
|
|
89
|
+
Thread.current[:catpm_request_segments] = req_segments
|
|
90
|
+
owns_segments = true
|
|
91
|
+
|
|
92
|
+
if config.max_request_memory
|
|
93
|
+
req_segments.on_checkpoint do |checkpoint_data|
|
|
94
|
+
Collector.process_checkpoint(
|
|
95
|
+
kind: kind, target: target, operation: operation,
|
|
96
|
+
context: context, metadata: metadata,
|
|
97
|
+
checkpoint_data: checkpoint_data,
|
|
98
|
+
request_start: start_time
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
88
103
|
end
|
|
89
104
|
|
|
90
105
|
if req_segments
|
data/lib/catpm/version.rb
CHANGED