catpm 0.3.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/endpoints_controller.rb +29 -5
- data/app/controllers/catpm/errors_controller.rb +30 -6
- data/app/controllers/catpm/events_controller.rb +28 -4
- data/app/controllers/catpm/status_controller.rb +14 -2
- data/app/helpers/catpm/application_helper.rb +18 -6
- data/app/views/catpm/endpoints/show.html.erb +29 -12
- 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/status/index.html.erb +2 -2
- data/app/views/layouts/catpm/application.html.erb +62 -16
- data/lib/catpm/configuration.rb +6 -2
- data/lib/catpm/event.rb +11 -5
- data/lib/catpm/flusher.rb +96 -74
- data/lib/catpm/segment_subscribers.rb +2 -0
- data/lib/catpm/stack_sampler.rb +11 -8
- 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
|
|
@@ -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)
|
|
@@ -49,14 +49,38 @@ module Catpm
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
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
|
+
|
|
52
80
|
endpoint_samples = Catpm::Sample
|
|
53
81
|
.joins(:bucket)
|
|
54
82
|
.where(catpm_buckets: { kind: @kind, target: @target, operation: @operation })
|
|
55
83
|
|
|
56
|
-
if @range != 'all'
|
|
57
|
-
endpoint_samples = endpoint_samples.where('catpm_samples.recorded_at >= ?', period.ago)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
84
|
@slow_samples = endpoint_samples.where(sample_type: 'slow').order(duration: :desc).limit(10)
|
|
61
85
|
@samples = endpoint_samples.where(sample_type: 'random').order(recorded_at: :desc).limit(10)
|
|
62
86
|
@error_samples = endpoint_samples.where(sample_type: 'error').order(recorded_at: :desc).limit(10)
|
|
@@ -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
|
|
@@ -7,11 +7,23 @@ module Catpm
|
|
|
7
7
|
def index
|
|
8
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
|
|
@@ -61,11 +73,23 @@ module Catpm
|
|
|
61
73
|
@name = params[:name]
|
|
62
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
|
|
|
@@ -8,7 +8,13 @@ module Catpm
|
|
|
8
8
|
# Time range (parsed first — everything below uses this)
|
|
9
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)
|
|
@@ -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
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
<% ep_params = { kind: @kind, target: @target, operation: @operation } %>
|
|
23
23
|
<div class="time-range">
|
|
24
|
-
<% (Catpm::ApplicationHelper::RANGE_KEYS
|
|
24
|
+
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
25
25
|
<a href="<%= catpm.endpoint_path(ep_params.merge(range: r)) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
|
|
26
26
|
<% end %>
|
|
27
27
|
</div>
|
|
@@ -106,19 +106,36 @@
|
|
|
106
106
|
</div>
|
|
107
107
|
</div>
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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>
|
|
115
122
|
</div>
|
|
116
123
|
<% end %>
|
|
117
|
-
<% if
|
|
118
|
-
<div class="card">
|
|
119
|
-
<div class="label">
|
|
120
|
-
<div class="value"><%=
|
|
121
|
-
<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>
|
|
122
139
|
</div>
|
|
123
140
|
<% end %>
|
|
124
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>
|
|
@@ -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") %> ·
|
|
@@ -230,7 +230,17 @@
|
|
|
230
230
|
.bar-chart-wrap { position: relative; }
|
|
231
231
|
.bar-chart-max { position: absolute; top: 2px; left: 4px; font-size: 11px; color: var(--text-2); font-family: var(--font-sans); pointer-events: none; line-height: 1; }
|
|
232
232
|
|
|
233
|
+
/* ─── Charts Row (2-up on wide screens) ─── */
|
|
234
|
+
.charts-row { display: grid; grid-template-columns: 1fr; gap: 12px; margin-bottom: 20px; }
|
|
235
|
+
@media (min-width: 900px) { .charts-row { grid-template-columns: 1fr 1fr; } }
|
|
236
|
+
.chart-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; }
|
|
237
|
+
.chart-card .label { color: var(--text-1); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; }
|
|
238
|
+
.chart-card .value { color: var(--text-0); font-size: 20px; font-weight: 600; margin-top: 2px; line-height: 1.2; }
|
|
239
|
+
.chart-card .detail { color: var(--text-2); font-size: 12px; margin-top: 4px; }
|
|
240
|
+
.chart-card .sparkline { margin-top: 8px; }
|
|
241
|
+
|
|
233
242
|
/* ─── Sparkline Interaction ─── */
|
|
243
|
+
.sparkline-chart { cursor: default; }
|
|
234
244
|
.sparkline-tip { position: absolute; background: #ffffff; color: var(--text-0); font-size: 11px; font-family: var(--font-sans); padding: 4px 8px; border-radius: 4px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.12); pointer-events: none; white-space: nowrap; z-index: 10; display: none; line-height: 1.3; }
|
|
235
245
|
|
|
236
246
|
/* ─── Error Hero ─── */
|
|
@@ -344,29 +354,65 @@
|
|
|
344
354
|
});
|
|
345
355
|
}
|
|
346
356
|
|
|
347
|
-
/* Sparkline hover */
|
|
357
|
+
/* Sparkline hover — snap to nearest point by X */
|
|
348
358
|
var sparkTip = null;
|
|
349
|
-
var
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
359
|
+
var activeSvg = null;
|
|
360
|
+
|
|
361
|
+
function hideSparkline() {
|
|
362
|
+
if (activeSvg) {
|
|
363
|
+
var hl = activeSvg.querySelector('.sparkline-highlight');
|
|
364
|
+
var vl = activeSvg.querySelector('.sparkline-vline');
|
|
365
|
+
if (hl) hl.style.display = 'none';
|
|
366
|
+
if (vl) vl.style.display = 'none';
|
|
367
|
+
activeSvg = null;
|
|
368
|
+
}
|
|
369
|
+
if (sparkTip) sparkTip.style.display = 'none';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
document.addEventListener('mousemove', function(e) {
|
|
373
|
+
var svg = e.target.closest('.sparkline-chart');
|
|
374
|
+
if (!svg) { hideSparkline(); return; }
|
|
375
|
+
|
|
376
|
+
var rect = svg.getBoundingClientRect();
|
|
377
|
+
var vb = svg.viewBox.baseVal;
|
|
378
|
+
var scaleX = vb.width / rect.width;
|
|
379
|
+
var mouseX = (e.clientX - rect.left) * scaleX;
|
|
380
|
+
|
|
381
|
+
var dots = svg.querySelectorAll('.sparkline-dot');
|
|
382
|
+
var nearest = null, nearestDist = Infinity;
|
|
383
|
+
for (var i = 0; i < dots.length; i++) {
|
|
384
|
+
var cx = parseFloat(dots[i].getAttribute('cx'));
|
|
385
|
+
var dist = Math.abs(cx - mouseX);
|
|
386
|
+
if (dist < nearestDist) { nearestDist = dist; nearest = dots[i]; }
|
|
387
|
+
}
|
|
388
|
+
if (!nearest) { hideSparkline(); return; }
|
|
389
|
+
|
|
390
|
+
var nx = nearest.getAttribute('cx');
|
|
391
|
+
var ny = nearest.getAttribute('cy');
|
|
392
|
+
|
|
393
|
+
// Highlight dot
|
|
394
|
+
var hl = svg.querySelector('.sparkline-highlight');
|
|
395
|
+
if (hl) { hl.setAttribute('cx', nx); hl.setAttribute('cy', ny); hl.style.display = ''; }
|
|
396
|
+
|
|
397
|
+
// Vertical guide line
|
|
398
|
+
var vl = svg.querySelector('.sparkline-vline');
|
|
399
|
+
if (vl) { vl.setAttribute('x1', nx); vl.setAttribute('x2', nx); vl.style.display = ''; }
|
|
400
|
+
|
|
401
|
+
activeSvg = svg;
|
|
402
|
+
|
|
403
|
+
// Tooltip
|
|
354
404
|
if (!sparkTip) { sparkTip = document.createElement('div'); sparkTip.className = 'sparkline-tip'; document.body.appendChild(sparkTip); }
|
|
355
|
-
var val =
|
|
356
|
-
var time =
|
|
405
|
+
var val = nearest.dataset.value || '';
|
|
406
|
+
var time = nearest.dataset.time || '';
|
|
357
407
|
sparkTip.innerHTML = '<strong>' + val + '</strong>' + (time ? '<br>' + time : '');
|
|
358
408
|
sparkTip.style.display = 'block';
|
|
359
|
-
});
|
|
360
|
-
document.addEventListener('mousemove', function(e) {
|
|
361
|
-
if (!sparkTip || sparkTip.style.display === 'none') return;
|
|
362
409
|
sparkTip.style.left = (e.pageX) + 'px';
|
|
363
410
|
sparkTip.style.top = (e.pageY + 16) + 'px';
|
|
364
411
|
});
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
});
|
|
412
|
+
|
|
413
|
+
document.addEventListener('mouseleave', function(e) {
|
|
414
|
+
if (e.target.closest && e.target.closest('.sparkline-chart')) hideSparkline();
|
|
415
|
+
}, true);
|
|
370
416
|
|
|
371
417
|
document.addEventListener('keydown', function(e) {
|
|
372
418
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
data/lib/catpm/configuration.rb
CHANGED
|
@@ -41,7 +41,9 @@ module Catpm
|
|
|
41
41
|
:shutdown_timeout,
|
|
42
42
|
:events_enabled,
|
|
43
43
|
:events_max_samples_per_name,
|
|
44
|
-
:track_own_requests
|
|
44
|
+
:track_own_requests,
|
|
45
|
+
:stack_sample_interval,
|
|
46
|
+
:max_stack_samples_per_request
|
|
45
47
|
|
|
46
48
|
def initialize
|
|
47
49
|
@enabled = true
|
|
@@ -58,7 +60,7 @@ module Catpm
|
|
|
58
60
|
@slow_threshold_per_kind = {}
|
|
59
61
|
@ignored_targets = []
|
|
60
62
|
@retention_period = nil # nil = keep forever (data is downsampled, not deleted)
|
|
61
|
-
@max_buffer_memory =
|
|
63
|
+
@max_buffer_memory = 8.megabytes
|
|
62
64
|
@flush_interval = 30 # seconds
|
|
63
65
|
@flush_jitter = 5 # ±seconds
|
|
64
66
|
@max_error_contexts = 5
|
|
@@ -84,6 +86,8 @@ module Catpm
|
|
|
84
86
|
@events_enabled = false
|
|
85
87
|
@events_max_samples_per_name = 20
|
|
86
88
|
@track_own_requests = false
|
|
89
|
+
@stack_sample_interval = 0.005 # seconds (5ms)
|
|
90
|
+
@max_stack_samples_per_request = 200
|
|
87
91
|
end
|
|
88
92
|
|
|
89
93
|
def slow_threshold_for(kind)
|
data/lib/catpm/event.rb
CHANGED
|
@@ -10,18 +10,24 @@ module Catpm
|
|
|
10
10
|
:metadata, :error_class, :error_message, :backtrace,
|
|
11
11
|
:sample_type, :context, :status
|
|
12
12
|
|
|
13
|
+
EMPTY_HASH = {}.freeze
|
|
14
|
+
private_constant :EMPTY_HASH
|
|
15
|
+
|
|
13
16
|
def initialize(kind:, target:, operation: '', duration: 0.0, started_at: nil,
|
|
14
|
-
metadata:
|
|
15
|
-
sample_type: nil, context:
|
|
17
|
+
metadata: nil, error_class: nil, error_message: nil, backtrace: nil,
|
|
18
|
+
sample_type: nil, context: nil, status: nil)
|
|
16
19
|
@kind = kind.to_s
|
|
17
20
|
@target = target.to_s
|
|
18
21
|
@operation = (operation || '').to_s
|
|
19
22
|
@duration = duration.to_f
|
|
20
23
|
@started_at = started_at || Time.current
|
|
21
|
-
@metadata = metadata ||
|
|
24
|
+
@metadata = metadata || EMPTY_HASH
|
|
22
25
|
@error_class = error_class
|
|
23
26
|
@error_message = error_message
|
|
24
|
-
@backtrace = backtrace
|
|
27
|
+
@backtrace = if backtrace
|
|
28
|
+
limit = Catpm.config.backtrace_lines
|
|
29
|
+
limit ? backtrace.first(limit) : backtrace
|
|
30
|
+
end
|
|
25
31
|
@sample_type = sample_type
|
|
26
32
|
@context = context
|
|
27
33
|
@status = status
|
|
@@ -67,7 +73,7 @@ module Catpm
|
|
|
67
73
|
end
|
|
68
74
|
|
|
69
75
|
def metadata_bytes
|
|
70
|
-
return 0 if metadata.empty?
|
|
76
|
+
return 0 if metadata.nil? || metadata.empty?
|
|
71
77
|
|
|
72
78
|
metadata.to_json.bytesize + REF_SIZE
|
|
73
79
|
end
|
data/lib/catpm/flusher.rb
CHANGED
|
@@ -225,23 +225,49 @@ module Catpm
|
|
|
225
225
|
|
|
226
226
|
|
|
227
227
|
def rotate_samples(samples)
|
|
228
|
+
return samples if samples.empty?
|
|
229
|
+
|
|
230
|
+
# Pre-fetch counts for all endpoints and types in bulk
|
|
231
|
+
endpoint_keys = samples.map { |s| s[:bucket_key][0..2] }.uniq
|
|
232
|
+
error_fps = samples.filter_map { |s| s[:error_fingerprint] }.uniq
|
|
233
|
+
|
|
234
|
+
# Build counts cache: { [kind, target, op, type] => count }
|
|
235
|
+
counts_cache = {}
|
|
236
|
+
if endpoint_keys.any?
|
|
237
|
+
Catpm::Sample.joins(:bucket)
|
|
238
|
+
.where(catpm_buckets: { kind: endpoint_keys.map(&:first), target: endpoint_keys.map { |k| k[1] }, operation: endpoint_keys.map { |k| k[2] } })
|
|
239
|
+
.where(sample_type: %w[random slow])
|
|
240
|
+
.group('catpm_buckets.kind', 'catpm_buckets.target', 'catpm_buckets.operation', 'catpm_samples.sample_type')
|
|
241
|
+
.count
|
|
242
|
+
.each { |(kind, target, op, type), cnt| counts_cache[[kind, target, op, type]] = cnt }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
error_counts = {}
|
|
246
|
+
if error_fps.any?
|
|
247
|
+
Catpm::Sample.where(sample_type: 'error', error_fingerprint: error_fps)
|
|
248
|
+
.group(:error_fingerprint).count
|
|
249
|
+
.each { |fp, cnt| error_counts[fp] = cnt }
|
|
250
|
+
end
|
|
251
|
+
|
|
228
252
|
samples.each do |sample|
|
|
229
|
-
kind, target, operation = sample[:bucket_key][0
|
|
230
|
-
endpoint_samples = Catpm::Sample
|
|
231
|
-
.joins(:bucket)
|
|
232
|
-
.where(catpm_buckets: { kind: kind, target: target, operation: operation })
|
|
253
|
+
kind, target, operation = sample[:bucket_key][0..2]
|
|
233
254
|
|
|
234
255
|
case sample[:sample_type]
|
|
235
256
|
when 'random'
|
|
236
|
-
|
|
237
|
-
if
|
|
238
|
-
|
|
257
|
+
cache_key = [kind, target, operation, 'random']
|
|
258
|
+
if (counts_cache[cache_key] || 0) >= Catpm.config.max_random_samples_per_endpoint
|
|
259
|
+
oldest = Catpm::Sample.joins(:bucket)
|
|
260
|
+
.where(catpm_buckets: { kind: kind, target: target, operation: operation })
|
|
261
|
+
.where(sample_type: 'random').order(recorded_at: :asc).first
|
|
262
|
+
oldest&.destroy
|
|
239
263
|
end
|
|
240
264
|
when 'slow'
|
|
241
|
-
|
|
242
|
-
if
|
|
243
|
-
weakest =
|
|
244
|
-
|
|
265
|
+
cache_key = [kind, target, operation, 'slow']
|
|
266
|
+
if (counts_cache[cache_key] || 0) >= Catpm.config.max_slow_samples_per_endpoint
|
|
267
|
+
weakest = Catpm::Sample.joins(:bucket)
|
|
268
|
+
.where(catpm_buckets: { kind: kind, target: target, operation: operation })
|
|
269
|
+
.where(sample_type: 'slow').order(duration: :asc).first
|
|
270
|
+
if weakest && sample[:duration] > weakest.duration
|
|
245
271
|
weakest.destroy
|
|
246
272
|
else
|
|
247
273
|
sample[:_skip] = true
|
|
@@ -249,11 +275,10 @@ module Catpm
|
|
|
249
275
|
end
|
|
250
276
|
when 'error'
|
|
251
277
|
fp = sample[:error_fingerprint]
|
|
252
|
-
if fp
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
end
|
|
278
|
+
if fp && (error_counts[fp] || 0) >= Catpm.config.max_error_samples_per_fingerprint
|
|
279
|
+
oldest = Catpm::Sample.where(sample_type: 'error', error_fingerprint: fp)
|
|
280
|
+
.order(recorded_at: :asc).first
|
|
281
|
+
oldest&.destroy
|
|
257
282
|
end
|
|
258
283
|
end
|
|
259
284
|
end
|
|
@@ -267,11 +292,7 @@ module Catpm
|
|
|
267
292
|
occurred_at: event.started_at.iso8601,
|
|
268
293
|
kind: event.kind,
|
|
269
294
|
operation: event_context.slice(:method, :path, :params, :job_class, :job_id, :queue, :target, :metadata),
|
|
270
|
-
backtrace:
|
|
271
|
-
bt = event.backtrace || []
|
|
272
|
-
limit = Catpm.config.backtrace_lines
|
|
273
|
-
limit ? bt.first(limit) : bt
|
|
274
|
-
end,
|
|
295
|
+
backtrace: event.backtrace || [],
|
|
275
296
|
duration: event.duration,
|
|
276
297
|
status: event.status
|
|
277
298
|
}
|
|
@@ -377,60 +398,61 @@ module Catpm
|
|
|
377
398
|
cutoff = age_threshold.ago
|
|
378
399
|
target_seconds = target_interval.to_i
|
|
379
400
|
|
|
380
|
-
#
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
[bucket.kind, bucket.target, bucket.operation, aligned_start]
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
groups.each do |(kind, target, operation, aligned_start), buckets|
|
|
394
|
-
# Skip if only one bucket already at the target alignment
|
|
395
|
-
next if buckets.size == 1 && buckets.first.bucket_start.to_i % target_seconds == 0
|
|
401
|
+
# Process in batches to avoid loading all old buckets into memory
|
|
402
|
+
Catpm::Bucket.where(bucket_start: ...cutoff)
|
|
403
|
+
.select(:id, :kind, :target, :operation, :bucket_start)
|
|
404
|
+
.group_by { |b| [b.kind, b.target, b.operation] }
|
|
405
|
+
.each do |(_kind, _target, _operation), endpoint_buckets|
|
|
406
|
+
groups = endpoint_buckets.group_by do |bucket|
|
|
407
|
+
epoch = bucket.bucket_start.to_i
|
|
408
|
+
aligned_epoch = epoch - (epoch % target_seconds)
|
|
409
|
+
Time.at(aligned_epoch).utc
|
|
410
|
+
end
|
|
396
411
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
412
|
+
groups.each do |aligned_start, stub_buckets|
|
|
413
|
+
next if stub_buckets.size == 1 && stub_buckets.first.bucket_start.to_i % target_seconds == 0
|
|
414
|
+
|
|
415
|
+
# Load full records only for groups that need merging
|
|
416
|
+
bucket_ids = stub_buckets.map(&:id)
|
|
417
|
+
buckets = Catpm::Bucket.where(id: bucket_ids).to_a
|
|
418
|
+
|
|
419
|
+
merged = {
|
|
420
|
+
kind: buckets.first.kind,
|
|
421
|
+
target: buckets.first.target,
|
|
422
|
+
operation: buckets.first.operation,
|
|
423
|
+
bucket_start: aligned_start,
|
|
424
|
+
count: buckets.sum(&:count),
|
|
425
|
+
success_count: buckets.sum(&:success_count),
|
|
426
|
+
failure_count: buckets.sum(&:failure_count),
|
|
427
|
+
duration_sum: buckets.sum(&:duration_sum),
|
|
428
|
+
duration_max: buckets.map(&:duration_max).max,
|
|
429
|
+
duration_min: buckets.map(&:duration_min).min,
|
|
430
|
+
metadata_sum: merge_bucket_metadata(buckets, adapter),
|
|
431
|
+
p95_digest: merge_bucket_digests(buckets)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
survivor = buckets.first
|
|
435
|
+
|
|
436
|
+
# Reassign all samples to the survivor bucket
|
|
437
|
+
Catpm::Sample.where(bucket_id: bucket_ids).update_all(bucket_id: survivor.id)
|
|
438
|
+
|
|
439
|
+
# Delete non-survivor source buckets (now sample-free)
|
|
440
|
+
Catpm::Bucket.where(id: bucket_ids - [survivor.id]).delete_all
|
|
441
|
+
|
|
442
|
+
# Overwrite survivor with merged data
|
|
443
|
+
survivor.update!(
|
|
444
|
+
bucket_start: aligned_start,
|
|
445
|
+
count: merged[:count],
|
|
446
|
+
success_count: merged[:success_count],
|
|
447
|
+
failure_count: merged[:failure_count],
|
|
448
|
+
duration_sum: merged[:duration_sum],
|
|
449
|
+
duration_max: merged[:duration_max],
|
|
450
|
+
duration_min: merged[:duration_min],
|
|
451
|
+
metadata_sum: merged[:metadata_sum],
|
|
452
|
+
p95_digest: merged[:p95_digest]
|
|
453
|
+
)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
434
456
|
end
|
|
435
457
|
|
|
436
458
|
def downsample_event_tier(target_interval:, age_threshold:, adapter:)
|
|
@@ -171,6 +171,8 @@ module Catpm
|
|
|
171
171
|
|
|
172
172
|
duration = event.duration
|
|
173
173
|
sql = payload[:sql].to_s
|
|
174
|
+
max_len = Catpm.config.max_sql_length
|
|
175
|
+
sql = sql.truncate(max_len) if max_len && sql.length > max_len
|
|
174
176
|
source = duration >= Catpm.config.segment_source_threshold ? extract_source_location : nil
|
|
175
177
|
|
|
176
178
|
req_segments.add(
|
data/lib/catpm/stack_sampler.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Catpm
|
|
4
4
|
class StackSampler
|
|
5
|
-
|
|
5
|
+
MS_PER_SECOND = 1000.0
|
|
6
6
|
|
|
7
7
|
# Single global thread that samples all active requests.
|
|
8
8
|
# Avoids creating a thread per request.
|
|
@@ -29,7 +29,7 @@ module Catpm
|
|
|
29
29
|
def start_thread
|
|
30
30
|
@thread = Thread.new do
|
|
31
31
|
loop do
|
|
32
|
-
sleep(
|
|
32
|
+
sleep(Catpm.config.stack_sample_interval)
|
|
33
33
|
sample_all
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -65,6 +65,9 @@ module Catpm
|
|
|
65
65
|
|
|
66
66
|
# Called by SamplingLoop from the global thread
|
|
67
67
|
def capture(now)
|
|
68
|
+
max = Catpm.config.max_stack_samples_per_request
|
|
69
|
+
return if max && @samples.size >= max
|
|
70
|
+
|
|
68
71
|
locs = @target.backtrace_locations
|
|
69
72
|
@samples << [now, locs] if locs
|
|
70
73
|
end
|
|
@@ -117,7 +120,7 @@ module Catpm
|
|
|
117
120
|
duration = estimate_duration(group)
|
|
118
121
|
next if duration < 1.0
|
|
119
122
|
|
|
120
|
-
offset = ((group[:start_time] - @request_start) *
|
|
123
|
+
offset = ((group[:start_time] - @request_start) * MS_PER_SECOND).round(2)
|
|
121
124
|
app_frame = group[:app_frame]
|
|
122
125
|
leaf = group[:leaves].first&.last
|
|
123
126
|
|
|
@@ -199,8 +202,8 @@ module Catpm
|
|
|
199
202
|
|
|
200
203
|
spans.filter_map do |span|
|
|
201
204
|
duration = [
|
|
202
|
-
(span[:end_time] - span[:start_time]) *
|
|
203
|
-
span[:count] *
|
|
205
|
+
(span[:end_time] - span[:start_time]) * MS_PER_SECOND,
|
|
206
|
+
span[:count] * Catpm.config.stack_sample_interval * MS_PER_SECOND
|
|
204
207
|
].max
|
|
205
208
|
next if duration < 1.0
|
|
206
209
|
|
|
@@ -211,7 +214,7 @@ module Catpm
|
|
|
211
214
|
type: classify_path(path),
|
|
212
215
|
detail: build_gem_detail(frame),
|
|
213
216
|
duration: duration.round(2),
|
|
214
|
-
offset: ((span[:start_time] - @request_start) *
|
|
217
|
+
offset: ((span[:start_time] - @request_start) * MS_PER_SECOND).round(2),
|
|
215
218
|
started_at: span[:start_time]
|
|
216
219
|
}
|
|
217
220
|
end
|
|
@@ -219,8 +222,8 @@ module Catpm
|
|
|
219
222
|
|
|
220
223
|
def estimate_duration(group)
|
|
221
224
|
[
|
|
222
|
-
(group[:end_time] - group[:start_time]) *
|
|
223
|
-
group[:count] *
|
|
225
|
+
(group[:end_time] - group[:start_time]) * MS_PER_SECOND,
|
|
226
|
+
group[:count] * Catpm.config.stack_sample_interval * MS_PER_SECOND
|
|
224
227
|
].max
|
|
225
228
|
end
|
|
226
229
|
|
data/lib/catpm/tdigest.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Catpm
|
|
|
12
12
|
Centroid = Struct.new(:mean, :weight)
|
|
13
13
|
|
|
14
14
|
COMPRESSION = 100 # Controls accuracy vs. memory trade-off
|
|
15
|
+
BUFFER_FLUSH_FACTOR = 2 # Lower = more frequent flushes (better accuracy), higher = fewer flushes (better performance)
|
|
15
16
|
|
|
16
17
|
attr_reader :count
|
|
17
18
|
|
|
@@ -22,7 +23,7 @@ module Catpm
|
|
|
22
23
|
@min = Float::INFINITY
|
|
23
24
|
@max = -Float::INFINITY
|
|
24
25
|
@buffer = []
|
|
25
|
-
@buffer_limit = @compression *
|
|
26
|
+
@buffer_limit = @compression * BUFFER_FLUSH_FACTOR
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def add(value, weight = 1)
|
data/lib/catpm/version.rb
CHANGED