catpm 0.9.6 → 0.10.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/controllers/catpm/application_controller.rb +14 -2
- data/app/views/catpm/shared/not_found.html.erb +24 -0
- data/app/views/catpm/shared/record_not_found.html.erb +24 -0
- data/config/routes.rb +1 -0
- data/lib/catpm/collector.rb +33 -199
- data/lib/catpm/configuration.rb +41 -7
- data/lib/catpm/flusher.rb +0 -1
- data/lib/catpm/lifecycle.rb +0 -7
- data/lib/catpm/middleware.rb +1 -0
- data/lib/catpm/trace.rb +5 -4
- data/lib/catpm/version.rb +1 -1
- data/lib/catpm.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0283e922a2169d04e59405a273250c1fd226b4b72731f01ba6f6495b6a216ce2'
|
|
4
|
+
data.tar.gz: 5af58e9875ee9ce18afc02816e79d94cb025025218b78695e560a7a662ce2953
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0e73b3ea6819f8633104be971da72ad30c5de1b13bc4e0bece856d1e13b5987c705fb322f48c91622ae87d8acbd6b3d71ff0d2bb151939370a2685f4cf0ec7a7
|
|
7
|
+
data.tar.gz: 988f46467fbf05398f71cefff884584fb1daf2b148500df100b0824049d2cc659e5e74038e65c8de30cec27b36aaa6d503c5267475b8b4c7313e35b4e208e375
|
data/README.md
CHANGED
|
@@ -3,16 +3,28 @@
|
|
|
3
3
|
module Catpm
|
|
4
4
|
class ApplicationController < ActionController::Base
|
|
5
5
|
before_action :authenticate!
|
|
6
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
|
|
7
|
+
|
|
8
|
+
def not_found
|
|
9
|
+
render 'catpm/shared/not_found', layout: 'catpm/application', status: :not_found
|
|
10
|
+
end
|
|
6
11
|
|
|
7
12
|
private
|
|
8
13
|
|
|
14
|
+
def render_not_found
|
|
15
|
+
respond_to do |format|
|
|
16
|
+
format.html { render 'catpm/shared/record_not_found', layout: 'catpm/application', status: :not_found }
|
|
17
|
+
format.json { render json: { error: 'not_found' }, status: :not_found }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
9
21
|
def authenticate!
|
|
10
22
|
if Catpm.config.access_policy
|
|
11
23
|
unless Catpm.config.access_policy.call(request)
|
|
12
|
-
render plain:
|
|
24
|
+
render plain: 'Unauthorized', status: :unauthorized
|
|
13
25
|
end
|
|
14
26
|
elsif Catpm.config.http_basic_auth_user.present? && Catpm.config.http_basic_auth_password.present?
|
|
15
|
-
authenticate_or_request_with_http_basic(
|
|
27
|
+
authenticate_or_request_with_http_basic('Catpm') do |username, password|
|
|
16
28
|
ActiveSupport::SecurityUtils.secure_compare(username, Catpm.config.http_basic_auth_user) &
|
|
17
29
|
ActiveSupport::SecurityUtils.secure_compare(password, Catpm.config.http_basic_auth_password)
|
|
18
30
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<% content_for :head_extra do %>
|
|
2
|
+
<style>
|
|
3
|
+
body { display: flex; flex-direction: column; min-height: 100vh; }
|
|
4
|
+
.not-found-content { flex: 1; display: flex; align-items: center; justify-content: center; }
|
|
5
|
+
.not-found-inner { text-align: center; padding: 24px; }
|
|
6
|
+
.not-found-cat {
|
|
7
|
+
font-family: var(--font-mono); font-size: 14px; line-height: 1.3;
|
|
8
|
+
color: var(--accent); white-space: pre; margin-bottom: 20px; user-select: none;
|
|
9
|
+
}
|
|
10
|
+
.not-found-code { font-size: 13px; color: var(--text-2); font-weight: 500; letter-spacing: 1px; margin-bottom: 24px; }
|
|
11
|
+
.not-found-link { font-size: 13px; color: var(--accent); }
|
|
12
|
+
.not-found-link:hover { text-decoration: underline; }
|
|
13
|
+
</style>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<div class="not-found-content">
|
|
17
|
+
<div class="not-found-inner">
|
|
18
|
+
<div class="not-found-cat"> /\_/\
|
|
19
|
+
( o.o )
|
|
20
|
+
> ^ <</div>
|
|
21
|
+
<div class="not-found-code">404</div>
|
|
22
|
+
<a href="<%= catpm.status_index_path %>" class="not-found-link">← Dashboard</a>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<% content_for :head_extra do %>
|
|
2
|
+
<style>
|
|
3
|
+
body { display: flex; flex-direction: column; min-height: 100vh; }
|
|
4
|
+
.not-found-content { flex: 1; display: flex; align-items: center; justify-content: center; }
|
|
5
|
+
.not-found-inner { text-align: center; padding: 24px; }
|
|
6
|
+
.not-found-paws { font-size: 40px; color: var(--accent); margin-bottom: 20px; letter-spacing: 6px; }
|
|
7
|
+
.not-found-title { font-size: 15px; font-weight: 600; color: var(--text-0); margin-bottom: 6px; }
|
|
8
|
+
.not-found-hint { font-size: 13px; color: var(--text-2); max-width: 380px; margin: 0 auto 24px; line-height: 1.6; }
|
|
9
|
+
.not-found-link { font-size: 13px; color: var(--accent); }
|
|
10
|
+
.not-found-link:hover { text-decoration: underline; }
|
|
11
|
+
</style>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<div class="not-found-content">
|
|
15
|
+
<div class="not-found-inner">
|
|
16
|
+
<div class="not-found-paws">🐾</div>
|
|
17
|
+
<div class="not-found-title">This record is no longer available</div>
|
|
18
|
+
<p class="not-found-hint">
|
|
19
|
+
catpm rotates older performance data to keep memory usage low.
|
|
20
|
+
This resource was likely purged during a routine cleanup.
|
|
21
|
+
</p>
|
|
22
|
+
<a href="<%= catpm.status_index_path %>" class="not-found-link">← Dashboard</a>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
data/config/routes.rb
CHANGED
data/lib/catpm/collector.rb
CHANGED
|
@@ -6,11 +6,9 @@ module Catpm
|
|
|
6
6
|
MIN_GAP_MS = 1.0
|
|
7
7
|
DEFAULT_ERROR_STATUS = 500
|
|
8
8
|
DEFAULT_SUCCESS_STATUS = 200
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
MAX_FORCE_INSTRUMENT_COUNT = 3
|
|
13
|
-
FORCE_INSTRUMENT_MAX_ENDPOINTS = 100 # cap per-endpoint force-instrument hash
|
|
9
|
+
|
|
10
|
+
@instrumentation_mutex = Mutex.new
|
|
11
|
+
@active_instrumented_count = 0
|
|
14
12
|
|
|
15
13
|
class << self
|
|
16
14
|
def process_action_controller(event)
|
|
@@ -63,25 +61,6 @@ module Catpm
|
|
|
63
61
|
instrumented: instrumented
|
|
64
62
|
)
|
|
65
63
|
|
|
66
|
-
# Force the NEXT HTTP request to be fully instrumented when this one
|
|
67
|
-
# wasn't instrumented and was slow/error.
|
|
68
|
-
# Filling phase is handled by @http_filling_active flag in
|
|
69
|
-
# should_instrument_request? — no need for force_instrument here.
|
|
70
|
-
if !instrumented
|
|
71
|
-
if payload[:exception] || duration >= Catpm.config.slow_threshold_for(:http)
|
|
72
|
-
trigger_force_instrument
|
|
73
|
-
elsif !@http_filling_active
|
|
74
|
-
# Detect new/underfilled endpoints that appeared after filling phase ended
|
|
75
|
-
max = Catpm.config.max_random_samples_per_endpoint
|
|
76
|
-
if max
|
|
77
|
-
endpoint_key = ['http', target, operation]
|
|
78
|
-
if instrumented_sample_counts[endpoint_key] < max
|
|
79
|
-
@http_filling_active = true
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
64
|
if sample_type
|
|
86
65
|
context = build_http_context(payload)
|
|
87
66
|
|
|
@@ -286,11 +265,6 @@ module Catpm
|
|
|
286
265
|
instrumented: instrumented
|
|
287
266
|
)
|
|
288
267
|
|
|
289
|
-
# Slow spike detection: force instrument next request for this endpoint
|
|
290
|
-
if !instrumented && (error || duration >= Catpm.config.slow_threshold_for(kind.to_sym))
|
|
291
|
-
trigger_force_instrument(kind: kind, target: target, operation: operation)
|
|
292
|
-
end
|
|
293
|
-
|
|
294
268
|
if sample_type
|
|
295
269
|
context = (context || {}).dup
|
|
296
270
|
|
|
@@ -417,152 +391,49 @@ module Catpm
|
|
|
417
391
|
|
|
418
392
|
# --- Pre-sampling: decide BEFORE request whether to instrument ---
|
|
419
393
|
|
|
420
|
-
# Eagerly load sample counts at startup so old endpoints don't
|
|
421
|
-
# re-enter filling phase on every process restart.
|
|
422
|
-
# Called from Lifecycle.register_hooks after flusher init.
|
|
423
|
-
def load_sample_counts_eagerly!
|
|
424
|
-
@instrumented_sample_counts = load_sample_counts_from_db
|
|
425
|
-
@instrumented_sample_counts_loaded = true
|
|
426
|
-
recompute_http_filling_active
|
|
427
|
-
end
|
|
428
|
-
|
|
429
394
|
# For HTTP middleware where endpoint is unknown at start.
|
|
430
|
-
# Returns true if this request should get full instrumentation.
|
|
431
395
|
def should_instrument_request?
|
|
432
|
-
|
|
433
|
-
if (@force_instrument_count || 0) > 0
|
|
434
|
-
@force_instrument_count -= 1
|
|
435
|
-
return true
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
# During filling phase, instrument all requests so underfilled
|
|
439
|
-
# endpoints collect their quota (max_random_samples_per_endpoint).
|
|
440
|
-
# The flag is set by load_sample_counts_eagerly! and maintained
|
|
441
|
-
# by early_sample_type as endpoints fill up.
|
|
442
|
-
return true if @http_filling_active
|
|
443
|
-
|
|
444
|
-
rand(Catpm.config.random_sample_rate) == 0
|
|
396
|
+
rand(Catpm.config.random_sample_rate) == 0 && try_start_instrumentation
|
|
445
397
|
end
|
|
446
398
|
|
|
447
399
|
# For track_request where endpoint is known at start.
|
|
448
|
-
#
|
|
449
|
-
def should_instrument?(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
# Force after slow spike
|
|
453
|
-
if force_instrument_endpoints.delete(endpoint_key)
|
|
454
|
-
return true
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
# Filling phase — endpoint hasn't collected enough instrumented samples yet
|
|
458
|
-
max = Catpm.config.max_random_samples_per_endpoint
|
|
459
|
-
if max.nil? || instrumented_sample_counts[endpoint_key] < max
|
|
460
|
-
return true
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
rand(Catpm.config.random_sample_rate) == 0
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
# Called when a slow/error request had no instrumentation —
|
|
467
|
-
# forces the NEXT request(s) to be fully instrumented.
|
|
468
|
-
#
|
|
469
|
-
# Two modes (mutually exclusive to avoid double-instrumentation):
|
|
470
|
-
# - With endpoint: sets per-endpoint flag consumed by should_instrument?
|
|
471
|
-
# (for track_request paths where endpoint is known)
|
|
472
|
-
# - Without endpoint: increments global counter consumed by
|
|
473
|
-
# should_instrument_request? (for middleware path where endpoint is unknown)
|
|
474
|
-
def trigger_force_instrument(kind: nil, target: nil, operation: nil)
|
|
475
|
-
if kind && target
|
|
476
|
-
endpoint_key = [kind.to_s, target.to_s, (operation || '').to_s]
|
|
477
|
-
if force_instrument_endpoints.size < FORCE_INSTRUMENT_MAX_ENDPOINTS
|
|
478
|
-
force_instrument_endpoints[endpoint_key] = true
|
|
479
|
-
end
|
|
400
|
+
# Always instruments targets matched by always_sample_targets.
|
|
401
|
+
def should_instrument?(_kind, target, _operation)
|
|
402
|
+
if Catpm.config.always_sample?(target)
|
|
403
|
+
try_start_instrumentation
|
|
480
404
|
else
|
|
481
|
-
|
|
405
|
+
rand(Catpm.config.random_sample_rate) == 0 && try_start_instrumentation
|
|
482
406
|
end
|
|
483
407
|
end
|
|
484
408
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
@
|
|
488
|
-
@force_instrument_endpoints = nil
|
|
489
|
-
@force_instrument_count = nil
|
|
490
|
-
@http_filling_active = false
|
|
409
|
+
# Reset the concurrency counter (for tests and config reloads).
|
|
410
|
+
def reset_instrumentation_counter!
|
|
411
|
+
@instrumentation_mutex.synchronize { @active_instrumented_count = 0 }
|
|
491
412
|
end
|
|
492
413
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
max = Catpm.config.max_random_samples_per_endpoint
|
|
499
|
-
@http_filling_active = if max
|
|
500
|
-
# True if hash is empty (new app / new endpoints may appear) or any endpoint below quota
|
|
501
|
-
instrumented_sample_counts.empty? || instrumented_sample_counts.any? { |_, c| c < max }
|
|
502
|
-
else
|
|
503
|
-
false # unlimited quota → no filling phase for HTTP middleware
|
|
414
|
+
# Release an instrumentation slot. Must be called in ensure block
|
|
415
|
+
# for every request where try_start_instrumentation returned true.
|
|
416
|
+
def end_instrumentation
|
|
417
|
+
@instrumentation_mutex.synchronize do
|
|
418
|
+
@active_instrumented_count = [@active_instrumented_count - 1, 0].max
|
|
504
419
|
end
|
|
505
420
|
end
|
|
506
421
|
|
|
507
|
-
#
|
|
508
|
-
#
|
|
509
|
-
#
|
|
510
|
-
def
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
unfilled_keys = []
|
|
515
|
-
instrumented_sample_counts.each do |k, c|
|
|
516
|
-
(c >= max_random ? filled_keys : unfilled_keys) << k
|
|
517
|
-
end
|
|
518
|
-
# Evict filled first (safe), then unfilled if needed
|
|
519
|
-
to_evict = (filled_keys + unfilled_keys).first(evict_count)
|
|
520
|
-
to_evict.each { |k| instrumented_sample_counts.delete(k) }
|
|
521
|
-
else
|
|
522
|
-
evict_count.times { instrumented_sample_counts.shift }
|
|
523
|
-
end
|
|
524
|
-
end
|
|
525
|
-
|
|
526
|
-
def force_instrument_endpoints
|
|
527
|
-
@force_instrument_endpoints ||= {}
|
|
528
|
-
end
|
|
422
|
+
# Atomically claim an instrumentation slot if within budget.
|
|
423
|
+
# Returns true if the slot was acquired, false if at capacity.
|
|
424
|
+
# Already-running instrumented requests are not affected.
|
|
425
|
+
def try_start_instrumentation
|
|
426
|
+
@instrumentation_mutex.synchronize do
|
|
427
|
+
max = Catpm.config.effective_max_concurrent_instrumented
|
|
428
|
+
return false if @active_instrumented_count >= max
|
|
529
429
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
@instrumented_sample_counts = load_sample_counts_from_db
|
|
534
|
-
@instrumented_sample_counts_loaded = true
|
|
535
|
-
@instrumented_sample_counts
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
# Pre-populate filling counters from DB so old endpoints don't
|
|
539
|
-
# re-enter filling phase on every process restart.
|
|
540
|
-
# Temporarily clears thread-local to prevent our query from being
|
|
541
|
-
# captured as a segment in any active request.
|
|
542
|
-
def load_sample_counts_from_db
|
|
543
|
-
counts = Hash.new(0)
|
|
544
|
-
return counts unless defined?(Catpm::Sample) && Catpm::Bucket.table_exists?
|
|
545
|
-
|
|
546
|
-
saved_rs = Thread.current[:catpm_request_segments]
|
|
547
|
-
Thread.current[:catpm_request_segments] = nil
|
|
548
|
-
begin
|
|
549
|
-
Catpm::Sample.joins(:bucket)
|
|
550
|
-
.where(sample_type: 'random')
|
|
551
|
-
.group('catpm_buckets.kind', 'catpm_buckets.target', 'catpm_buckets.operation')
|
|
552
|
-
.count
|
|
553
|
-
.each do |(kind, target, operation), count|
|
|
554
|
-
counts[[kind.to_s, target.to_s, operation.to_s]] = count
|
|
555
|
-
end
|
|
556
|
-
ensure
|
|
557
|
-
Thread.current[:catpm_request_segments] = saved_rs
|
|
430
|
+
@active_instrumented_count += 1
|
|
431
|
+
true
|
|
558
432
|
end
|
|
559
|
-
|
|
560
|
-
counts
|
|
561
|
-
rescue => e
|
|
562
|
-
Catpm.config.error_handler&.call(e)
|
|
563
|
-
Hash.new(0)
|
|
564
433
|
end
|
|
565
434
|
|
|
435
|
+
private
|
|
436
|
+
|
|
566
437
|
# Remove near-zero-duration "code" spans that merely wrap a "controller" span.
|
|
567
438
|
# This happens when CallTracer (TracePoint) captures a thin dispatch method
|
|
568
439
|
# (e.g. Telegram::WebhookController#process) whose :return fires before the
|
|
@@ -618,49 +489,12 @@ module Catpm
|
|
|
618
489
|
|
|
619
490
|
# Determine sample type at event creation time so only sampled events
|
|
620
491
|
# carry full context in the buffer.
|
|
621
|
-
#
|
|
622
|
-
# Non-instrumented requests never get a sample (they have no segments).
|
|
623
|
-
# Filling phase is handled by the caller via trigger_force_instrument,
|
|
624
|
-
# so the NEXT request gets full instrumentation with segments.
|
|
625
|
-
#
|
|
626
|
-
# Post-filling: non-instrumented requests just contribute duration/count
|
|
627
|
-
# to the bucket, no sample created.
|
|
492
|
+
# Non-instrumented requests have no segments — skip sample creation.
|
|
628
493
|
def early_sample_type(error:, duration:, kind:, target:, operation:, instrumented: true)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return 'error' if error && instrumented
|
|
634
|
-
|
|
635
|
-
is_slow = duration >= Catpm.config.slow_threshold_for(kind.to_sym)
|
|
636
|
-
|
|
637
|
-
# Non-instrumented requests have no segments — skip sample creation.
|
|
638
|
-
# Slow/error spikes are handled by the caller via trigger_force_instrument
|
|
639
|
-
# so the NEXT request gets full instrumentation with useful segments.
|
|
640
|
-
return nil unless instrumented
|
|
641
|
-
|
|
642
|
-
# Count this instrumented request towards filling phase completion.
|
|
643
|
-
# Both slow and random requests count — without this, endpoints where
|
|
644
|
-
# most requests exceed slow_threshold would never exit the filling phase,
|
|
645
|
-
# causing 100% instrumentation regardless of random_sample_rate.
|
|
646
|
-
endpoint_key = [kind.to_s, target, operation.to_s]
|
|
647
|
-
count = instrumented_sample_counts[endpoint_key]
|
|
648
|
-
max_random = Catpm.config.max_random_samples_per_endpoint
|
|
649
|
-
if max_random.nil? || count < max_random
|
|
650
|
-
# Evict when hash exceeds derived limit — prefer filled entries
|
|
651
|
-
max_entries = Catpm.config.effective_sample_counts_max
|
|
652
|
-
if instrumented_sample_counts.size >= max_entries
|
|
653
|
-
evict_sample_counts(max_random)
|
|
654
|
-
end
|
|
655
|
-
instrumented_sample_counts[endpoint_key] = count + 1
|
|
656
|
-
|
|
657
|
-
# Endpoint just reached quota — recheck if any filling endpoints remain
|
|
658
|
-
if max_random && count + 1 >= max_random
|
|
659
|
-
recompute_http_filling_active
|
|
660
|
-
end
|
|
661
|
-
end
|
|
662
|
-
|
|
663
|
-
return 'slow' if is_slow
|
|
494
|
+
always = Catpm.config.always_sample?(target)
|
|
495
|
+
return 'error' if error && (instrumented || always)
|
|
496
|
+
return nil unless instrumented || always
|
|
497
|
+
return 'slow' if duration >= Catpm.config.slow_threshold_for(kind.to_sym)
|
|
664
498
|
|
|
665
499
|
'random'
|
|
666
500
|
end
|
data/lib/catpm/configuration.rb
CHANGED
|
@@ -9,8 +9,12 @@ module Catpm
|
|
|
9
9
|
BUFFER_MEMORY_SHARE = 0.5 # 50% of max_memory for event buffer
|
|
10
10
|
CACHE_ENTRIES_PER_MB = 10_000 # ~100 bytes/entry in path_cache
|
|
11
11
|
PATH_CACHE_BUDGET_SHARE = 0.05 # 5% of max_memory for path_cache
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
INSTRUMENTATION_BUDGET_SHARE = 0.30 # 30% of max_memory for concurrent instrumented requests
|
|
13
|
+
|
|
14
|
+
# Estimated per-request memory for instrumentation budget
|
|
15
|
+
ESTIMATED_BYTES_PER_STACK_SAMPLE = 10_000 # ~10 KB: backtrace_locations array (~100 frames × ~100 bytes)
|
|
16
|
+
ESTIMATED_BYTES_PER_SEGMENT = 500 # segment hash with strings
|
|
17
|
+
DEFAULT_SEGMENTS_ESTIMATE = 200 # used when max_segments_per_request is nil
|
|
14
18
|
|
|
15
19
|
# Boolean / non-numeric settings — plain attr_accessor
|
|
16
20
|
attr_accessor :enabled,
|
|
@@ -32,7 +36,8 @@ module Catpm
|
|
|
32
36
|
:events_enabled,
|
|
33
37
|
:track_own_requests,
|
|
34
38
|
:downsampling_thresholds,
|
|
35
|
-
:show_untracked_segments
|
|
39
|
+
:show_untracked_segments,
|
|
40
|
+
:always_sample_targets
|
|
36
41
|
|
|
37
42
|
# Numeric settings that must be positive numbers (nil not allowed)
|
|
38
43
|
REQUIRED_NUMERIC = %i[
|
|
@@ -124,6 +129,7 @@ module Catpm
|
|
|
124
129
|
@caller_scan_depth = 50
|
|
125
130
|
@instrument_call_tree = false
|
|
126
131
|
@show_untracked_segments = false
|
|
132
|
+
@always_sample_targets = []
|
|
127
133
|
end
|
|
128
134
|
|
|
129
135
|
# Buffer gets BUFFER_MEMORY_SHARE of max_memory, scaled by thread count
|
|
@@ -137,9 +143,13 @@ module Catpm
|
|
|
137
143
|
(max_memory * CACHE_ENTRIES_PER_MB * PATH_CACHE_BUDGET_SHARE).to_i
|
|
138
144
|
end
|
|
139
145
|
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
# Max concurrent instrumented requests derived from max_memory.
|
|
147
|
+
# Each instrumented request holds stack samples + segments in memory
|
|
148
|
+
# until the request completes. This limits how many can run at once
|
|
149
|
+
# so total live instrumentation data stays within budget.
|
|
150
|
+
def effective_max_concurrent_instrumented
|
|
151
|
+
budget_bytes = max_memory * 1_048_576 * INSTRUMENTATION_BUDGET_SHARE
|
|
152
|
+
(budget_bytes / estimated_instrumented_request_bytes).clamp(1, 1000).to_i
|
|
143
153
|
end
|
|
144
154
|
|
|
145
155
|
def slow_threshold_for(kind)
|
|
@@ -147,7 +157,17 @@ module Catpm
|
|
|
147
157
|
end
|
|
148
158
|
|
|
149
159
|
def ignored?(target)
|
|
150
|
-
ignored_targets
|
|
160
|
+
match_target?(ignored_targets, target)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def always_sample?(target)
|
|
164
|
+
match_target?(always_sample_targets, target)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def match_target?(patterns, target)
|
|
170
|
+
patterns.any? do |pattern|
|
|
151
171
|
case pattern
|
|
152
172
|
when Regexp then pattern.match?(target)
|
|
153
173
|
when String
|
|
@@ -159,5 +179,19 @@ module Catpm
|
|
|
159
179
|
end
|
|
160
180
|
end
|
|
161
181
|
end
|
|
182
|
+
|
|
183
|
+
def estimated_instrumented_request_bytes
|
|
184
|
+
bytes = 0
|
|
185
|
+
|
|
186
|
+
if instrument_stack_sampler || instrument_call_tree
|
|
187
|
+
samples = max_stack_samples_per_request || StackSampler::HARD_SAMPLE_CAP
|
|
188
|
+
bytes += samples * ESTIMATED_BYTES_PER_STACK_SAMPLE
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
segments = max_segments_per_request || DEFAULT_SEGMENTS_ESTIMATE
|
|
192
|
+
bytes += segments * ESTIMATED_BYTES_PER_SEGMENT
|
|
193
|
+
|
|
194
|
+
[bytes, 50_000].max # minimum 50 KB estimate per request
|
|
195
|
+
end
|
|
162
196
|
end
|
|
163
197
|
end
|
data/lib/catpm/flusher.rb
CHANGED
data/lib/catpm/lifecycle.rb
CHANGED
|
@@ -8,7 +8,6 @@ module Catpm
|
|
|
8
8
|
|
|
9
9
|
initialize_buffer
|
|
10
10
|
initialize_flusher
|
|
11
|
-
load_sample_counts
|
|
12
11
|
apply_patches
|
|
13
12
|
|
|
14
13
|
# Start the flusher in the current process.
|
|
@@ -25,12 +24,6 @@ module Catpm
|
|
|
25
24
|
|
|
26
25
|
private
|
|
27
26
|
|
|
28
|
-
def load_sample_counts
|
|
29
|
-
Collector.load_sample_counts_eagerly!
|
|
30
|
-
rescue => e
|
|
31
|
-
Catpm.config.error_handler&.call(e)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
27
|
def apply_patches
|
|
35
28
|
if Catpm.config.instrument_net_http
|
|
36
29
|
if defined?(::Net::HTTP)
|
data/lib/catpm/middleware.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Catpm
|
|
|
33
33
|
ensure
|
|
34
34
|
req_segments&.stop_sampler
|
|
35
35
|
req_segments&.release!
|
|
36
|
+
Collector.end_instrumentation if req_segments
|
|
36
37
|
Thread.current[:catpm_request_segments] = nil
|
|
37
38
|
Thread.current[:catpm_request_start] = nil
|
|
38
39
|
Thread.current[:catpm_tracked_instrumented] = nil
|
data/lib/catpm/trace.rb
CHANGED
|
@@ -75,14 +75,15 @@ module Catpm
|
|
|
75
75
|
# process_update(...)
|
|
76
76
|
# end
|
|
77
77
|
#
|
|
78
|
-
def self.track_request(kind: :http, target:, operation: '', context: {}, metadata: {})
|
|
78
|
+
def self.track_request(kind: :http, target:, operation: '', context: {}, metadata: {}, always_sample: false)
|
|
79
79
|
return yield unless enabled?
|
|
80
80
|
|
|
81
81
|
req_segments = Thread.current[:catpm_request_segments]
|
|
82
82
|
owns_segments = false
|
|
83
83
|
|
|
84
84
|
if req_segments.nil? && config.instrument_segments
|
|
85
|
-
|
|
85
|
+
force = always_sample || config.always_sample?(target)
|
|
86
|
+
if force ? Collector.try_start_instrumentation : Collector.should_instrument?(kind, target, operation)
|
|
86
87
|
use_sampler = config.instrument_stack_sampler || config.instrument_call_tree
|
|
87
88
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
88
89
|
req_segments = RequestSegments.new(
|
|
@@ -122,11 +123,11 @@ module Catpm
|
|
|
122
123
|
|
|
123
124
|
if owns_segments
|
|
124
125
|
req_segments&.release!
|
|
126
|
+
Collector.end_instrumentation
|
|
125
127
|
Thread.current[:catpm_request_segments] = nil
|
|
126
128
|
# Mark that this request was already instrumented and processed by
|
|
127
129
|
# track_request. Without this, process_action_controller would see
|
|
128
|
-
# nil req_segments and
|
|
129
|
-
# requests — even though they were fully instrumented here.
|
|
130
|
+
# nil req_segments and think the request was not instrumented.
|
|
130
131
|
Thread.current[:catpm_tracked_instrumented] = true
|
|
131
132
|
end
|
|
132
133
|
end
|
data/lib/catpm/version.rb
CHANGED
data/lib/catpm.rb
CHANGED
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.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
@@ -66,6 +66,8 @@ files:
|
|
|
66
66
|
- app/views/catpm/samples/show.html.erb
|
|
67
67
|
- app/views/catpm/shared/_page_nav.html.erb
|
|
68
68
|
- app/views/catpm/shared/_segments_waterfall.html.erb
|
|
69
|
+
- app/views/catpm/shared/not_found.html.erb
|
|
70
|
+
- app/views/catpm/shared/record_not_found.html.erb
|
|
69
71
|
- app/views/catpm/status/index.html.erb
|
|
70
72
|
- app/views/catpm/system/index.html.erb
|
|
71
73
|
- app/views/catpm/system/pipeline.html.erb
|