catpm 0.2.0 → 0.4.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/app/controllers/catpm/application_controller.rb +8 -0
- data/app/controllers/catpm/endpoints_controller.rb +44 -7
- data/app/controllers/catpm/errors_controller.rb +31 -7
- data/app/controllers/catpm/events_controller.rb +30 -6
- data/app/controllers/catpm/status_controller.rb +15 -3
- data/app/controllers/catpm/system_controller.rb +0 -3
- data/app/helpers/catpm/application_helper.rb +19 -7
- data/app/views/catpm/endpoints/show.html.erb +42 -20
- data/app/views/catpm/errors/show.html.erb +12 -7
- data/app/views/catpm/events/index.html.erb +2 -2
- data/app/views/catpm/events/show.html.erb +12 -8
- data/app/views/catpm/samples/show.html.erb +20 -34
- data/app/views/catpm/shared/_page_nav.html.erb +3 -1
- data/app/views/catpm/status/index.html.erb +2 -2
- data/app/views/catpm/system/index.html.erb +2 -2
- data/app/views/layouts/catpm/application.html.erb +62 -16
- data/config/routes.rb +1 -0
- data/lib/catpm/collector.rb +215 -150
- data/lib/catpm/configuration.rb +6 -2
- data/lib/catpm/event.rb +13 -7
- data/lib/catpm/flusher.rb +104 -95
- data/lib/catpm/segment_subscribers.rb +2 -0
- data/lib/catpm/stack_sampler.rb +63 -19
- data/lib/catpm/tdigest.rb +2 -1
- data/lib/catpm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 30a794032ccb0fcb32152d5ae6c7285758260c6a80c5241c217924a887040885
|
|
4
|
+
data.tar.gz: 7691f6480aa62e4efd41d657fda9e1501a2765584a3d926a5d9ac3a8b6ac0c98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c902a837f12312f1f856d232b056eb3669c8648f5907e5088ced55972888d64682749f937a994d79b040688d1f59d929dccd11a1d28ffdcfe4e25f2458c02f49
|
|
7
|
+
data.tar.gz: 73dd36a0cb7ea32b405388bb62f54e9f327f30155b3b30794101feb056abe70b7b747cd5dda3dc67e6669e4289392964b37ae76385e6cb680c96e5a5bb550d55
|
|
@@ -2,5 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Catpm
|
|
4
4
|
class ApplicationController < ActionController::Base
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def remembered_range
|
|
8
|
+
if params[:range].present?
|
|
9
|
+
cookies[:catpm_range] = { value: params[:range], expires: 1.year.from_now }
|
|
10
|
+
end
|
|
11
|
+
params[:range] || cookies[:catpm_range]
|
|
12
|
+
end
|
|
5
13
|
end
|
|
6
14
|
end
|
|
@@ -8,7 +8,7 @@ module Catpm
|
|
|
8
8
|
@operation = params[:operation].presence || ''
|
|
9
9
|
|
|
10
10
|
# Time range filter
|
|
11
|
-
@range, period,
|
|
11
|
+
@range, period, bucket_seconds = helpers.parse_range(remembered_range)
|
|
12
12
|
|
|
13
13
|
scope = Catpm::Bucket
|
|
14
14
|
.where(kind: @kind, target: @target, operation: @operation)
|
|
@@ -23,11 +23,15 @@ module Catpm
|
|
|
23
23
|
'MAX(duration_max)',
|
|
24
24
|
'MIN(duration_min)',
|
|
25
25
|
'SUM(failure_count)',
|
|
26
|
-
'SUM(success_count)'
|
|
26
|
+
'SUM(success_count)',
|
|
27
|
+
'MIN(bucket_start)',
|
|
28
|
+
'MAX(bucket_start)'
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
@count, @duration_sum, @duration_max, @duration_min, @failure_count, @success_count =
|
|
30
|
-
@aggregate.map { |v| v || 0 }
|
|
32
|
+
@aggregate[0..5].map { |v| v || 0 }
|
|
33
|
+
@first_event_at = @aggregate[6]
|
|
34
|
+
@last_event_at = @aggregate[7]
|
|
31
35
|
|
|
32
36
|
@avg_duration = @count > 0 ? @duration_sum / @count : 0.0
|
|
33
37
|
@failure_rate = @count > 0 ? @failure_count.to_f / @count : 0.0
|
|
@@ -45,19 +49,52 @@ module Catpm
|
|
|
45
49
|
end
|
|
46
50
|
end
|
|
47
51
|
|
|
52
|
+
# Chart data — request volume, errors, avg duration
|
|
53
|
+
chart_buckets = scope.order(bucket_start: :asc).to_a
|
|
54
|
+
bucket_seconds = helpers.compute_bucket_seconds(chart_buckets) if @range == 'all'
|
|
55
|
+
|
|
56
|
+
if bucket_seconds
|
|
57
|
+
slots_count = {}
|
|
58
|
+
slots_errors = {}
|
|
59
|
+
slots_dur_sum = {}
|
|
60
|
+
slots_dur_count = {}
|
|
61
|
+
chart_buckets.each do |b|
|
|
62
|
+
slot_key = (b.bucket_start.to_i / bucket_seconds) * bucket_seconds
|
|
63
|
+
slots_count[slot_key] = (slots_count[slot_key] || 0) + b.count
|
|
64
|
+
slots_errors[slot_key] = (slots_errors[slot_key] || 0) + b.failure_count
|
|
65
|
+
slots_dur_sum[slot_key] = (slots_dur_sum[slot_key] || 0) + b.duration_sum
|
|
66
|
+
slots_dur_count[slot_key] = (slots_dur_count[slot_key] || 0) + b.count
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
now_slot = (Time.current.to_i / bucket_seconds) * bucket_seconds
|
|
70
|
+
@chart_requests = 60.times.map { |i| slots_count[now_slot - (59 - i) * bucket_seconds] || 0 }
|
|
71
|
+
@chart_errors = 60.times.map { |i| slots_errors[now_slot - (59 - i) * bucket_seconds] || 0 }
|
|
72
|
+
@chart_durations = 60.times.map do |i|
|
|
73
|
+
key = now_slot - (59 - i) * bucket_seconds
|
|
74
|
+
c = slots_dur_count[key] || 0
|
|
75
|
+
c > 0 ? (slots_dur_sum[key] / c).round(1) : 0
|
|
76
|
+
end
|
|
77
|
+
@chart_times = 60.times.map { |i| Time.at(now_slot - (59 - i) * bucket_seconds).strftime('%H:%M') }
|
|
78
|
+
end
|
|
79
|
+
|
|
48
80
|
endpoint_samples = Catpm::Sample
|
|
49
81
|
.joins(:bucket)
|
|
50
82
|
.where(catpm_buckets: { kind: @kind, target: @target, operation: @operation })
|
|
51
83
|
|
|
52
|
-
if @range != 'all'
|
|
53
|
-
endpoint_samples = endpoint_samples.where('catpm_samples.recorded_at >= ?', period.ago)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
84
|
@slow_samples = endpoint_samples.where(sample_type: 'slow').order(duration: :desc).limit(10)
|
|
57
85
|
@samples = endpoint_samples.where(sample_type: 'random').order(recorded_at: :desc).limit(10)
|
|
58
86
|
@error_samples = endpoint_samples.where(sample_type: 'error').order(recorded_at: :desc).limit(10)
|
|
59
87
|
|
|
60
88
|
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
61
89
|
end
|
|
90
|
+
|
|
91
|
+
def destroy
|
|
92
|
+
kind = params[:kind]
|
|
93
|
+
target = params[:target]
|
|
94
|
+
operation = params[:operation].presence || ''
|
|
95
|
+
|
|
96
|
+
Catpm::Bucket.where(kind: kind, target: target, operation: operation).destroy_all
|
|
97
|
+
redirect_to catpm.status_index_path, notice: 'Endpoint deleted'
|
|
98
|
+
end
|
|
62
99
|
end
|
|
63
100
|
end
|
|
@@ -36,7 +36,7 @@ module Catpm
|
|
|
36
36
|
@contexts = @error.parsed_contexts
|
|
37
37
|
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
38
38
|
|
|
39
|
-
@range, period, bucket_seconds = helpers.parse_range(
|
|
39
|
+
@range, period, bucket_seconds = helpers.parse_range(remembered_range)
|
|
40
40
|
|
|
41
41
|
# Samples table: 20 most recent linked by fingerprint
|
|
42
42
|
@samples = Catpm::Sample.where(error_fingerprint: @error.fingerprint)
|
|
@@ -66,12 +66,36 @@ module Catpm
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
slots = {}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
if @range == 'all'
|
|
70
|
+
# Collect all timestamps to compute dynamic bucket_seconds
|
|
71
|
+
all_timestamps = []
|
|
72
|
+
(ob[resolution] || {}).each do |ts_str, count|
|
|
73
|
+
ts = ts_str.to_i
|
|
74
|
+
all_timestamps << ts
|
|
75
|
+
slots[ts] = (slots[ts] || 0) + count
|
|
76
|
+
end
|
|
77
|
+
if all_timestamps.any?
|
|
78
|
+
span = all_timestamps.max - all_timestamps.min
|
|
79
|
+
span = 3600 if span < 3600
|
|
80
|
+
bucket_seconds = (span / 60.0).ceil
|
|
81
|
+
else
|
|
82
|
+
bucket_seconds = 60
|
|
83
|
+
end
|
|
84
|
+
# Re-bucket with computed bucket_seconds
|
|
85
|
+
rebucketed = {}
|
|
86
|
+
slots.each do |ts, count|
|
|
87
|
+
slot_key = (ts / bucket_seconds) * bucket_seconds
|
|
88
|
+
rebucketed[slot_key] = (rebucketed[slot_key] || 0) + count
|
|
89
|
+
end
|
|
90
|
+
slots = rebucketed
|
|
91
|
+
else
|
|
92
|
+
cutoff = period.ago.to_i
|
|
93
|
+
(ob[resolution] || {}).each do |ts_str, count|
|
|
94
|
+
ts = ts_str.to_i
|
|
95
|
+
next if ts < cutoff
|
|
96
|
+
slot_key = (ts / bucket_seconds) * bucket_seconds
|
|
97
|
+
slots[slot_key] = (slots[slot_key] || 0) + count
|
|
98
|
+
end
|
|
75
99
|
end
|
|
76
100
|
|
|
77
101
|
now_slot = (Time.current.to_i / bucket_seconds) * bucket_seconds
|
|
@@ -5,13 +5,25 @@ module Catpm
|
|
|
5
5
|
PER_PAGE = 25
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
|
-
@range, period, bucket_seconds = helpers.parse_range(
|
|
8
|
+
@range, period, bucket_seconds = helpers.parse_range(remembered_range)
|
|
9
9
|
|
|
10
|
-
recent_buckets =
|
|
10
|
+
recent_buckets = if @range == 'all'
|
|
11
|
+
Catpm::EventBucket.all.to_a
|
|
12
|
+
else
|
|
13
|
+
Catpm::EventBucket.recent(period).to_a
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
bucket_seconds = helpers.compute_bucket_seconds(recent_buckets) if @range == 'all'
|
|
11
17
|
|
|
12
18
|
# Hero metrics
|
|
13
19
|
@total_events = recent_buckets.sum(&:count)
|
|
14
|
-
|
|
20
|
+
effective_period = if @range == 'all'
|
|
21
|
+
earliest = recent_buckets.min_by(&:bucket_start)&.bucket_start
|
|
22
|
+
earliest ? [Time.current - earliest, 60].max : 3600
|
|
23
|
+
else
|
|
24
|
+
period
|
|
25
|
+
end
|
|
26
|
+
period_minutes = effective_period.to_f / 60
|
|
15
27
|
@events_per_min = (period_minutes > 0 ? @total_events / period_minutes : 0).round(1)
|
|
16
28
|
|
|
17
29
|
# Group by name for table
|
|
@@ -59,13 +71,25 @@ module Catpm
|
|
|
59
71
|
|
|
60
72
|
def show
|
|
61
73
|
@name = params[:name]
|
|
62
|
-
@range, period, bucket_seconds = helpers.parse_range(
|
|
74
|
+
@range, period, bucket_seconds = helpers.parse_range(remembered_range)
|
|
63
75
|
|
|
64
|
-
recent_buckets =
|
|
76
|
+
recent_buckets = if @range == 'all'
|
|
77
|
+
Catpm::EventBucket.by_name(@name).all.to_a
|
|
78
|
+
else
|
|
79
|
+
Catpm::EventBucket.by_name(@name).recent(period).to_a
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
bucket_seconds = helpers.compute_bucket_seconds(recent_buckets) if @range == 'all'
|
|
65
83
|
|
|
66
84
|
# Hero metrics
|
|
67
85
|
@total_count = recent_buckets.sum(&:count)
|
|
68
|
-
|
|
86
|
+
effective_period = if @range == 'all'
|
|
87
|
+
earliest = recent_buckets.min_by(&:bucket_start)&.bucket_start
|
|
88
|
+
earliest ? [Time.current - earliest, 60].max : 3600
|
|
89
|
+
else
|
|
90
|
+
period
|
|
91
|
+
end
|
|
92
|
+
period_minutes = effective_period.to_f / 60
|
|
69
93
|
@events_per_min = (period_minutes > 0 ? @total_count / period_minutes : 0).round(1)
|
|
70
94
|
@last_seen = recent_buckets.map(&:bucket_start).max
|
|
71
95
|
|
|
@@ -6,9 +6,15 @@ module Catpm
|
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
8
|
# Time range (parsed first — everything below uses this)
|
|
9
|
-
@range, period, bucket_seconds = helpers.parse_range(
|
|
9
|
+
@range, period, bucket_seconds = helpers.parse_range(remembered_range)
|
|
10
10
|
|
|
11
|
-
recent_buckets =
|
|
11
|
+
recent_buckets = if @range == 'all'
|
|
12
|
+
Catpm::Bucket.all.to_a
|
|
13
|
+
else
|
|
14
|
+
Catpm::Bucket.recent(period).to_a
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
bucket_seconds = helpers.compute_bucket_seconds(recent_buckets) if @range == 'all'
|
|
12
18
|
|
|
13
19
|
# Sparkline data
|
|
14
20
|
slots = {}
|
|
@@ -31,7 +37,13 @@ module Catpm
|
|
|
31
37
|
|
|
32
38
|
recent_count = recent_buckets.sum(&:count)
|
|
33
39
|
recent_failures = recent_buckets.sum(&:failure_count)
|
|
34
|
-
|
|
40
|
+
earliest_bucket = recent_buckets.min_by(&:bucket_start)&.bucket_start
|
|
41
|
+
effective_period = if @range == 'all'
|
|
42
|
+
earliest_bucket ? [Time.current - earliest_bucket, 60].max : 3600
|
|
43
|
+
else
|
|
44
|
+
earliest_bucket ? [[period, Time.current - earliest_bucket].min, 60].max : period
|
|
45
|
+
end
|
|
46
|
+
period_minutes = effective_period.to_f / 60
|
|
35
47
|
@recent_avg_duration = recent_count > 0 ? (recent_buckets.sum(&:duration_sum) / recent_count).round(1) : 0.0
|
|
36
48
|
@error_rate = recent_count > 0 ? (recent_failures.to_f / recent_count * 100).round(1) : 0.0
|
|
37
49
|
@requests_per_min = (recent_count / period_minutes).round(1)
|
|
@@ -7,9 +7,6 @@ module Catpm
|
|
|
7
7
|
@buffer_size = Catpm.buffer&.size || 0
|
|
8
8
|
@buffer_bytes = Catpm.buffer&.current_bytes || 0
|
|
9
9
|
@config = Catpm.config
|
|
10
|
-
@bucket_count = Catpm::Bucket.count
|
|
11
|
-
@sample_count = Catpm::Sample.count
|
|
12
|
-
@error_count = Catpm::ErrorRecord.count
|
|
13
10
|
@oldest_bucket = Catpm::Bucket.minimum(:bucket_start)
|
|
14
11
|
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
15
12
|
end
|
|
@@ -116,10 +116,14 @@ module Catpm
|
|
|
116
116
|
circles = parsed.map.with_index do |(x, y, val), i|
|
|
117
117
|
label = labels ? labels[i] : val.is_a?(Float) ? ('%.1f' % val) : val
|
|
118
118
|
time_attr = time_labels ? %( data-time="#{time_labels[i]}") : ''
|
|
119
|
-
%(<circle cx="#{x}" cy="#{y}" r="
|
|
119
|
+
%(<circle cx="#{x}" cy="#{y}" r="0" data-value="#{label}"#{time_attr} class="sparkline-dot"/>)
|
|
120
120
|
end.join
|
|
121
121
|
|
|
122
|
-
%(<
|
|
122
|
+
capture = %(<rect width="#{width}" height="#{height}" fill="transparent"/>)
|
|
123
|
+
highlight = %(<circle cx="0" cy="0" r="3" fill="#{color}" class="sparkline-highlight" style="display:none"/>)
|
|
124
|
+
vline = %(<line x1="0" y1="0" x2="0" y2="#{height}" stroke="#{color}" stroke-width="0.5" opacity="0.4" class="sparkline-vline" style="display:none"/>)
|
|
125
|
+
|
|
126
|
+
%(<svg class="sparkline-chart" width="#{width}" height="#{height}" viewBox="0 0 #{width} #{height}" xmlns="http://www.w3.org/2000/svg" style="display:block">#{capture}#{fill_el}<polyline points="#{coords_str.join(" ")}" fill="none" stroke="#{color}" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>#{circles}#{vline}#{highlight}</svg>).html_safe
|
|
123
127
|
end
|
|
124
128
|
|
|
125
129
|
def bar_chart_svg(data_points, width: 600, height: 200, color: 'var(--accent)', time_labels: nil)
|
|
@@ -207,16 +211,16 @@ module Catpm
|
|
|
207
211
|
%(<span class="status-dot"><span class="dot" style="background:#{color}"></span> #{label}</span>).html_safe
|
|
208
212
|
end
|
|
209
213
|
|
|
210
|
-
def parse_range(range_str
|
|
211
|
-
|
|
212
|
-
key
|
|
213
|
-
return [key, nil, nil] if extra_valid.include?(key) && !RANGES.key?(key)
|
|
214
|
+
def parse_range(range_str)
|
|
215
|
+
key = (RANGE_KEYS + ['all']).include?(range_str) ? range_str : 'all'
|
|
216
|
+
return [key, nil, nil] if key == 'all'
|
|
214
217
|
period, bucket_seconds = RANGES[key]
|
|
215
218
|
[key, period, bucket_seconds]
|
|
216
219
|
end
|
|
217
220
|
|
|
218
221
|
def range_label(range)
|
|
219
222
|
case range
|
|
223
|
+
when 'all' then 'All time'
|
|
220
224
|
when '6h' then 'Last 6 hours'
|
|
221
225
|
when '24h' then 'Last 24 hours'
|
|
222
226
|
when '1w' then 'Last week'
|
|
@@ -227,6 +231,14 @@ module Catpm
|
|
|
227
231
|
end
|
|
228
232
|
end
|
|
229
233
|
|
|
234
|
+
def compute_bucket_seconds(buckets)
|
|
235
|
+
return 60 if buckets.empty?
|
|
236
|
+
times = buckets.map { |b| b.bucket_start.to_i }
|
|
237
|
+
span = times.max - times.min
|
|
238
|
+
span = 3600 if span < 3600
|
|
239
|
+
(span / 60.0).ceil
|
|
240
|
+
end
|
|
241
|
+
|
|
230
242
|
def pagination_nav(current_page, total_count, per_page, extra_params: {})
|
|
231
243
|
total_pages = (total_count.to_f / per_page).ceil
|
|
232
244
|
return '' if total_pages <= 1
|
|
@@ -236,7 +248,7 @@ module Catpm
|
|
|
236
248
|
prev_url = '?' + prev_params.compact.map { |k, v| "#{k}=#{v}" }.join('&')
|
|
237
249
|
next_url = '?' + next_params.compact.map { |k, v| "#{k}=#{v}" }.join('&')
|
|
238
250
|
|
|
239
|
-
html = '<div class="pagination">'
|
|
251
|
+
html = +'<div class="pagination">'
|
|
240
252
|
if current_page > 1
|
|
241
253
|
html << %(<a href="#{prev_url}" class="btn">← Previous</a>)
|
|
242
254
|
else
|
|
@@ -8,15 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
<%= render "catpm/shared/page_nav", active: "performance" %>
|
|
10
10
|
|
|
11
|
-
<div class="breadcrumbs">
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
<div class="breadcrumbs" style="display:flex; align-items:center; justify-content:space-between">
|
|
12
|
+
<div>
|
|
13
|
+
<a href="<%= catpm.status_index_path %>">Performance</a>
|
|
14
|
+
<span class="sep">/</span>
|
|
15
|
+
<span><%= @target %></span>
|
|
16
|
+
</div>
|
|
17
|
+
<%= button_to "Delete Endpoint", catpm.endpoint_path(kind: @kind, target: @target, operation: @operation),
|
|
18
|
+
method: :delete, class: "btn btn-danger",
|
|
19
|
+
data: { confirm: "Delete this endpoint and all its data? This cannot be undone." } %>
|
|
15
20
|
</div>
|
|
16
21
|
|
|
17
22
|
<% ep_params = { kind: @kind, target: @target, operation: @operation } %>
|
|
18
23
|
<div class="time-range">
|
|
19
|
-
<% (Catpm::ApplicationHelper::RANGE_KEYS
|
|
24
|
+
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
20
25
|
<a href="<%= catpm.endpoint_path(ep_params.merge(range: r)) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
|
|
21
26
|
<% end %>
|
|
22
27
|
</div>
|
|
@@ -27,12 +32,12 @@
|
|
|
27
32
|
<div class="value"><%= @count %></div>
|
|
28
33
|
</div>
|
|
29
34
|
<div class="card">
|
|
30
|
-
<div class="label">
|
|
31
|
-
<div class="value"><%=
|
|
35
|
+
<div class="label">First Event</div>
|
|
36
|
+
<div class="value"><%= @first_event_at ? time_with_tooltip(@first_event_at) : "—" %></div>
|
|
32
37
|
</div>
|
|
33
38
|
<div class="card">
|
|
34
|
-
<div class="label">
|
|
35
|
-
<div class="value"><%= @
|
|
39
|
+
<div class="label">Last Event</div>
|
|
40
|
+
<div class="value"><%= @last_event_at ? time_with_tooltip(@last_event_at) : "—" %></div>
|
|
36
41
|
</div>
|
|
37
42
|
<div class="card">
|
|
38
43
|
<div class="label">Max</div>
|
|
@@ -101,19 +106,36 @@
|
|
|
101
106
|
</div>
|
|
102
107
|
</div>
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
<% end %>
|
|
110
|
+
|
|
111
|
+
<% has_requests = @chart_requests&.any? { |v| v > 0 } %>
|
|
112
|
+
<% has_errors = @chart_errors&.any? { |v| v > 0 } %>
|
|
113
|
+
<% has_durations = @chart_durations&.any? { |v| v > 0 } %>
|
|
114
|
+
<% if has_requests || has_errors || has_durations %>
|
|
115
|
+
<div class="charts-row">
|
|
116
|
+
<% if has_requests %>
|
|
117
|
+
<div class="chart-card">
|
|
118
|
+
<div class="label">Request Volume</div>
|
|
119
|
+
<div class="value"><%= @chart_requests.sum %></div>
|
|
120
|
+
<div class="detail"><%= range_label(@range) %></div>
|
|
121
|
+
<div class="sparkline"><%= sparkline_svg(@chart_requests, width: 500, height: 64, color: "var(--accent)", fill: true, time_labels: @chart_times) %></div>
|
|
110
122
|
</div>
|
|
111
123
|
<% end %>
|
|
112
|
-
<% if
|
|
113
|
-
<div class="card">
|
|
114
|
-
<div class="label">
|
|
115
|
-
<div class="value"><%=
|
|
116
|
-
<div class="detail"
|
|
124
|
+
<% if has_errors %>
|
|
125
|
+
<div class="chart-card">
|
|
126
|
+
<div class="label">Error Volume</div>
|
|
127
|
+
<div class="value"><%= @chart_errors.sum %></div>
|
|
128
|
+
<div class="detail"><%= range_label(@range) %></div>
|
|
129
|
+
<div class="sparkline"><%= sparkline_svg(@chart_errors, width: 500, height: 64, color: "var(--red)", fill: true, time_labels: @chart_times) %></div>
|
|
130
|
+
</div>
|
|
131
|
+
<% end %>
|
|
132
|
+
<% if has_durations %>
|
|
133
|
+
<div class="chart-card">
|
|
134
|
+
<div class="label">Avg Duration</div>
|
|
135
|
+
<% nonzero = @chart_durations.select { |v| v > 0 } %>
|
|
136
|
+
<div class="value"><%= nonzero.any? ? format_duration(nonzero.sum / nonzero.size) : "—" %></div>
|
|
137
|
+
<div class="detail"><%= range_label(@range) %></div>
|
|
138
|
+
<div class="sparkline"><%= sparkline_svg(@chart_durations, width: 500, height: 64, color: "var(--accent)", fill: true, labels: @chart_durations.map { |d| format_duration(d) }, time_labels: @chart_times) %></div>
|
|
117
139
|
</div>
|
|
118
140
|
<% end %>
|
|
119
141
|
</div>
|
|
@@ -50,16 +50,21 @@
|
|
|
50
50
|
|
|
51
51
|
<%# ─── Error Frequency Chart ─── %>
|
|
52
52
|
<div class="time-range">
|
|
53
|
-
<% Catpm::ApplicationHelper::RANGE_KEYS.each do |r| %>
|
|
54
|
-
<a href="<%= catpm.error_path(@error, range: r) %>" class="<%= 'active' if @range == r %>"><%= r %></a>
|
|
53
|
+
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
54
|
+
<a href="<%= catpm.error_path(@error, range: r) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
|
|
55
55
|
<% end %>
|
|
56
56
|
</div>
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<div
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
<% if @chart_data&.any? { |v| v > 0 } %>
|
|
59
|
+
<div class="charts-row">
|
|
60
|
+
<div class="chart-card">
|
|
61
|
+
<div class="label">Error Frequency</div>
|
|
62
|
+
<div class="value"><%= @chart_data.sum %></div>
|
|
63
|
+
<div class="detail">occurrences · <%= range_label(@range) %></div>
|
|
64
|
+
<div class="sparkline"><%= sparkline_svg(@chart_data, width: 500, height: 64, color: "var(--red)", fill: true, time_labels: @chart_times) %></div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
63
68
|
|
|
64
69
|
<%# ─── Backtrace ─── %>
|
|
65
70
|
<% first_bt = @contexts.first && (@contexts.first["backtrace"] || @contexts.first[:backtrace] || []) %>
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
<%# ─── Time Range ─── %>
|
|
7
7
|
<div class="time-range">
|
|
8
|
-
<% Catpm::ApplicationHelper::RANGE_KEYS.each do |r| %>
|
|
9
|
-
<a href="<%= catpm.events_path(range: r) %>" class="<%= 'active' if @range == r %>"><%= r %></a>
|
|
8
|
+
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
9
|
+
<a href="<%= catpm.events_path(range: r) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
|
|
10
10
|
<% end %>
|
|
11
11
|
<span style="margin-left:auto; font-size:12px; color:var(--text-2)">
|
|
12
12
|
Updated <%= Time.current.strftime("%H:%M:%S") %>
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
<%# ─── Time Range ─── %>
|
|
13
13
|
<div class="time-range">
|
|
14
|
-
<% Catpm::ApplicationHelper::RANGE_KEYS.each do |r| %>
|
|
15
|
-
<a href="<%= catpm.event_path(name: @name, range: r) %>" class="<%= 'active' if @range == r %>"><%= r %></a>
|
|
14
|
+
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
15
|
+
<a href="<%= catpm.event_path(name: @name, range: r) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
|
|
16
16
|
<% end %>
|
|
17
17
|
</div>
|
|
18
18
|
|
|
@@ -34,12 +34,16 @@
|
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
<div
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
<% if @chart_data&.any? { |v| v > 0 } %>
|
|
38
|
+
<div class="charts-row">
|
|
39
|
+
<div class="chart-card">
|
|
40
|
+
<div class="label">Event Volume</div>
|
|
41
|
+
<div class="value"><%= @chart_data.sum %></div>
|
|
42
|
+
<div class="detail"><%= range_label(@range) %></div>
|
|
43
|
+
<div class="sparkline"><%= sparkline_svg(@chart_data, width: 500, height: 64, color: "var(--accent)", fill: true, time_labels: @chart_times) %></div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
43
47
|
|
|
44
48
|
<%# ─── Recent Samples ─── %>
|
|
45
49
|
<h2>Recent Samples</h2>
|
|
@@ -34,48 +34,34 @@
|
|
|
34
34
|
<% end %>
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
|
-
<%# ─── Request Context
|
|
37
|
+
<%# ─── Request Context ─── %>
|
|
38
38
|
<%
|
|
39
|
-
ctx_display = @context.except("segments", :segments, "segment_summary", :segment_summary, "segments_capped", :segments_capped, "backtrace", :backtrace)
|
|
39
|
+
ctx_display = @context.except("segments", :segments, "segment_summary", :segment_summary, "segments_capped", :segments_capped, "backtrace", :backtrace, "method", :method, "path", :path, "status", :status)
|
|
40
40
|
ctx_flat = ctx_display.select { |_, v| !v.is_a?(Hash) && !v.is_a?(Array) }
|
|
41
41
|
ctx_nested = ctx_display.select { |_, v| v.is_a?(Hash) || v.is_a?(Array) }
|
|
42
42
|
%>
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<%
|
|
49
|
-
<div class="
|
|
50
|
-
|
|
51
|
-
<div class="ctx-key"><%= k %></div>
|
|
52
|
-
<div class="ctx-val"><%= v.to_s.truncate(200) %></div>
|
|
53
|
-
<% end %>
|
|
54
|
-
</div>
|
|
55
|
-
<% end %>
|
|
56
|
-
<% if ctx_nested.any? %>
|
|
57
|
-
<% ctx_nested.each do |k, v| %>
|
|
58
|
-
<details class="collapsible" open>
|
|
59
|
-
<summary><%= k %></summary>
|
|
60
|
-
<div class="details-body">
|
|
61
|
-
<pre class="mono" style="color:var(--text-1); white-space:pre-wrap; font-size:12px"><%= JSON.pretty_generate(v) rescue v.inspect %></pre>
|
|
62
|
-
</div>
|
|
63
|
-
</details>
|
|
64
|
-
<% end %>
|
|
44
|
+
<% if ctx_display.any? %>
|
|
45
|
+
<h2>Request Context</h2>
|
|
46
|
+
<% if ctx_flat.any? %>
|
|
47
|
+
<div class="context-grid" style="margin-bottom:12px">
|
|
48
|
+
<% ctx_flat.each do |k, v| %>
|
|
49
|
+
<div class="ctx-key"><%= k %></div>
|
|
50
|
+
<div class="ctx-val"><%= v.to_s.truncate(200) %></div>
|
|
65
51
|
<% end %>
|
|
66
52
|
</div>
|
|
67
53
|
<% end %>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
54
|
+
<% if ctx_nested.any? %>
|
|
55
|
+
<% ctx_nested.each do |k, v| %>
|
|
56
|
+
<details class="collapsible" open>
|
|
57
|
+
<summary><%= k %></summary>
|
|
58
|
+
<div class="details-body">
|
|
59
|
+
<pre class="mono" style="color:var(--text-1); white-space:pre-wrap; font-size:12px"><%= JSON.pretty_generate(v) rescue v.inspect %></pre>
|
|
60
|
+
</div>
|
|
61
|
+
</details>
|
|
62
|
+
<% end %>
|
|
63
|
+
<% end %>
|
|
64
|
+
<% end %>
|
|
79
65
|
|
|
80
66
|
<%# ─── Time Breakdown (full width, above waterfall) ─── %>
|
|
81
67
|
<% if @summary.any? %>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<div class="page-nav">
|
|
2
2
|
<a href="<%= catpm.status_index_path %>"<%= ' class="active"'.html_safe if active == "performance" %>>Performance</a>
|
|
3
|
-
|
|
3
|
+
<% if Catpm.config.events_enabled || Catpm::EventBucket.exists? %>
|
|
4
|
+
<a href="<%= catpm.events_path %>"<%= ' class="active"'.html_safe if active == "events" %>>Events</a>
|
|
5
|
+
<% end %>
|
|
4
6
|
<a href="<%= catpm.errors_path %>"<%= ' class="active"'.html_safe if active == "errors" %>>Errors<% if @active_error_count.to_i > 0 %><span class="nav-count alert"><%= @active_error_count %></span><% end %></a>
|
|
5
7
|
<a href="<%= catpm.system_index_path %>"<%= ' class="active"'.html_safe if active == "system" %>>System</a>
|
|
6
8
|
</div>
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
<%# ─── Time Range ─── %>
|
|
7
7
|
<div class="time-range">
|
|
8
|
-
<% Catpm::ApplicationHelper::RANGE_KEYS.each do |r| %>
|
|
9
|
-
<a href="<%= catpm.status_index_path(range: r) %>" class="<%= 'active' if @range == r %>"><%= r %></a>
|
|
8
|
+
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
9
|
+
<a href="<%= catpm.status_index_path(range: r) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
|
|
10
10
|
<% end %>
|
|
11
11
|
<span style="margin-left:auto; font-size:12px; color:var(--text-2)">
|
|
12
12
|
Updated <%= Time.current.strftime("%H:%M:%S") %> ·
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
<div class="pipeline-node">
|
|
39
39
|
<div class="node-icon"><svg width="28" height="28" viewBox="0 0 28 28" fill="none" stroke="var(--text-2)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="14" cy="8" rx="8" ry="4"/><path d="M6 8v12c0 2.2 3.58 4 8 4s8-1.8 8-4V8"/><path d="M6 14c0 2.2 3.58 4 8 4s8-1.8 8-4"/></svg></div>
|
|
40
40
|
<div class="node-label">Database</div>
|
|
41
|
-
<div class="node-value"
|
|
42
|
-
<div class="node-detail">Aggregated stats are stored as time buckets,
|
|
41
|
+
<div class="node-value" style="font-size:14px">Storage</div>
|
|
42
|
+
<div class="node-detail">Aggregated stats are stored as time buckets, with detailed samples and error fingerprints.<br><%= @oldest_bucket ? "Data since #{@oldest_bucket.strftime('%b %-d')}, retained #{@config.retention_period ? "#{(@config.retention_period / 1.day).to_i} days" : "forever"}." : "No data yet." %></div>
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
45
|
|