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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3731353688fdebaef1f9cb164731daba00a0d69df65da03dc309f6f7901e2708
4
- data.tar.gz: 4ef7e26d0b721c8fea556f797d74c58ef2b0df8f9257edead653393fcb991229
3
+ metadata.gz: '0283e922a2169d04e59405a273250c1fd226b4b72731f01ba6f6495b6a216ce2'
4
+ data.tar.gz: 5af58e9875ee9ce18afc02816e79d94cb025025218b78695e560a7a662ce2953
5
5
  SHA512:
6
- metadata.gz: df633940cf6beba3252b6915c45d22688ff23ef3200edf17622e971ce65191ad85af476bfdd853a2bee449da6d28df81431c48e663001c80b14242ba407d3a7b
7
- data.tar.gz: 473011238fdf84d011bf9d3c0ceed1d47b7f1a3be5e6e9d640f6f66b62273c2a396c425166b09ceb5b0b8bf63a993e51651d99d373c48b1058ead4ede2451266
6
+ metadata.gz: 0e73b3ea6819f8633104be971da72ad30c5de1b13bc4e0bece856d1e13b5987c705fb322f48c91622ae87d8acbd6b3d71ff0d2bb151939370a2685f4cf0ec7a7
7
+ data.tar.gz: 988f46467fbf05398f71cefff884584fb1daf2b148500df100b0824049d2cc659e5e74038e65c8de30cec27b36aaa6d503c5267475b8b4c7313e35b4e208e375
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  gem build catpm.gemspec
2
- gem push catpm-0.9.5.gem
2
+ gem push catpm-0.9.7.gem
3
3
 
4
4
  # Catpm
5
5
 
@@ -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: "Unauthorized", status: :unauthorized
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("Catpm") do |username, password|
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">&larr; 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">&#x1F43E;</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">&larr; Dashboard</a>
23
+ </div>
24
+ </div>
data/config/routes.rb CHANGED
@@ -29,4 +29,5 @@ Catpm::Engine.routes.draw do
29
29
  patch :toggle_pin
30
30
  end
31
31
  end
32
+ match '*unmatched', to: 'application#not_found', via: :all
32
33
  end
@@ -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
- # Cap global force-instrument counter to avoid cascade when many requests
10
- # are slow. Without this cap, apps with 30% slow requests would see ~23%
11
- # instrumentation instead of the configured 1/random_sample_rate.
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
- # Force after slow spike detection
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
- # Filling phase ensures new endpoints get instrumented samples quickly.
449
- def should_instrument?(kind, target, operation)
450
- endpoint_key = [kind.to_s, target.to_s, (operation || '').to_s]
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
- @force_instrument_count = [(@force_instrument_count || 0) + 1, MAX_FORCE_INSTRUMENT_COUNT].min
405
+ rand(Catpm.config.random_sample_rate) == 0 && try_start_instrumentation
482
406
  end
483
407
  end
484
408
 
485
- def reset_sample_counts!
486
- @instrumented_sample_counts = nil
487
- @instrumented_sample_counts_loaded = false
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
- private
494
-
495
- # Recompute whether any HTTP endpoint is still below its sample quota.
496
- # Called after loading counts from DB and when an endpoint exits filling.
497
- def recompute_http_filling_active
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
- # Evict half the entries from instrumented_sample_counts.
508
- # Prefers evicting filled entries (count >= max) to avoid
509
- # re-triggering filling phase for those endpoints.
510
- def evict_sample_counts(max_random)
511
- evict_count = instrumented_sample_counts.size / 2
512
- if max_random
513
- filled_keys = []
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
- def instrumented_sample_counts
531
- return @instrumented_sample_counts if @instrumented_sample_counts_loaded
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
- # Errors: only create sample for instrumented requests (with segments).
630
- # Non-instrumented errors are still tracked in error_groups via
631
- # event.error? occurrence counts, contexts, and backtrace are preserved.
632
- # trigger_force_instrument ensures the next occurrence gets full segments.
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
@@ -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
- SAMPLE_COUNTS_PER_MB = 12_500 # ~80 bytes/entry in sample counts hash
13
- SAMPLE_COUNTS_BUDGET_SHARE = 0.02 # 2% of max_memory for sample counts
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
- # Sample counts hash limit derived from max_memory
141
- def effective_sample_counts_max
142
- (max_memory * SAMPLE_COUNTS_PER_MB * SAMPLE_COUNTS_BUDGET_SHARE).to_i
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.any? do |pattern|
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
@@ -346,7 +346,6 @@ module Catpm
346
346
  @last_cleanup_at = Time.now
347
347
  downsample_buckets
348
348
  cleanup_expired_data if Catpm.config.retention_period
349
- Collector.reset_sample_counts!
350
349
  end
351
350
 
352
351
  def downsample_buckets
@@ -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)
@@ -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
- if Collector.should_instrument?(kind, target, operation)
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 falsely trigger force_instrument for slow
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Catpm
4
- VERSION = '0.9.6'
4
+ VERSION = '0.10.0'
5
5
  end
data/lib/catpm.rb CHANGED
@@ -39,7 +39,7 @@ module Catpm
39
39
  @buffer = nil
40
40
  @flusher = nil
41
41
  Fingerprint.reset_caches!
42
- Collector.reset_sample_counts!
42
+ Collector.reset_instrumentation_counter!
43
43
  end
44
44
 
45
45
  def enabled?
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.9.6
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