catpm 0.9.7 → 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 +41 -5
- data/lib/catpm/configuration.rb +43 -2
- data/lib/catpm/middleware.rb +1 -0
- data/lib/catpm/trace.rb +4 -2
- data/lib/catpm/version.rb +1 -1
- data/lib/catpm.rb +1 -0
- 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,6 +6,10 @@ module Catpm
|
|
|
6
6
|
MIN_GAP_MS = 1.0
|
|
7
7
|
DEFAULT_ERROR_STATUS = 500
|
|
8
8
|
DEFAULT_SUCCESS_STATUS = 200
|
|
9
|
+
|
|
10
|
+
@instrumentation_mutex = Mutex.new
|
|
11
|
+
@active_instrumented_count = 0
|
|
12
|
+
|
|
9
13
|
class << self
|
|
10
14
|
def process_action_controller(event)
|
|
11
15
|
return unless Catpm.enabled?
|
|
@@ -389,12 +393,43 @@ module Catpm
|
|
|
389
393
|
|
|
390
394
|
# For HTTP middleware where endpoint is unknown at start.
|
|
391
395
|
def should_instrument_request?
|
|
392
|
-
rand(Catpm.config.random_sample_rate) == 0
|
|
396
|
+
rand(Catpm.config.random_sample_rate) == 0 && try_start_instrumentation
|
|
393
397
|
end
|
|
394
398
|
|
|
395
399
|
# For track_request where endpoint is known at start.
|
|
396
|
-
|
|
397
|
-
|
|
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
|
|
404
|
+
else
|
|
405
|
+
rand(Catpm.config.random_sample_rate) == 0 && try_start_instrumentation
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Reset the concurrency counter (for tests and config reloads).
|
|
410
|
+
def reset_instrumentation_counter!
|
|
411
|
+
@instrumentation_mutex.synchronize { @active_instrumented_count = 0 }
|
|
412
|
+
end
|
|
413
|
+
|
|
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
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
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
|
|
429
|
+
|
|
430
|
+
@active_instrumented_count += 1
|
|
431
|
+
true
|
|
432
|
+
end
|
|
398
433
|
end
|
|
399
434
|
|
|
400
435
|
private
|
|
@@ -456,8 +491,9 @@ module Catpm
|
|
|
456
491
|
# carry full context in the buffer.
|
|
457
492
|
# Non-instrumented requests have no segments — skip sample creation.
|
|
458
493
|
def early_sample_type(error:, duration:, kind:, target:, operation:, instrumented: true)
|
|
459
|
-
|
|
460
|
-
return
|
|
494
|
+
always = Catpm.config.always_sample?(target)
|
|
495
|
+
return 'error' if error && (instrumented || always)
|
|
496
|
+
return nil unless instrumented || always
|
|
461
497
|
return 'slow' if duration >= Catpm.config.slow_threshold_for(kind.to_sym)
|
|
462
498
|
|
|
463
499
|
'random'
|
data/lib/catpm/configuration.rb
CHANGED
|
@@ -9,6 +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
|
+
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
|
|
12
18
|
|
|
13
19
|
# Boolean / non-numeric settings — plain attr_accessor
|
|
14
20
|
attr_accessor :enabled,
|
|
@@ -30,7 +36,8 @@ module Catpm
|
|
|
30
36
|
:events_enabled,
|
|
31
37
|
:track_own_requests,
|
|
32
38
|
:downsampling_thresholds,
|
|
33
|
-
:show_untracked_segments
|
|
39
|
+
:show_untracked_segments,
|
|
40
|
+
:always_sample_targets
|
|
34
41
|
|
|
35
42
|
# Numeric settings that must be positive numbers (nil not allowed)
|
|
36
43
|
REQUIRED_NUMERIC = %i[
|
|
@@ -122,6 +129,7 @@ module Catpm
|
|
|
122
129
|
@caller_scan_depth = 50
|
|
123
130
|
@instrument_call_tree = false
|
|
124
131
|
@show_untracked_segments = false
|
|
132
|
+
@always_sample_targets = []
|
|
125
133
|
end
|
|
126
134
|
|
|
127
135
|
# Buffer gets BUFFER_MEMORY_SHARE of max_memory, scaled by thread count
|
|
@@ -135,12 +143,31 @@ module Catpm
|
|
|
135
143
|
(max_memory * CACHE_ENTRIES_PER_MB * PATH_CACHE_BUDGET_SHARE).to_i
|
|
136
144
|
end
|
|
137
145
|
|
|
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
|
|
153
|
+
end
|
|
154
|
+
|
|
138
155
|
def slow_threshold_for(kind)
|
|
139
156
|
slow_threshold_per_kind.fetch(kind.to_sym, slow_threshold)
|
|
140
157
|
end
|
|
141
158
|
|
|
142
159
|
def ignored?(target)
|
|
143
|
-
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|
|
|
144
171
|
case pattern
|
|
145
172
|
when Regexp then pattern.match?(target)
|
|
146
173
|
when String
|
|
@@ -152,5 +179,19 @@ module Catpm
|
|
|
152
179
|
end
|
|
153
180
|
end
|
|
154
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
|
|
155
196
|
end
|
|
156
197
|
end
|
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,6 +123,7 @@ 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
|
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
|