spartan_apm 0.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +55 -0
- data/VERSION +1 -0
- data/app/assets/flatpickr-4.6.9/LICENSE.md +21 -0
- data/app/assets/flatpickr-4.6.9/flatpickr.min.css +13 -0
- data/app/assets/flatpickr-4.6.9/flatpickr.min.js +2 -0
- data/app/assets/nice-select2-2.0.0/LICENSE +21 -0
- data/app/assets/nice-select2-2.0.0/nice-select2.min.css +1 -0
- data/app/assets/nice-select2-2.0.0/nice-select2.min.js +1 -0
- data/app/assets/spartan.svg +5 -0
- data/app/views/_help.html.erb +147 -0
- data/app/views/index.html.erb +231 -0
- data/app/views/scripts.js +911 -0
- data/app/views/styles.css +332 -0
- data/config.ru +36 -0
- data/lib/spartan_apm/engine.rb +45 -0
- data/lib/spartan_apm/error_info.rb +17 -0
- data/lib/spartan_apm/instrumentation/active_record.rb +13 -0
- data/lib/spartan_apm/instrumentation/base.rb +36 -0
- data/lib/spartan_apm/instrumentation/bunny.rb +24 -0
- data/lib/spartan_apm/instrumentation/cassandra.rb +13 -0
- data/lib/spartan_apm/instrumentation/curb.rb +13 -0
- data/lib/spartan_apm/instrumentation/dalli.rb +13 -0
- data/lib/spartan_apm/instrumentation/elasticsearch.rb +18 -0
- data/lib/spartan_apm/instrumentation/excon.rb +13 -0
- data/lib/spartan_apm/instrumentation/http.rb +13 -0
- data/lib/spartan_apm/instrumentation/httpclient.rb +13 -0
- data/lib/spartan_apm/instrumentation/net_http.rb +13 -0
- data/lib/spartan_apm/instrumentation/redis.rb +13 -0
- data/lib/spartan_apm/instrumentation/typhoeus.rb +13 -0
- data/lib/spartan_apm/instrumentation.rb +71 -0
- data/lib/spartan_apm/measure.rb +172 -0
- data/lib/spartan_apm/metric.rb +26 -0
- data/lib/spartan_apm/middleware/rack/end_middleware.rb +29 -0
- data/lib/spartan_apm/middleware/rack/start_middleware.rb +57 -0
- data/lib/spartan_apm/middleware/sidekiq/end_middleware.rb +25 -0
- data/lib/spartan_apm/middleware/sidekiq/start_middleware.rb +34 -0
- data/lib/spartan_apm/middleware.rb +16 -0
- data/lib/spartan_apm/persistence.rb +648 -0
- data/lib/spartan_apm/report.rb +436 -0
- data/lib/spartan_apm/string_cache.rb +27 -0
- data/lib/spartan_apm/web/api_request.rb +133 -0
- data/lib/spartan_apm/web/helpers.rb +88 -0
- data/lib/spartan_apm/web/router.rb +90 -0
- data/lib/spartan_apm/web.rb +10 -0
- data/lib/spartan_apm.rb +399 -0
- data/spartan_apm.gemspec +39 -0
- metadata +161 -0
@@ -0,0 +1,436 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpartanAPM
|
4
|
+
# This is the main interface for reading the APM metrics. It will expose
|
5
|
+
# a list of metrics and errors from a given time period. The report can also
|
6
|
+
# be filtered by action and/or host.
|
7
|
+
#
|
8
|
+
# You can use this class to query the APM metrics if you want to construct
|
9
|
+
# your own interface or create custom monitors.
|
10
|
+
#
|
11
|
+
# The metrics in the report are averaged and grouped over a number of minutes. This
|
12
|
+
# number is set in the `interver_minutes` attribute and it increases as the time range
|
13
|
+
# of the report increases. The time range for each interval is referred to as the
|
14
|
+
# interval time.
|
15
|
+
#
|
16
|
+
# Loading the report metrics, errors, and actions will each send a request to Redis
|
17
|
+
# for each minute in the time range. At larger interval times, hourly or daily aggregated
|
18
|
+
# metrics are used and hosts, actions, and errors are not available.
|
19
|
+
#
|
20
|
+
# The end time for the report must be at least one minute in the past since metrics are
|
21
|
+
# queued up before they are persisted to Redis so there will always be at least a one
|
22
|
+
# minute lag.
|
23
|
+
class Report
|
24
|
+
attr_reader :env, :app, :start_time, :end_time, :minutes, :interval_minutes, :host, :action
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def interval_minutes(minutes)
|
28
|
+
if minutes <= 60
|
29
|
+
1
|
30
|
+
elsif minutes <= 2 * 60
|
31
|
+
2
|
32
|
+
elsif minutes <= 4 * 60
|
33
|
+
5
|
34
|
+
elsif minutes <= 12 * 60
|
35
|
+
10
|
36
|
+
elsif minutes <= 14 * 24 * 60
|
37
|
+
60
|
38
|
+
else
|
39
|
+
60 * 24
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param app [String, Symbol] The app name to get metrics for.
|
45
|
+
# @param start_time [Time] The start time for the report (inclusive).
|
46
|
+
# @param end_time [Time] The end time for the report (inclusive).
|
47
|
+
# @param host [String] Optional host name to filter the metrics.
|
48
|
+
# @param action [String] Optional action name to filter the metrics.
|
49
|
+
# @param actions_limit [Integer] Limit on how many actions to pull from Redis.
|
50
|
+
def initialize(app, start_time, end_time, host: nil, action: nil, actions_limit: 100, env: SpartanAPM.env)
|
51
|
+
@app = app.to_s.dup.freeze
|
52
|
+
@env = env.to_s.dup.freeze
|
53
|
+
@start_time = normalize_time(start_time)
|
54
|
+
@end_time = normalize_time(end_time)
|
55
|
+
@end_time = @start_time if @end_time < @start_time
|
56
|
+
|
57
|
+
@minutes = ((@end_time - @start_time) / 60).to_i + 1
|
58
|
+
@interval_minutes = self.class.interval_minutes(@minutes)
|
59
|
+
if !aggregated? && @start_time < Time.now - SpartanAPM.ttl
|
60
|
+
@interval_minutes = 60 unless SpartanAPM.env == "test"
|
61
|
+
end
|
62
|
+
@end_time += (60 * (@minutes % @interval_minutes))
|
63
|
+
|
64
|
+
@host = host&.dup.freeze
|
65
|
+
@action = action&.dup.freeze
|
66
|
+
@actions_limit = [actions_limit, SpartanAPM.max_actions].min
|
67
|
+
@metrics = nil
|
68
|
+
@actions = nil
|
69
|
+
@errors = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns true if using aggregated metrics averaged over an hour or day
|
73
|
+
# @return [Boolean]
|
74
|
+
def aggregated?
|
75
|
+
interval_minutes >= 60
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns true if using hourly aggregated metrics
|
79
|
+
# @return [Boolean]
|
80
|
+
def aggregated_to_hour?
|
81
|
+
interval_minutes == 60
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns true if using daily aggregated metrics
|
85
|
+
# @return [Boolean]
|
86
|
+
def aggregated_to_day?
|
87
|
+
interval_minutes > 60
|
88
|
+
end
|
89
|
+
|
90
|
+
# Iterate over each time segment in the report. Yields the start time
|
91
|
+
# for each segment.
|
92
|
+
def each_time
|
93
|
+
time = start_time
|
94
|
+
while time <= end_time
|
95
|
+
yield time
|
96
|
+
time += 60 * interval_minutes
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Collects a value for each time segment in the report. Yields the start
|
101
|
+
# time for each segment and returns and array of the return values from the block.
|
102
|
+
def collect
|
103
|
+
list = []
|
104
|
+
each_time do |time|
|
105
|
+
list << yield(time)
|
106
|
+
end
|
107
|
+
list
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get a Metric from a given time in the report.
|
111
|
+
# @param time [Time] The time to get the metric for.
|
112
|
+
# @return [SpartanAPM::Metric]
|
113
|
+
def metric(time)
|
114
|
+
load_metrics
|
115
|
+
@metrics[normalize_time(time)] || Metric.new(time)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get the average request time taken in milliseconds for the named component at a time interval.
|
119
|
+
# @param time [Time] The interval time.
|
120
|
+
# @param name [String, Symbol] The component to get the value for.
|
121
|
+
# @return [Integer]
|
122
|
+
def component_request_time(time, name)
|
123
|
+
value = 0.0
|
124
|
+
count = 0
|
125
|
+
each_interval(time) do |t|
|
126
|
+
value += metric(t).component_request_time(name).to_f
|
127
|
+
count += 1
|
128
|
+
end
|
129
|
+
(value / count).round
|
130
|
+
end
|
131
|
+
|
132
|
+
# Get the average number of calls per request for the named component.
|
133
|
+
# @param time [Time] The interval time.
|
134
|
+
# @param name [String, Symbol] The component to get the value for.
|
135
|
+
# @return [Integer]
|
136
|
+
def component_request_count(time, name)
|
137
|
+
value = 0.0
|
138
|
+
count = 0
|
139
|
+
each_interval(time) do |t|
|
140
|
+
value += metric(t).component_request_count(name).to_f
|
141
|
+
count += 1
|
142
|
+
end
|
143
|
+
value / count
|
144
|
+
end
|
145
|
+
|
146
|
+
# Get the average total time taken in milliseconds for a request at a time interval.
|
147
|
+
# @param time [Time] The interval time.
|
148
|
+
# @param measurement [String, Symbol] The measurement to get. This must be one of :avg, :p50, :p90, or :p99.
|
149
|
+
# @return [Integer]
|
150
|
+
def request_time(time, measurement)
|
151
|
+
value = 0.0
|
152
|
+
count = 0
|
153
|
+
each_interval(time) do |t|
|
154
|
+
value += metric(t).send(measurement).to_f
|
155
|
+
count += 1
|
156
|
+
end
|
157
|
+
(value / count).round
|
158
|
+
end
|
159
|
+
|
160
|
+
# Get the average time in milliseconds taken for a component for the report time range.
|
161
|
+
# @param name [String, Symbol] The component name to get.
|
162
|
+
# @return [Integer]
|
163
|
+
def avg_component_time(name)
|
164
|
+
total = 0
|
165
|
+
count = 0
|
166
|
+
each_time do |time|
|
167
|
+
each_interval(time) do |t|
|
168
|
+
value = metric(t).component_request_time(name)
|
169
|
+
if value
|
170
|
+
count += 1
|
171
|
+
total += value.to_f
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
return 0 if count == 0
|
176
|
+
(total / count).round
|
177
|
+
end
|
178
|
+
|
179
|
+
# Get the average time in milliseconds taken for a component for the report time range.
|
180
|
+
# @param name [String, Symbol] The component name to get.
|
181
|
+
# @return [Integer]
|
182
|
+
def avg_component_count(name)
|
183
|
+
total = 0.0
|
184
|
+
count = 0
|
185
|
+
each_time do |time|
|
186
|
+
each_interval(time) do |t|
|
187
|
+
m = metric(t)
|
188
|
+
count += 1
|
189
|
+
total += m.component_request_count(name).to_f
|
190
|
+
end
|
191
|
+
end
|
192
|
+
return 0 if count == 0
|
193
|
+
total / count
|
194
|
+
end
|
195
|
+
|
196
|
+
# Get the average time in milliseconds taken for a measurement for the report time range.
|
197
|
+
# @param measurement [String, Symbol] The measurement to get. This must be one of :avg, :p50, :p90, or :p99.
|
198
|
+
# @return [Integer]
|
199
|
+
def avg_request_time(measurement)
|
200
|
+
total = 0
|
201
|
+
count = 0
|
202
|
+
each_time do |time|
|
203
|
+
each_interval(time) do |t|
|
204
|
+
value = metric(t)&.send(measurement)
|
205
|
+
if value
|
206
|
+
count += 1
|
207
|
+
total += value.to_f
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
return 0 if count == 0
|
212
|
+
(total / count).round
|
213
|
+
end
|
214
|
+
|
215
|
+
# Get the total number of requests for a time interval.
|
216
|
+
# @param time [Time] The interval time.
|
217
|
+
# @return [Integer]
|
218
|
+
def request_count(time)
|
219
|
+
count = 0
|
220
|
+
each_interval(time) do |t|
|
221
|
+
count += metric(t)&.count.to_i
|
222
|
+
end
|
223
|
+
count
|
224
|
+
end
|
225
|
+
|
226
|
+
# Get the average number of requests per minute for a time interval.
|
227
|
+
# @param time [Time] The interval time.
|
228
|
+
# @return [Integer]
|
229
|
+
def requests_per_minute(time)
|
230
|
+
(request_count(time).to_f / interval_minutes).round
|
231
|
+
end
|
232
|
+
|
233
|
+
# Get the average requests per minute for the report time range.
|
234
|
+
# @return [Integer]
|
235
|
+
def avg_requests_per_minute
|
236
|
+
total = 0.0
|
237
|
+
count = 0
|
238
|
+
each_time do |time|
|
239
|
+
count += 1
|
240
|
+
each_interval(time) do |t|
|
241
|
+
total += metric(t)&.count.to_f
|
242
|
+
end
|
243
|
+
end
|
244
|
+
if count > 0
|
245
|
+
((total / count) / interval_minutes).round
|
246
|
+
else
|
247
|
+
0
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Get the number of errors reported for a time interval.
|
252
|
+
# @param time [Time] The interval time.
|
253
|
+
# @return [Integer]
|
254
|
+
def error_count(time)
|
255
|
+
total = 0
|
256
|
+
each_interval(time) do |t|
|
257
|
+
total += metric(t)&.error_count.to_i
|
258
|
+
end
|
259
|
+
total
|
260
|
+
end
|
261
|
+
|
262
|
+
# Get the average number of errors per minute for the report time range.
|
263
|
+
# @return [Integer]
|
264
|
+
def avg_errors_per_minute
|
265
|
+
total = 0.0
|
266
|
+
count = 0
|
267
|
+
each_time do |time|
|
268
|
+
each_interval(time) do |t|
|
269
|
+
error_count = metric(t)&.error_count
|
270
|
+
if error_count
|
271
|
+
count += 1
|
272
|
+
total += error_count
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
if count > 0
|
277
|
+
(total.to_f / count).round(2)
|
278
|
+
else
|
279
|
+
0.0
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Get the average of error rate (errors per request) for a time interval.
|
284
|
+
# @param time [Time] The interval time.
|
285
|
+
# @return [Float]
|
286
|
+
def error_rate(time)
|
287
|
+
count = request_count(time)
|
288
|
+
if count > 0
|
289
|
+
error_count(time).to_f / count.to_f
|
290
|
+
else
|
291
|
+
0.0
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Get the average error rate (errors per request) for the report time range.
|
296
|
+
# @return [Float]
|
297
|
+
def avg_error_rate
|
298
|
+
total_rate = 0.0
|
299
|
+
count = 0
|
300
|
+
each_time do |time|
|
301
|
+
count += 1
|
302
|
+
total_rate += error_rate(time)
|
303
|
+
end
|
304
|
+
if count > 0
|
305
|
+
total_rate / count
|
306
|
+
else
|
307
|
+
0.0
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Get the list of errors reported during the report time range.
|
312
|
+
# @return [Array<SpartanAPM::ErrorInfo]
|
313
|
+
def errors
|
314
|
+
load_errors
|
315
|
+
all_errors = {}
|
316
|
+
@errors.values.each do |time_errors|
|
317
|
+
time_errors.each do |error_info|
|
318
|
+
key = [error_info.class_name, error_info.backtrace]
|
319
|
+
err = all_errors[key]
|
320
|
+
unless err
|
321
|
+
err = ErrorInfo.new(nil, error_info.class_name, error_info.message, error_info.backtrace, 0)
|
322
|
+
all_errors[key] = err
|
323
|
+
end
|
324
|
+
err.count += error_info.count
|
325
|
+
end
|
326
|
+
end
|
327
|
+
all_errors.values.sort_by { |error_info| -error_info.count }
|
328
|
+
end
|
329
|
+
|
330
|
+
# Get the list of action names reported during the report time range. The returned
|
331
|
+
# value is limited by the `actions_limit` argument passed in the constructor. The
|
332
|
+
# list will be sorted by the amount of time spent in each action with the most
|
333
|
+
# heavily used actions coming first.
|
334
|
+
# @return [Array<String>] List of action names.
|
335
|
+
def actions
|
336
|
+
load_actions
|
337
|
+
@actions.keys
|
338
|
+
end
|
339
|
+
|
340
|
+
# The the percent of time spent in the specified action.
|
341
|
+
# @return [Float]
|
342
|
+
def action_percent_time(action)
|
343
|
+
load_actions
|
344
|
+
@actions[action]
|
345
|
+
end
|
346
|
+
|
347
|
+
# Get the list of host names that reported metrics during the report time range.
|
348
|
+
# @return [Array<String>]
|
349
|
+
def hosts
|
350
|
+
load_metrics
|
351
|
+
@hosts.sort
|
352
|
+
end
|
353
|
+
|
354
|
+
# Get the list of component names that reported metrics during the report time range.
|
355
|
+
# @return [Array<String>]
|
356
|
+
def component_names
|
357
|
+
load_metrics
|
358
|
+
@names.sort
|
359
|
+
end
|
360
|
+
|
361
|
+
private
|
362
|
+
|
363
|
+
# Lazily load the metric data.
|
364
|
+
def load_metrics
|
365
|
+
return if @metrics
|
366
|
+
metrics_map = {}
|
367
|
+
names = Set.new
|
368
|
+
metrics = nil
|
369
|
+
hosts = []
|
370
|
+
persistence = Persistence.new(app, env: env)
|
371
|
+
if interval_minutes >= 60 * 24
|
372
|
+
metrics = persistence.daily_metrics([start_time, end_time])
|
373
|
+
elsif interval_minutes >= 60
|
374
|
+
metrics = persistence.hourly_metrics([start_time, end_time])
|
375
|
+
else
|
376
|
+
metrics, hosts = persistence.report_info([start_time, end_time], host: host, action: action)
|
377
|
+
end
|
378
|
+
metrics.each do |metric|
|
379
|
+
metric.component_names.each { |n| names << n }
|
380
|
+
metrics_map[metric.time] = metric
|
381
|
+
end
|
382
|
+
@names = names.to_a.freeze
|
383
|
+
@hosts = hosts.sort.freeze
|
384
|
+
@metrics = metrics_map
|
385
|
+
end
|
386
|
+
|
387
|
+
# Lazily load the action data.
|
388
|
+
def load_actions
|
389
|
+
return if @actions
|
390
|
+
actions = {}
|
391
|
+
unless aggregated?
|
392
|
+
Persistence.new(app, env: env).actions([start_time, end_time], interval: interval_minutes, limit: @actions_limit).each do |action, load_val|
|
393
|
+
actions[action] = load_val
|
394
|
+
end
|
395
|
+
end
|
396
|
+
@actions = actions
|
397
|
+
end
|
398
|
+
|
399
|
+
# Lazily load the error data.
|
400
|
+
def load_errors
|
401
|
+
return if @errors
|
402
|
+
errors = {}
|
403
|
+
unless aggregated?
|
404
|
+
Persistence.new(app, env: env).errors([start_time, end_time]).each do |error|
|
405
|
+
time_errors = errors[error.time]
|
406
|
+
unless time_errors
|
407
|
+
time_errors = []
|
408
|
+
errors[error.time] = time_errors
|
409
|
+
end
|
410
|
+
time_errors << error
|
411
|
+
end
|
412
|
+
end
|
413
|
+
@errors = errors
|
414
|
+
end
|
415
|
+
|
416
|
+
def normalize_time(time)
|
417
|
+
SpartanAPM.bucket_time(SpartanAPM.bucket(time)).freeze
|
418
|
+
end
|
419
|
+
|
420
|
+
def each_interval(time)
|
421
|
+
time = normalize_time(time)
|
422
|
+
if aggregated?
|
423
|
+
time = if aggregated_to_hour?
|
424
|
+
SpartanAPM::Persistence.truncate_to_hour(time)
|
425
|
+
else
|
426
|
+
SpartanAPM::Persistence.truncate_to_date(time)
|
427
|
+
end
|
428
|
+
yield(time)
|
429
|
+
else
|
430
|
+
interval_minutes.times do |interval|
|
431
|
+
yield(time + (interval * 60))
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpartanAPM
|
4
|
+
# Simple string cache. This is used to prevent memory bloat
|
5
|
+
# when collecting measures by caching and reusing strings
|
6
|
+
# in the enqueued metrics rather than having the same string
|
7
|
+
# repeated for each request.
|
8
|
+
class StringCache
|
9
|
+
def initialize
|
10
|
+
@cache = Concurrent::Hash.new
|
11
|
+
end
|
12
|
+
|
13
|
+
# Fetch a string from the cache. If it isn't already there,
|
14
|
+
# then it will be frozen and stored in the cache so the same
|
15
|
+
# object can be returned by subsequent calls for a matching string.
|
16
|
+
def fetch(value)
|
17
|
+
return nil if value.nil?
|
18
|
+
value = value.to_s
|
19
|
+
cached = @cache[value]
|
20
|
+
unless cached
|
21
|
+
cached = -value
|
22
|
+
@cache[cached] = cached
|
23
|
+
end
|
24
|
+
cached
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpartanAPM
|
4
|
+
module Web
|
5
|
+
# Wrapper for API requests.
|
6
|
+
class ApiRequest
|
7
|
+
attr_reader :request
|
8
|
+
|
9
|
+
def initialize(request)
|
10
|
+
@request = request
|
11
|
+
end
|
12
|
+
|
13
|
+
# Response for /metrics
|
14
|
+
def metrics
|
15
|
+
report = create_report
|
16
|
+
component_data = {}
|
17
|
+
report.component_names.each do |name|
|
18
|
+
component_data[name] = {
|
19
|
+
time: report.collect { |time| report.component_request_time(time, name) },
|
20
|
+
count: report.collect { |time| report.component_request_count(time, name).round(1) }
|
21
|
+
}
|
22
|
+
end
|
23
|
+
{
|
24
|
+
env: report.env,
|
25
|
+
app: report.app,
|
26
|
+
host: report.host,
|
27
|
+
action: report.action,
|
28
|
+
hosts: report.hosts,
|
29
|
+
actions: report.actions,
|
30
|
+
minutes: report.minutes,
|
31
|
+
interval_minutes: report.interval_minutes,
|
32
|
+
times: report.collect { |time| time.iso8601 },
|
33
|
+
avg: {
|
34
|
+
avg: report.avg_request_time(:avg),
|
35
|
+
data: component_data
|
36
|
+
},
|
37
|
+
p50: {
|
38
|
+
avg: report.avg_request_time(:p50),
|
39
|
+
data: report.collect { |time| report.request_time(time, :p50) }
|
40
|
+
},
|
41
|
+
p90: {
|
42
|
+
avg: report.avg_request_time(:p90),
|
43
|
+
data: report.collect { |time| report.request_time(time, :p90) }
|
44
|
+
},
|
45
|
+
p99: {
|
46
|
+
avg: report.avg_request_time(:p99),
|
47
|
+
data: report.collect { |time| report.request_time(time, :p99) }
|
48
|
+
},
|
49
|
+
throughput: {
|
50
|
+
avg: report.avg_requests_per_minute,
|
51
|
+
data: report.collect { |time| report.requests_per_minute(time) }
|
52
|
+
},
|
53
|
+
errors: {
|
54
|
+
avg: report.avg_errors_per_minute,
|
55
|
+
data: report.collect { |time| report.error_count(time) }
|
56
|
+
},
|
57
|
+
error_rate: {
|
58
|
+
avg: report.avg_error_rate,
|
59
|
+
data: report.collect { |time| report.error_rate(time) }
|
60
|
+
}
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Response for /live_metrics
|
65
|
+
def live_metrics
|
66
|
+
begin
|
67
|
+
current_bucket = SpartanAPM.bucket(Time.now - 65)
|
68
|
+
last_bucket = SpartanAPM.bucket(Time.parse(param(:live_time)))
|
69
|
+
return({}) if current_bucket <= last_bucket
|
70
|
+
rescue
|
71
|
+
return({})
|
72
|
+
end
|
73
|
+
metrics
|
74
|
+
end
|
75
|
+
|
76
|
+
# Response for /errors
|
77
|
+
def errors
|
78
|
+
report = create_report
|
79
|
+
{
|
80
|
+
errors: report.errors.collect { |error| {class_name: error.class_name, message: error.message, count: error.count, backtrace: error.backtrace} }
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
# Response for /actions
|
85
|
+
def actions
|
86
|
+
report = create_report
|
87
|
+
{
|
88
|
+
actions: report.actions.collect { |action| {name: action, load: report.action_percent_time(action)} }
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def param(name, default: nil)
|
95
|
+
value = request.params[name.to_s]
|
96
|
+
if value.nil? || value == ""
|
97
|
+
value = default&.to_s
|
98
|
+
end
|
99
|
+
value
|
100
|
+
end
|
101
|
+
|
102
|
+
def create_report
|
103
|
+
env = (param(:env) || SpartanAPM.env)
|
104
|
+
env = SpartanAPM.env unless SpartanAPM.environments.include?(env)
|
105
|
+
app = param(:app)
|
106
|
+
action = param(:action)
|
107
|
+
host = param(:host)
|
108
|
+
minutes = param(:minutes).to_i
|
109
|
+
minutes = 30 if minutes <= 0
|
110
|
+
minutes = 60 * 24 * 365 if minutes > 60 * 24 * 365
|
111
|
+
start_time = nil
|
112
|
+
time = param(:time)
|
113
|
+
if time
|
114
|
+
begin
|
115
|
+
start_time = Time.parse(time)
|
116
|
+
rescue
|
117
|
+
# Use default
|
118
|
+
end
|
119
|
+
end
|
120
|
+
if start_time.nil?
|
121
|
+
interval_minutes = Report.interval_minutes(minutes)
|
122
|
+
start_time = if interval_minutes < 60
|
123
|
+
Time.now - (minutes * 60)
|
124
|
+
else
|
125
|
+
Time.now - ((interval_minutes * 60) + (minutes * 60))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end_time = start_time + ((minutes - 1) * 60)
|
129
|
+
Report.new(app, start_time, end_time, host: host, action: action, env: env)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpartanAPM
|
4
|
+
module Web
|
5
|
+
# Helper classes for the web UI.
|
6
|
+
class Helpers
|
7
|
+
VIEWS_DIR = File.expand_path(File.join("..", "..", "..", "app", "views"), __dir__).freeze
|
8
|
+
|
9
|
+
@mutex = Mutex.new
|
10
|
+
@templates = {}
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# @api private
|
14
|
+
# ERB template cache.
|
15
|
+
def template(path)
|
16
|
+
template = @templates[path]
|
17
|
+
unless template
|
18
|
+
template = File.read(path)
|
19
|
+
if path.end_with?(".erb")
|
20
|
+
template = ERB.new(File.read(path))
|
21
|
+
end
|
22
|
+
@mutex.synchronize { @templates[path] = template } unless ENV["DEVELOPMENT"] == "true"
|
23
|
+
end
|
24
|
+
template
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :request
|
29
|
+
|
30
|
+
def initialize(request)
|
31
|
+
@request = request
|
32
|
+
end
|
33
|
+
|
34
|
+
# Render an ERB template.
|
35
|
+
# @param path [String] Relative path to the template in the gem app/views directory.
|
36
|
+
# @variables [Hash] Local variables to set for the ERB binding.
|
37
|
+
def render(path, variables = {})
|
38
|
+
file_path = File.expand_path(File.join(VIEWS_DIR, *path.split("/")))
|
39
|
+
raise ArgumentError.new("Invalid template ") unless file_path.start_with?(VIEWS_DIR)
|
40
|
+
template = self.class.template(file_path)
|
41
|
+
if template.is_a?(ERB)
|
42
|
+
template.result(get_binding(variables))
|
43
|
+
else
|
44
|
+
template
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# HTML escape text.
|
49
|
+
def h(text)
|
50
|
+
ERB::Util.h(text.to_s)
|
51
|
+
end
|
52
|
+
|
53
|
+
# List of available apps.
|
54
|
+
def apps
|
55
|
+
SpartanAPM.apps
|
56
|
+
end
|
57
|
+
|
58
|
+
# List of available apps.
|
59
|
+
def environments
|
60
|
+
SpartanAPM.environments
|
61
|
+
end
|
62
|
+
|
63
|
+
# Optional URL for authenticating access to the web UI.
|
64
|
+
def authentication_url
|
65
|
+
SpartanAPM.authentication_url
|
66
|
+
end
|
67
|
+
|
68
|
+
# Optional application name to show in the web UI.
|
69
|
+
def application_name
|
70
|
+
SpartanAPM.application_name || "SpartanAPM"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Optional link URL back to the application for the web UI.
|
74
|
+
def application_url
|
75
|
+
SpartanAPM.application_url
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def get_binding(local_variables = {})
|
81
|
+
local_variables.each do |name, value|
|
82
|
+
binding.local_variable_set(name, value)
|
83
|
+
end
|
84
|
+
binding
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|