catpm 0.10.3 → 0.11.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.
@@ -13,6 +13,7 @@ module Catpm
13
13
 
14
14
  env['catpm.request_start'] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
15
15
  Thread.current[:catpm_request_start] = env['catpm.request_start']
16
+ Thread.current[:catpm_request_env] = env
16
17
 
17
18
  if Catpm.config.instrument_segments && Collector.should_instrument_request?
18
19
  use_sampler = Catpm.config.instrument_stack_sampler || Catpm.config.instrument_call_tree
@@ -36,6 +37,7 @@ module Catpm
36
37
  Collector.end_instrumentation if req_segments
37
38
  Thread.current[:catpm_request_segments] = nil
38
39
  Thread.current[:catpm_request_start] = nil
40
+ Thread.current[:catpm_request_env] = nil
39
41
  Thread.current[:catpm_tracked_instrumented] = nil
40
42
  end
41
43
 
@@ -55,10 +57,7 @@ module Catpm
55
57
  error_class: exception.class.name,
56
58
  error_message: exception.message,
57
59
  backtrace: exception.backtrace,
58
- context: {
59
- method: env['REQUEST_METHOD'],
60
- path: env['PATH_INFO']
61
- }
60
+ context: Collector.build_exception_context(env)
62
61
  )
63
62
 
64
63
  Catpm.buffer.push(ev)
@@ -18,6 +18,7 @@ module Catpm
18
18
  @overflow = false
19
19
  @summary = Hash.new(0)
20
20
  @span_stack = []
21
+ @span_children_duration = {} # span_index => accumulated direct children duration
21
22
  @tracked_ranges = []
22
23
  @call_tree = call_tree
23
24
  @estimated_bytes = 0
@@ -35,6 +36,11 @@ module Catpm
35
36
  @summary[count_key] += 1
36
37
  @summary[dur_key] += duration
37
38
 
39
+ # Attribute this leaf duration to immediate parent span for self-time calculation
40
+ if @span_stack.any?
41
+ @span_children_duration[@span_stack.last] += duration
42
+ end
43
+
38
44
  # Record time range so sampler can skip already-tracked periods
39
45
  if started_at && duration > 0
40
46
  @tracked_ranges << [started_at, started_at + duration / 1000.0]
@@ -79,6 +85,7 @@ module Catpm
79
85
  index = @segments.size
80
86
  @segments << segment
81
87
  @span_stack.push(index)
88
+ @span_children_duration[index] = 0.0
82
89
  @estimated_bytes += estimate_segment_bytes(segment)
83
90
  index
84
91
  end
@@ -99,10 +106,19 @@ module Catpm
99
106
  duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
100
107
  segment[:duration] = duration.round(2)
101
108
 
109
+ # Self-time = total duration minus direct children (avoids double-counting in breakdown)
110
+ children_dur = @span_children_duration.delete(index) || 0.0
111
+ self_duration = [duration - children_dur, 0.0].max
112
+
113
+ # Report total duration to parent span so it can compute its own self-time
114
+ if @span_stack.any?
115
+ @span_children_duration[@span_stack.last] += duration
116
+ end
117
+
102
118
  type_key = segment[:type].to_sym
103
119
  count_key, dur_key = SUMMARY_KEYS[type_key]
104
120
  @summary[count_key] += 1
105
- @summary[dur_key] += duration
121
+ @summary[dur_key] += self_duration
106
122
  end
107
123
 
108
124
  def stop_sampler
@@ -128,6 +144,7 @@ module Catpm
128
144
  def release!
129
145
  @segments = []
130
146
  @summary = {}
147
+ @span_children_duration = {}
131
148
  @tracked_ranges = []
132
149
  @sampler = nil
133
150
  @estimated_bytes = 0
@@ -77,7 +77,7 @@ module Catpm
77
77
  )
78
78
  Thread.current[:catpm_request_segments] = req_segments
79
79
 
80
- index = req_segments.push_span(type: :controller, detail: target, started_at: start_time)
80
+ index = req_segments.push_span(type: :job, detail: target, started_at: start_time)
81
81
  payload[:_catpm_job_span_index] = index
82
82
  payload[:_catpm_job_owns_segments] = true
83
83
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Catpm
4
+ # Sidekiq server middleware that tracks native Sidekiq::Worker jobs.
5
+ # ActiveJob jobs are tracked via the perform.active_job subscriber instead;
6
+ # this middleware detects and skips them to avoid double-tracking.
7
+ #
8
+ # Automatically registered when Sidekiq is loaded and config.instrument_jobs is true.
9
+ class SidekiqServerMiddleware
10
+ def call(job_instance, job_payload, queue)
11
+ return yield unless Catpm.enabled? && Catpm.config.instrument_jobs
12
+
13
+ # ActiveJob wraps jobs with a 'wrapped' key — those are tracked
14
+ # by the perform.active_job subscriber, skip here.
15
+ return yield if job_payload.key?('wrapped')
16
+
17
+ target = job_payload['class'] || job_instance.class.name
18
+ return yield if Catpm.config.ignored?(target)
19
+
20
+ # No HTTP middleware in workers — restart flusher after fork
21
+ Catpm.flusher&.ensure_running!
22
+
23
+ instrumented = Collector.should_instrument?(:job, target, queue)
24
+ req_segments = nil
25
+
26
+ if instrumented
27
+ use_sampler = Catpm.config.instrument_stack_sampler || Catpm.config.instrument_call_tree
28
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ req_segments = RequestSegments.new(
30
+ max_segments: Catpm.config.effective_max_segments_per_request,
31
+ request_start: start_time,
32
+ stack_sample: use_sampler,
33
+ call_tree: Catpm.config.instrument_call_tree
34
+ )
35
+ Thread.current[:catpm_request_segments] = req_segments
36
+ span_index = req_segments.push_span(type: :job, detail: target, started_at: start_time)
37
+ end
38
+
39
+ Thread.current[:catpm_job_active] = true
40
+ job_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
41
+ error = nil
42
+
43
+ begin
44
+ yield
45
+ rescue => e
46
+ error = e
47
+ raise
48
+ ensure
49
+ Thread.current[:catpm_job_active] = nil
50
+ duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - job_start) * 1000.0
51
+
52
+ if req_segments
53
+ req_segments.pop_span(span_index)
54
+ req_segments.stop_sampler
55
+ end
56
+
57
+ queue_wait = compute_queue_wait(job_payload)
58
+
59
+ Collector.process_sidekiq_job(
60
+ target: target,
61
+ job_id: job_payload['jid'],
62
+ queue: queue,
63
+ retry_count: job_payload['retry_count'] || 0,
64
+ queue_wait: queue_wait,
65
+ duration: duration,
66
+ error: error,
67
+ req_segments: req_segments,
68
+ instrumented: instrumented
69
+ )
70
+
71
+ if instrumented
72
+ req_segments&.release!
73
+ Collector.end_instrumentation
74
+ Thread.current[:catpm_request_segments] = nil
75
+ end
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def compute_queue_wait(job_payload)
82
+ enqueued_at = job_payload['enqueued_at']
83
+ return nil unless enqueued_at
84
+
85
+ ((Time.now.to_f - enqueued_at.to_f) * 1000.0).round(1)
86
+ rescue
87
+ nil
88
+ end
89
+ end
90
+ end
@@ -147,7 +147,7 @@ module Catpm
147
147
  if app_frame
148
148
  app_path = app_frame.path.to_s
149
149
  parent = {
150
- type: 'code',
150
+ type: Fingerprint.app_code_type(app_path),
151
151
  detail: build_app_detail(app_frame),
152
152
  duration: duration.round(2),
153
153
  offset: offset,
@@ -233,7 +233,7 @@ module Catpm
233
233
 
234
234
  frame = node[:frame]
235
235
  seg = {
236
- type: 'code',
236
+ type: Fingerprint.app_code_type(frame.path.to_s),
237
237
  detail: build_app_detail(frame),
238
238
  duration: duration.round(2),
239
239
  offset: ((node[:first_time] - @request_start) * MS_PER_SECOND).round(2),
@@ -329,7 +329,7 @@ module Catpm
329
329
  end
330
330
 
331
331
  def classify_path(path)
332
- return 'code' if Fingerprint.app_frame?(path)
332
+ return Fingerprint.app_code_type(path) if Fingerprint.app_frame?(path)
333
333
 
334
334
  gem = extract_gem_name(path)
335
335
  case gem
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.10.3'
4
+ VERSION = '0.11.0'
5
5
  end
data/lib/catpm.rb CHANGED
@@ -22,9 +22,14 @@ require 'catpm/segment_subscribers'
22
22
  require 'catpm/lifecycle'
23
23
  require 'catpm/trace'
24
24
  require 'catpm/span_helpers'
25
+ require 'catpm/sidekiq_server_middleware' if defined?(::Sidekiq)
25
26
  require 'catpm/engine'
26
27
 
27
28
  module Catpm
29
+ # Migration version matching the host app's Rails major.minor.
30
+ # Used in db/migrate/* so the gem works with Rails 7.x, 8.x, etc.
31
+ MIGRATION_VERSION = "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}".to_f
32
+
28
33
  class << self
29
34
  def configure
30
35
  yield(config)
@@ -28,7 +28,7 @@ Catpm.configure do |config|
28
28
  # config.min_segment_duration = 5.0 # ms — segments shorter than this are counted but not stored (0 = store all)
29
29
  # config.max_sql_length = 200 # nil = no truncation
30
30
  # config.slow_threshold = 500 # ms
31
- # config.slow_threshold_per_kind = {} # { http: 500, job: 5_000, custom: 1_000 }
31
+ # config.slow_threshold_per_kind = {} # { http: 500, job: 1_500, custom: 1_000 }
32
32
  # config.ignored_targets = []
33
33
 
34
34
  # === Stack Sampling ===
@@ -50,7 +50,7 @@ Catpm.configure do |config|
50
50
  # config.events_max_samples_per_name = 20 # nil = unlimited
51
51
 
52
52
  # === Memory ===
53
- # config.max_memory = 20 # MB — global memory budget (2% of 1GB server)
53
+ # config.max_memory = 20 # MB — global memory budget
54
54
 
55
55
  # === Buffering & Flushing ===
56
56
  # config.flush_interval = 30 # seconds
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.10.3
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
@@ -65,6 +65,7 @@ files:
65
65
  - app/views/catpm/events/show.html.erb
66
66
  - app/views/catpm/samples/show.html.erb
67
67
  - app/views/catpm/shared/_page_nav.html.erb
68
+ - app/views/catpm/shared/_segments_timeline.html.erb
68
69
  - app/views/catpm/shared/_segments_waterfall.html.erb
69
70
  - app/views/catpm/shared/not_found.html.erb
70
71
  - app/views/catpm/shared/record_not_found.html.erb
@@ -98,6 +99,7 @@ files:
98
99
  - lib/catpm/patches/net_http.rb
99
100
  - lib/catpm/request_segments.rb
100
101
  - lib/catpm/segment_subscribers.rb
102
+ - lib/catpm/sidekiq_server_middleware.rb
101
103
  - lib/catpm/span_helpers.rb
102
104
  - lib/catpm/stack_sampler.rb
103
105
  - lib/catpm/subscribers.rb