catpm 0.8.3 → 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 -21
- 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
|
|
|
@@ -145,7 +162,6 @@ module Catpm
|
|
|
145
162
|
}
|
|
146
163
|
end
|
|
147
164
|
|
|
148
|
-
req_segments.release! # free internal state for GC
|
|
149
165
|
end
|
|
150
166
|
|
|
151
167
|
context = scrub(context)
|
|
@@ -225,20 +241,30 @@ module Catpm
|
|
|
225
241
|
return if Catpm.config.ignored?(target)
|
|
226
242
|
|
|
227
243
|
metadata = (metadata || {}).dup
|
|
244
|
+
instrumented = !req_segments.nil?
|
|
228
245
|
|
|
229
246
|
if req_segments
|
|
230
247
|
segment_data = req_segments.to_h
|
|
231
248
|
segment_data[:segment_summary]&.each { |k, v| metadata[k] = v }
|
|
232
249
|
end
|
|
233
250
|
|
|
251
|
+
# Track instrumented count for correct dashboard averaging
|
|
252
|
+
metadata[:_instrumented] = 1 if instrumented
|
|
253
|
+
|
|
234
254
|
sample_type = early_sample_type(
|
|
235
255
|
error: error,
|
|
236
256
|
duration: duration,
|
|
237
257
|
kind: kind,
|
|
238
258
|
target: target,
|
|
239
|
-
operation: operation
|
|
259
|
+
operation: operation,
|
|
260
|
+
instrumented: instrumented
|
|
240
261
|
)
|
|
241
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
|
+
|
|
242
268
|
if sample_type
|
|
243
269
|
context = (context || {}).dup
|
|
244
270
|
|
|
@@ -317,7 +343,6 @@ module Catpm
|
|
|
317
343
|
}
|
|
318
344
|
end
|
|
319
345
|
|
|
320
|
-
req_segments.release! # free internal state for GC
|
|
321
346
|
end
|
|
322
347
|
|
|
323
348
|
context = scrub(context)
|
|
@@ -343,6 +368,53 @@ module Catpm
|
|
|
343
368
|
Catpm.buffer&.push(ev)
|
|
344
369
|
end
|
|
345
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
|
+
|
|
346
418
|
def process_custom(name:, duration:, metadata: {}, error: nil, context: {})
|
|
347
419
|
return unless Catpm.enabled?
|
|
348
420
|
return if Catpm.config.ignored?(name)
|
|
@@ -363,7 +435,64 @@ module Catpm
|
|
|
363
435
|
Catpm.buffer&.push(ev)
|
|
364
436
|
end
|
|
365
437
|
|
|
366
|
-
|
|
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
|
|
367
496
|
|
|
368
497
|
# Remove near-zero-duration "code" spans that merely wrap a "controller" span.
|
|
369
498
|
# This happens when CallTracer (TracePoint) captures a thin dispatch method
|
|
@@ -419,31 +548,30 @@ module Catpm
|
|
|
419
548
|
end
|
|
420
549
|
|
|
421
550
|
# Determine sample type at event creation time so only sampled events
|
|
422
|
-
# carry full context in the buffer.
|
|
423
|
-
#
|
|
424
|
-
|
|
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)
|
|
425
558
|
return 'error' if error
|
|
426
559
|
return 'slow' if duration >= Catpm.config.slow_threshold_for(kind.to_sym)
|
|
427
560
|
|
|
428
|
-
#
|
|
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
|
|
429
565
|
endpoint_key = [kind.to_s, target, operation.to_s]
|
|
430
|
-
count =
|
|
566
|
+
count = instrumented_sample_counts[endpoint_key]
|
|
431
567
|
max_random = Catpm.config.max_random_samples_per_endpoint
|
|
432
568
|
if max_random.nil? || count < max_random
|
|
433
|
-
|
|
569
|
+
instrumented_sample_counts[endpoint_key] = count + 1
|
|
434
570
|
return 'random'
|
|
435
571
|
end
|
|
436
572
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
def random_sample_counts
|
|
442
|
-
@random_sample_counts ||= Hash.new(0)
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
def reset_sample_counts!
|
|
446
|
-
@random_sample_counts = nil
|
|
573
|
+
# Instrumented request was already chosen by dice roll at start — always sample
|
|
574
|
+
'random'
|
|
447
575
|
end
|
|
448
576
|
|
|
449
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