catpm 0.6.5 → 0.7.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 +15 -0
- data/app/controllers/catpm/system_controller.rb +4 -0
- data/app/helpers/catpm/application_helper.rb +142 -0
- data/app/views/catpm/system/index.html.erb +76 -479
- data/app/views/catpm/system/pipeline.html.erb +344 -0
- data/app/views/layouts/catpm/application.html.erb +40 -0
- data/app/views/layouts/catpm/pipeline.html.erb +79 -0
- data/config/routes.rb +1 -0
- data/lib/catpm/buffer.rb +22 -2
- data/lib/catpm/call_tracer.rb +32 -9
- data/lib/catpm/flusher.rb +44 -63
- data/lib/catpm/request_segments.rb +6 -1
- data/lib/catpm/version.rb +1 -1
- 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: c9d4f65535550d3a1201fe91d55206136dac09a1fb028e37b01e860a81260abc
|
|
4
|
+
data.tar.gz: 5ba5802a2361921a743745b88983399a1f792c7539f40c5a0fc804375c435101
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3aee4ecf169a3254ff5193c77e7d1579559d0cfd7915ceeba7d6608ebb397c813dd9e3bf1170ea253ef5c893a1c8a42d009d5d911a8b46eb9a5c194b51fed23
|
|
7
|
+
data.tar.gz: fc242376c61f8b0f6943bb534922f6b854f6896e554ab2aa26f72a63a3aea615b243c4d14d3ec1c9738ab2457aeaabfbbbb277ca5a4c8d7b91a8a220e1685e97
|
data/README.md
CHANGED
|
@@ -2,8 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module Catpm
|
|
4
4
|
class ApplicationController < ActionController::Base
|
|
5
|
+
before_action :authenticate!
|
|
6
|
+
|
|
5
7
|
private
|
|
6
8
|
|
|
9
|
+
def authenticate!
|
|
10
|
+
if Catpm.config.access_policy
|
|
11
|
+
unless Catpm.config.access_policy.call(request)
|
|
12
|
+
render plain: "Unauthorized", status: :unauthorized
|
|
13
|
+
end
|
|
14
|
+
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|
|
|
16
|
+
ActiveSupport::SecurityUtils.secure_compare(username, Catpm.config.http_basic_auth_user) &
|
|
17
|
+
ActiveSupport::SecurityUtils.secure_compare(password, Catpm.config.http_basic_auth_password)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
7
22
|
def remembered_range
|
|
8
23
|
if params[:range].present?
|
|
9
24
|
cookies[:catpm_range] = { value: params[:range], expires: 1.year.from_now }
|
|
@@ -47,6 +47,119 @@ module Catpm
|
|
|
47
47
|
|
|
48
48
|
RANGE_KEYS = RANGES.keys.freeze
|
|
49
49
|
|
|
50
|
+
# Setting metadata for the system page — maps config attributes to display info.
|
|
51
|
+
# Ordered by group; Ruby hashes preserve insertion order.
|
|
52
|
+
CONFIG_METADATA = {
|
|
53
|
+
# ── Core ──
|
|
54
|
+
enabled: { group: 'Core', label: 'Enabled', desc: 'Master switch for all catpm instrumentation', fmt: :bool },
|
|
55
|
+
track_own_requests: { group: 'Core', label: 'Track Own Requests', desc: 'Whether catpm dashboard requests are tracked', fmt: :bool },
|
|
56
|
+
|
|
57
|
+
# ── Instrumentation ──
|
|
58
|
+
instrument_http: { group: 'Instrumentation', label: 'HTTP', desc: 'Capture HTTP request/response metrics via Rack middleware', fmt: :bool },
|
|
59
|
+
instrument_jobs: { group: 'Instrumentation', label: 'Jobs', desc: 'Track ActiveJob and Sidekiq background job performance', fmt: :bool },
|
|
60
|
+
instrument_segments: { group: 'Instrumentation', label: 'Segments', desc: 'Capture SQL, view, cache, and HTTP sub-segments within requests', fmt: :bool },
|
|
61
|
+
instrument_net_http: { group: 'Instrumentation', label: 'Net::HTTP', desc: 'Patch Net::HTTP to capture outbound HTTP calls as segments', fmt: :bool },
|
|
62
|
+
instrument_middleware_stack: { group: 'Instrumentation', label: 'Middleware Stack', desc: 'Instrument the full Rack middleware stack for per-middleware timing', fmt: :bool },
|
|
63
|
+
instrument_stack_sampler: { group: 'Instrumentation', label: 'Stack Sampler', desc: 'Periodically sample the call stack during requests for flame-graph data', fmt: :bool },
|
|
64
|
+
instrument_call_tree: { group: 'Instrumentation', label: 'Call Tree', desc: 'Capture full method-level call trees within requests', fmt: :bool },
|
|
65
|
+
show_untracked_segments: { group: 'Instrumentation', label: 'Show Untracked', desc: 'Display time not attributed to any segment in the waterfall view', fmt: :bool },
|
|
66
|
+
|
|
67
|
+
# ── Segments ──
|
|
68
|
+
slow_threshold: { group: 'Segments', label: 'Slow Threshold', desc: 'Requests slower than this are flagged as slow', fmt: :ms },
|
|
69
|
+
slow_threshold_per_kind: { group: 'Segments', label: 'Slow Threshold (per kind)', desc: 'Override slow threshold for specific request kinds (http, job, custom)', fmt: :hash_ms },
|
|
70
|
+
max_segments_per_request: { group: 'Segments', label: 'Max Segments / Request', desc: 'Cap on segments captured per request', fmt: :nullable_int },
|
|
71
|
+
segment_source_threshold: { group: 'Segments', label: 'Source Capture Threshold', desc: 'Minimum segment duration (ms) before caller_locations is captured; 0 = always', fmt: :ms_zero },
|
|
72
|
+
max_sql_length: { group: 'Segments', label: 'Max SQL Length', desc: 'Truncate SQL queries beyond this many characters', fmt: :nullable_chars },
|
|
73
|
+
ignored_targets: { group: 'Segments', label: 'Ignored Targets', desc: 'Endpoint patterns excluded from tracking (strings or regexps)', fmt: :list },
|
|
74
|
+
|
|
75
|
+
# ── Stack Sampling ──
|
|
76
|
+
stack_sample_interval: { group: 'Stack Sampling', label: 'Sample Interval', desc: 'How often the call stack is sampled during a request', fmt: :seconds },
|
|
77
|
+
max_stack_samples_per_request: { group: 'Stack Sampling', label: 'Max Samples / Request', desc: 'Cap on stack samples per request', fmt: :nullable_int },
|
|
78
|
+
|
|
79
|
+
# ── Sampling ──
|
|
80
|
+
random_sample_rate: { group: 'Sampling', label: 'Random Sample Rate', desc: '1-in-N requests are sampled randomly for detailed traces', fmt: :one_in_n },
|
|
81
|
+
max_random_samples_per_endpoint: { group: 'Sampling', label: 'Max Random / Endpoint', desc: 'Random samples retained per endpoint', fmt: :nullable_int },
|
|
82
|
+
max_slow_samples_per_endpoint: { group: 'Sampling', label: 'Max Slow / Endpoint', desc: 'Slow samples retained per endpoint', fmt: :nullable_int },
|
|
83
|
+
max_error_samples_per_fingerprint: { group: 'Sampling', label: 'Max Error / Fingerprint', desc: 'Error samples retained per error fingerprint', fmt: :nullable_int },
|
|
84
|
+
|
|
85
|
+
# ── Errors ──
|
|
86
|
+
max_error_contexts: { group: 'Errors', label: 'Max Error Contexts', desc: 'Context snapshots stored per error occurrence', fmt: :nullable_int },
|
|
87
|
+
backtrace_lines: { group: 'Errors', label: 'Backtrace Lines', desc: 'Number of backtrace lines captured per error', fmt: :nullable_int },
|
|
88
|
+
max_error_detail_length: { group: 'Errors', label: 'Max Error Detail Length', desc: 'Truncate error detail segments beyond this length', fmt: :nullable_chars },
|
|
89
|
+
max_fingerprint_app_frames: { group: 'Errors', label: 'Fingerprint App Frames', desc: 'App stack frames used for error fingerprinting', fmt: :nullable_int },
|
|
90
|
+
max_fingerprint_gem_frames: { group: 'Errors', label: 'Fingerprint Gem Frames', desc: 'Gem stack frames used when no app frames available', fmt: :nullable_int },
|
|
91
|
+
|
|
92
|
+
# ── Events ──
|
|
93
|
+
events_enabled: { group: 'Events', label: 'Events Enabled', desc: 'Enable custom event tracking via Catpm.event', fmt: :bool },
|
|
94
|
+
events_max_samples_per_name: { group: 'Events', label: 'Max Samples / Name', desc: 'Event samples retained per event name', fmt: :nullable_int },
|
|
95
|
+
|
|
96
|
+
# ── Buffer & Flush ──
|
|
97
|
+
max_buffer_memory: { group: 'Buffer & Flush', label: 'Max Buffer Memory', desc: 'Maximum memory for the in-memory event queue before events are dropped', fmt: :bytes },
|
|
98
|
+
flush_interval: { group: 'Buffer & Flush', label: 'Flush Interval', desc: 'How often the background thread drains the buffer to the database', fmt: :seconds },
|
|
99
|
+
flush_jitter: { group: 'Buffer & Flush', label: 'Flush Jitter', desc: 'Random jitter added to flush interval to avoid thundering herd', fmt: :pm_seconds },
|
|
100
|
+
persistence_batch_size: { group: 'Buffer & Flush', label: 'Batch Size', desc: 'Number of events written per database transaction', fmt: :int },
|
|
101
|
+
|
|
102
|
+
# ── Retention & Downsampling ──
|
|
103
|
+
retention_period: { group: 'Retention', label: 'Retention Period', desc: 'How long data is kept; nil = forever (data is downsampled, not deleted)', fmt: :duration },
|
|
104
|
+
cleanup_interval: { group: 'Retention', label: 'Cleanup Interval', desc: 'How often the cleanup job runs to remove expired data', fmt: :duration },
|
|
105
|
+
cleanup_batch_size: { group: 'Retention', label: 'Cleanup Batch Size', desc: 'Rows deleted per cleanup batch', fmt: :nullable_int },
|
|
106
|
+
bucket_sizes: { group: 'Retention', label: 'Bucket Sizes', desc: 'Time bucket granularities for data aggregation', fmt: :bucket_sizes },
|
|
107
|
+
downsampling_thresholds: { group: 'Retention', label: 'Downsampling Thresholds', desc: 'Age before each tier is merged into the next coarser tier', fmt: :downsampling },
|
|
108
|
+
|
|
109
|
+
# ── Resilience ──
|
|
110
|
+
circuit_breaker_failure_threshold: { group: 'Resilience', label: 'Circuit Breaker Threshold', desc: 'Consecutive DB write failures before the circuit opens', fmt: :int_failures },
|
|
111
|
+
circuit_breaker_recovery_timeout: { group: 'Resilience', label: 'Circuit Breaker Recovery', desc: 'Seconds before retrying after circuit opens', fmt: :seconds },
|
|
112
|
+
sqlite_busy_timeout: { group: 'Resilience', label: 'SQLite Busy Timeout', desc: 'How long SQLite waits for a lock before raising BUSY', fmt: :ms, condition: :sqlite? },
|
|
113
|
+
|
|
114
|
+
# ── Security ──
|
|
115
|
+
http_basic_auth_user: { group: 'Security', label: 'HTTP Basic Auth User', desc: 'Username for HTTP Basic authentication on the dashboard', fmt: :secret },
|
|
116
|
+
http_basic_auth_password: { group: 'Security', label: 'HTTP Basic Auth Password', desc: 'Password for HTTP Basic authentication on the dashboard', fmt: :secret },
|
|
117
|
+
|
|
118
|
+
# ── PII Filtering ──
|
|
119
|
+
additional_filter_parameters: { group: 'PII Filtering', label: 'Additional Filter Parameters', desc: 'Extra parameter names to redact from captured data (on top of Rails defaults)', fmt: :list },
|
|
120
|
+
|
|
121
|
+
# ── Advanced ──
|
|
122
|
+
shutdown_timeout: { group: 'Advanced', label: 'Shutdown Timeout', desc: 'Seconds to wait for buffer flush on application shutdown', fmt: :seconds },
|
|
123
|
+
caller_scan_depth: { group: 'Advanced', label: 'Caller Scan Depth', desc: 'Max stack frames scanned to find app code for source attribution', fmt: :int },
|
|
124
|
+
auto_instrument_methods: { group: 'Advanced', label: 'Auto-Instrument Methods', desc: 'Method signatures to automatically instrument (e.g. Worker#process)', fmt: :list },
|
|
125
|
+
service_base_classes: { group: 'Advanced', label: 'Service Base Classes', desc: 'Base classes for auto-detection of service objects; nil = auto-detect', fmt: :nullable_list },
|
|
126
|
+
}.freeze
|
|
127
|
+
|
|
128
|
+
def format_config_value(config, attr, meta)
|
|
129
|
+
value = config.send(attr)
|
|
130
|
+
case meta[:fmt]
|
|
131
|
+
when :bool then value ? 'true' : 'false'
|
|
132
|
+
when :ms then "#{value}ms"
|
|
133
|
+
when :ms_zero then value == 0 || value == 0.0 ? '0 (always)' : "#{value}ms"
|
|
134
|
+
when :seconds then "#{value}s"
|
|
135
|
+
when :pm_seconds then "\u00B1#{value}s"
|
|
136
|
+
when :bytes then number_to_human_size(value)
|
|
137
|
+
when :int then value.to_s
|
|
138
|
+
when :int_failures then "#{value} failures"
|
|
139
|
+
when :one_in_n then "1 in #{value}"
|
|
140
|
+
when :nullable_int then value.nil? ? 'unlimited' : value.to_s
|
|
141
|
+
when :nullable_chars then value.nil? ? 'unlimited' : "#{value} chars"
|
|
142
|
+
when :list then value.respond_to?(:any?) && value.any? ? value.map(&:to_s).join(', ') : 'none'
|
|
143
|
+
when :nullable_list then value.nil? ? 'auto-detect' : value.map(&:to_s).join(', ')
|
|
144
|
+
when :secret then value.present? ? 'set' : 'not set'
|
|
145
|
+
when :hash_ms then value.respond_to?(:any?) && value.any? ? value.map { |k, v| "#{k}: #{v}ms" }.join(', ') : 'none'
|
|
146
|
+
when :duration then format_duration_value(value)
|
|
147
|
+
when :bucket_sizes then value.map { |k, v| "#{k}: #{humanize_seconds(v)}" }.join(', ')
|
|
148
|
+
when :downsampling then value.map { |k, v| "#{k}: #{humanize_seconds(v)}" }.join(', ')
|
|
149
|
+
else value.to_s
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def config_condition_met?(meta)
|
|
154
|
+
return true unless meta[:condition]
|
|
155
|
+
case meta[:condition]
|
|
156
|
+
when :sqlite?
|
|
157
|
+
ActiveRecord::Base.connection.adapter_name.downcase.include?('sqlite')
|
|
158
|
+
else
|
|
159
|
+
true
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
50
163
|
def segment_colors
|
|
51
164
|
SEGMENT_COLORS
|
|
52
165
|
end
|
|
@@ -272,5 +385,34 @@ module Catpm
|
|
|
272
385
|
%(<span title="Quiet" style="color:var(--text-2)">—</span>).html_safe
|
|
273
386
|
end
|
|
274
387
|
end
|
|
388
|
+
|
|
389
|
+
private
|
|
390
|
+
|
|
391
|
+
def format_duration_value(value)
|
|
392
|
+
return 'forever' if value.nil?
|
|
393
|
+
secs = value.to_i
|
|
394
|
+
if secs >= 86_400
|
|
395
|
+
"#{secs / 86_400} days"
|
|
396
|
+
elsif secs >= 3600
|
|
397
|
+
"#{secs / 3600} hours"
|
|
398
|
+
else
|
|
399
|
+
"#{secs / 60} min"
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def humanize_seconds(secs)
|
|
404
|
+
secs = secs.to_i
|
|
405
|
+
if secs >= 604_800
|
|
406
|
+
"#{secs / 604_800}w"
|
|
407
|
+
elsif secs >= 86_400
|
|
408
|
+
"#{secs / 86_400}d"
|
|
409
|
+
elsif secs >= 3600
|
|
410
|
+
"#{secs / 3600}h"
|
|
411
|
+
elsif secs >= 60
|
|
412
|
+
"#{secs / 60}min"
|
|
413
|
+
else
|
|
414
|
+
"#{secs}s"
|
|
415
|
+
end
|
|
416
|
+
end
|
|
275
417
|
end
|
|
276
418
|
end
|