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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/helpers/catpm/application_helper.rb +5 -3
- data/app/views/catpm/errors/show.html.erb +1 -1
- data/app/views/catpm/samples/show.html.erb +17 -3
- data/app/views/catpm/shared/_segments_timeline.html.erb +155 -0
- data/app/views/catpm/shared/_segments_waterfall.html.erb +15 -14
- data/app/views/catpm/status/index.html.erb +4 -4
- data/app/views/catpm/system/pipeline.html.erb +1 -1
- data/app/views/layouts/catpm/application.html.erb +147 -6
- data/db/migrate/20250601000001_create_catpm_tables.rb +34 -32
- data/lib/catpm/call_tracer.rb +2 -1
- data/lib/catpm/collector.rb +434 -46
- data/lib/catpm/configuration.rb +6 -2
- data/lib/catpm/engine.rb +9 -0
- data/lib/catpm/fingerprint.rb +6 -0
- data/lib/catpm/middleware.rb +3 -4
- data/lib/catpm/request_segments.rb +18 -1
- data/lib/catpm/segment_subscribers.rb +1 -1
- data/lib/catpm/sidekiq_server_middleware.rb +90 -0
- data/lib/catpm/stack_sampler.rb +3 -3
- data/lib/catpm/version.rb +1 -1
- data/lib/catpm.rb +5 -0
- data/lib/generators/catpm/templates/initializer.rb.tt +2 -2
- metadata +3 -1
data/lib/catpm/middleware.rb
CHANGED
|
@@ -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] +=
|
|
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: :
|
|
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
|
data/lib/catpm/stack_sampler.rb
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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
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:
|
|
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
|
|
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.
|
|
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
|