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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0938b1a8e37174b8c0415de3303614b40657302ac1cf4d366214fe9dd0fd769c'
4
- data.tar.gz: cc9308558a8a7065ff99d225fb18244ab8f51492af67f3d7ba83fa2e48923b35
3
+ metadata.gz: c9d4f65535550d3a1201fe91d55206136dac09a1fb028e37b01e860a81260abc
4
+ data.tar.gz: 5ba5802a2361921a743745b88983399a1f792c7539f40c5a0fc804375c435101
5
5
  SHA512:
6
- metadata.gz: b5c7af3130bf87eb185ca324214dfefc2e5c4bd1ecf5a180200ade47847c3f9fa710bc8bc108055f969344e76134f5369bcd82441193ac9199bcbe072093d0e9
7
- data.tar.gz: 1c47ef33465c2eaa31a92c7cf538afc9cc9b055ef21128181888c94c1363b886e6bc81c4cabfb229d856b4d500856ac189820c020326615a460698737578cf1b
6
+ metadata.gz: b3aee4ecf169a3254ff5193c77e7d1579559d0cfd7915ceeba7d6608ebb397c813dd9e3bf1170ea253ef5c893a1c8a42d009d5d911a8b46eb9a5c194b51fed23
7
+ data.tar.gz: fc242376c61f8b0f6943bb534922f6b854f6896e554ab2aa26f72a63a3aea615b243c4d14d3ec1c9738ab2457aeaabfbbbb277ca5a4c8d7b91a8a220e1685e97
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  gem build catpm.gemspec
2
- gem push catpm-0.6.0.gem
2
+ gem push catpm-0.6.6.gem
3
3
 
4
4
  # Catpm
5
5
 
@@ -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 }
@@ -11,5 +11,9 @@ module Catpm
11
11
  @active_error_count = Catpm::ErrorRecord.unresolved.count
12
12
  @table_sizes = Catpm::Adapter.current.table_sizes
13
13
  end
14
+
15
+ def pipeline
16
+ render layout: "catpm/pipeline"
17
+ end
14
18
  end
15
19
  end
@@ -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