catpm 0.9.0 → 0.9.2

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: f73d14442e21e40a399d3df0635a640cf6067dc10f34e17a74054ab855ac212a
4
- data.tar.gz: db34666075f0f5e2c2506a6e7ddad705673f8328ae22edcf1264ae0b94fe999a
3
+ metadata.gz: 7550489165d4ba6b3c476bd1f2637fc6e68eed8e3def5bd32420dfc0632ff2d7
4
+ data.tar.gz: ec9ed224bcd90cd67835842385305146736ce634310b9b62eaf33997bf7e2ff8
5
5
  SHA512:
6
- metadata.gz: 16edf4257962cc5b52ee5207604d6f4643fd93b3219ef15aaf4fbaac14779fa09f49b22aff387a97796d9435b741820f4e9380e2e5276df4939b47af9202f01d
7
- data.tar.gz: c5e5b2de2aa7994d827760f77ff7e30fa313d77a51b58573a5129a6a09010815ba30deade3603c15482076b6a4e3cd888123cea7dd78ba026db6fbeab6bc7bc3
6
+ metadata.gz: ad84a45f6d0b8b508c94a5a9d0d06b9671a7820fedd93eda19eeee070b41669e625a814a6a2fab6e69fa80ae83edbaa81edd9a8f20e915d921973ba8a6e70822
7
+ data.tar.gz: 75cd050d65178c666e0369eecbc13c1fe1cb18f8d159f75c2078ecea7422e1844e4836c87320681d6b6d68718c9624f0b8abd67276888d28b09410265b43f8ad
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  gem build catpm.gemspec
2
- gem push catpm-0.8.4.gem
2
+ gem push catpm-0.9.0.gem
3
3
 
4
4
  # Catpm
5
5
 
@@ -4,6 +4,10 @@ module Catpm
4
4
  module Collector
5
5
  SYNTHETIC_MIDDLEWARE_OFFSET_MS = 0.5
6
6
  MIN_GAP_MS = 1.0
7
+ # Cap global force-instrument counter to avoid cascade when many requests
8
+ # are slow. Without this cap, apps with 30% slow requests would see ~23%
9
+ # instrumentation instead of the configured 1/random_sample_rate.
10
+ MAX_FORCE_INSTRUMENT_COUNT = 3
7
11
 
8
12
  class << self
9
13
  def process_action_controller(event)
@@ -19,7 +23,11 @@ module Catpm
19
23
  metadata = build_http_metadata(payload)
20
24
 
21
25
  req_segments = Thread.current[:catpm_request_segments]
22
- instrumented = !req_segments.nil?
26
+ # track_request clears req_segments after processing but leaves a marker
27
+ # so we know the request was already fully instrumented.
28
+ tracked_instrumented = Thread.current[:catpm_tracked_instrumented]
29
+ Thread.current[:catpm_tracked_instrumented] = nil
30
+ instrumented = !req_segments.nil? || tracked_instrumented
23
31
 
24
32
  if req_segments
25
33
  segment_data = req_segments.to_h
@@ -52,9 +60,11 @@ module Catpm
52
60
  instrumented: instrumented
53
61
  )
54
62
 
55
- # Slow spike detection: force instrument next request for this endpoint
63
+ # Slow spike detection: force the NEXT HTTP request through middleware
64
+ # to be fully instrumented (uses global counter for should_instrument_request?).
65
+ # Skip if already handled by track_request (tracked_instrumented).
56
66
  if !instrumented && (payload[:exception] || duration >= Catpm.config.slow_threshold_for(:http))
57
- trigger_force_instrument(kind: :http, target: target, operation: operation)
67
+ trigger_force_instrument
58
68
  end
59
69
 
60
70
  if sample_type
@@ -470,28 +480,68 @@ module Catpm
470
480
 
471
481
  # Called when a slow/error request had no instrumentation —
472
482
  # forces the NEXT request(s) to be fully instrumented.
483
+ #
484
+ # Two modes (mutually exclusive to avoid double-instrumentation):
485
+ # - With endpoint: sets per-endpoint flag consumed by should_instrument?
486
+ # (for track_request paths where endpoint is known)
487
+ # - Without endpoint: increments global counter consumed by
488
+ # should_instrument_request? (for middleware path where endpoint is unknown)
473
489
  def trigger_force_instrument(kind: nil, target: nil, operation: nil)
474
490
  if kind && target
475
491
  endpoint_key = [kind.to_s, target.to_s, (operation || '').to_s]
476
492
  force_instrument_endpoints[endpoint_key] = true
493
+ else
494
+ @force_instrument_count = [(@force_instrument_count || 0) + 1, MAX_FORCE_INSTRUMENT_COUNT].min
477
495
  end
478
- @force_instrument_count = (@force_instrument_count || 0) + 1
479
496
  end
480
497
 
481
498
  def reset_sample_counts!
482
499
  @instrumented_sample_counts = nil
500
+ @instrumented_sample_counts_loaded = false
483
501
  @force_instrument_endpoints = nil
484
502
  @force_instrument_count = nil
485
503
  end
486
504
 
487
- private
505
+ private
488
506
 
489
507
  def force_instrument_endpoints
490
508
  @force_instrument_endpoints ||= {}
491
509
  end
492
510
 
493
511
  def instrumented_sample_counts
494
- @instrumented_sample_counts ||= Hash.new(0)
512
+ return @instrumented_sample_counts if @instrumented_sample_counts_loaded
513
+
514
+ @instrumented_sample_counts = load_sample_counts_from_db
515
+ @instrumented_sample_counts_loaded = true
516
+ @instrumented_sample_counts
517
+ end
518
+
519
+ # Pre-populate filling counters from DB so old endpoints don't
520
+ # re-enter filling phase on every process restart.
521
+ # Temporarily clears thread-local to prevent our query from being
522
+ # captured as a segment in any active request.
523
+ def load_sample_counts_from_db
524
+ counts = Hash.new(0)
525
+ return counts unless defined?(Catpm::Sample) && Catpm::Bucket.table_exists?
526
+
527
+ saved_rs = Thread.current[:catpm_request_segments]
528
+ Thread.current[:catpm_request_segments] = nil
529
+ begin
530
+ Catpm::Sample.joins(:bucket)
531
+ .where(sample_type: 'random')
532
+ .group('catpm_buckets.kind', 'catpm_buckets.target', 'catpm_buckets.operation')
533
+ .count
534
+ .each do |(kind, target, operation), count|
535
+ counts[[kind.to_s, target.to_s, operation.to_s]] = count
536
+ end
537
+ ensure
538
+ Thread.current[:catpm_request_segments] = saved_rs
539
+ end
540
+
541
+ counts
542
+ rescue => e
543
+ Catpm.config.error_handler&.call(e)
544
+ Hash.new(0)
495
545
  end
496
546
 
497
547
  # Remove near-zero-duration "code" spans that merely wrap a "controller" span.
@@ -555,22 +605,34 @@ module Catpm
555
605
  # Filling counter only increments for instrumented requests so
556
606
  # non-instrumented requests don't waste filling slots.
557
607
  def early_sample_type(error:, duration:, kind:, target:, operation:, instrumented: true)
558
- return 'error' if error
559
- return 'slow' if duration >= Catpm.config.slow_threshold_for(kind.to_sym)
608
+ # Errors: only create sample for instrumented requests (with segments).
609
+ # Non-instrumented errors are still tracked in error_groups via
610
+ # event.error? — occurrence counts, contexts, and backtrace are preserved.
611
+ # trigger_force_instrument ensures the next occurrence gets full segments.
612
+ return 'error' if error && instrumented
613
+
614
+ is_slow = duration >= Catpm.config.slow_threshold_for(kind.to_sym)
615
+
616
+ # Non-instrumented slow requests still get a sample (for dashboard) but
617
+ # don't count towards filling phase (they have no segments).
618
+ return 'slow' if is_slow && !instrumented
560
619
 
561
620
  # Non-instrumented requests have no segments — skip sample creation
562
621
  return nil unless instrumented
563
622
 
564
- # Filling phase: always sample until endpoint has enough instrumented samples
623
+ # Count this instrumented request towards filling phase completion.
624
+ # Both slow and random requests count — without this, endpoints where
625
+ # most requests exceed slow_threshold would never exit the filling phase,
626
+ # causing 100% instrumentation regardless of random_sample_rate.
565
627
  endpoint_key = [kind.to_s, target, operation.to_s]
566
628
  count = instrumented_sample_counts[endpoint_key]
567
629
  max_random = Catpm.config.max_random_samples_per_endpoint
568
630
  if max_random.nil? || count < max_random
569
631
  instrumented_sample_counts[endpoint_key] = count + 1
570
- return 'random'
571
632
  end
572
633
 
573
- # Instrumented request was already chosen by dice roll at start — always sample
634
+ return 'slow' if is_slow
635
+
574
636
  'random'
575
637
  end
576
638
 
@@ -36,6 +36,7 @@ module Catpm
36
36
  req_segments&.release!
37
37
  Thread.current[:catpm_request_segments] = nil
38
38
  Thread.current[:catpm_request_start] = nil
39
+ Thread.current[:catpm_tracked_instrumented] = nil
39
40
  end
40
41
 
41
42
  private
data/lib/catpm/trace.rb CHANGED
@@ -9,6 +9,12 @@ module Catpm
9
9
 
10
10
  req_segments = Thread.current[:catpm_request_segments]
11
11
  unless req_segments
12
+ # Inside a non-instrumented HTTP request — just run the block
13
+ # without promoting the span to a standalone endpoint
14
+ if Thread.current[:catpm_request_start]
15
+ return block.call if block
16
+ return nil
17
+ end
12
18
  return trace(name, &block)
13
19
  end
14
20
 
@@ -46,7 +52,7 @@ module Catpm
46
52
  type: :custom, duration: duration_ms, detail: name,
47
53
  source: source, started_at: start_time
48
54
  )
49
- elsif buffer
55
+ elsif buffer && !Thread.current[:catpm_request_start]
50
56
  Collector.process_custom(
51
57
  name: name, duration: duration_ms,
52
58
  metadata: metadata, error: error, context: context
@@ -129,6 +135,11 @@ module Catpm
129
135
  if owns_segments
130
136
  req_segments&.release!
131
137
  Thread.current[:catpm_request_segments] = nil
138
+ # Mark that this request was already instrumented and processed by
139
+ # track_request. Without this, process_action_controller would see
140
+ # nil req_segments and falsely trigger force_instrument for slow
141
+ # requests — even though they were fully instrumented here.
142
+ Thread.current[:catpm_tracked_instrumented] = true
132
143
  end
133
144
  end
134
145
  end
@@ -155,7 +166,7 @@ module Catpm
155
166
  type: :custom, duration: duration_ms, detail: @name,
156
167
  source: source, started_at: @start_time
157
168
  )
158
- elsif Catpm.enabled? && Catpm.buffer
169
+ elsif Catpm.enabled? && Catpm.buffer && !Thread.current[:catpm_request_start]
159
170
  Collector.process_custom(
160
171
  name: @name, duration: duration_ms,
161
172
  metadata: @metadata, error: error, context: @context
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.0'
4
+ VERSION = '0.9.2'
5
5
  end
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.0
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''