spartan_apm 0.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +55 -0
  5. data/VERSION +1 -0
  6. data/app/assets/flatpickr-4.6.9/LICENSE.md +21 -0
  7. data/app/assets/flatpickr-4.6.9/flatpickr.min.css +13 -0
  8. data/app/assets/flatpickr-4.6.9/flatpickr.min.js +2 -0
  9. data/app/assets/nice-select2-2.0.0/LICENSE +21 -0
  10. data/app/assets/nice-select2-2.0.0/nice-select2.min.css +1 -0
  11. data/app/assets/nice-select2-2.0.0/nice-select2.min.js +1 -0
  12. data/app/assets/spartan.svg +5 -0
  13. data/app/views/_help.html.erb +147 -0
  14. data/app/views/index.html.erb +231 -0
  15. data/app/views/scripts.js +911 -0
  16. data/app/views/styles.css +332 -0
  17. data/config.ru +36 -0
  18. data/lib/spartan_apm/engine.rb +45 -0
  19. data/lib/spartan_apm/error_info.rb +17 -0
  20. data/lib/spartan_apm/instrumentation/active_record.rb +13 -0
  21. data/lib/spartan_apm/instrumentation/base.rb +36 -0
  22. data/lib/spartan_apm/instrumentation/bunny.rb +24 -0
  23. data/lib/spartan_apm/instrumentation/cassandra.rb +13 -0
  24. data/lib/spartan_apm/instrumentation/curb.rb +13 -0
  25. data/lib/spartan_apm/instrumentation/dalli.rb +13 -0
  26. data/lib/spartan_apm/instrumentation/elasticsearch.rb +18 -0
  27. data/lib/spartan_apm/instrumentation/excon.rb +13 -0
  28. data/lib/spartan_apm/instrumentation/http.rb +13 -0
  29. data/lib/spartan_apm/instrumentation/httpclient.rb +13 -0
  30. data/lib/spartan_apm/instrumentation/net_http.rb +13 -0
  31. data/lib/spartan_apm/instrumentation/redis.rb +13 -0
  32. data/lib/spartan_apm/instrumentation/typhoeus.rb +13 -0
  33. data/lib/spartan_apm/instrumentation.rb +71 -0
  34. data/lib/spartan_apm/measure.rb +172 -0
  35. data/lib/spartan_apm/metric.rb +26 -0
  36. data/lib/spartan_apm/middleware/rack/end_middleware.rb +29 -0
  37. data/lib/spartan_apm/middleware/rack/start_middleware.rb +57 -0
  38. data/lib/spartan_apm/middleware/sidekiq/end_middleware.rb +25 -0
  39. data/lib/spartan_apm/middleware/sidekiq/start_middleware.rb +34 -0
  40. data/lib/spartan_apm/middleware.rb +16 -0
  41. data/lib/spartan_apm/persistence.rb +648 -0
  42. data/lib/spartan_apm/report.rb +436 -0
  43. data/lib/spartan_apm/string_cache.rb +27 -0
  44. data/lib/spartan_apm/web/api_request.rb +133 -0
  45. data/lib/spartan_apm/web/helpers.rb +88 -0
  46. data/lib/spartan_apm/web/router.rb +90 -0
  47. data/lib/spartan_apm/web.rb +10 -0
  48. data/lib/spartan_apm.rb +399 -0
  49. data/spartan_apm.gemspec +39 -0
  50. metadata +161 -0
@@ -0,0 +1,648 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpartanAPM
4
+ class Persistence
5
+ STORE_STATS_SCRIPT = <<~LUA
6
+ local key = KEYS[1]
7
+ local actions_key = KEYS[2]
8
+
9
+ local action = ARGV[1]
10
+ local host = ARGV[2]
11
+ local data = cjson.decode(ARGV[3])
12
+ local total_time = tonumber(ARGV[4])
13
+ local ttl = tonumber(ARGV[5])
14
+
15
+ local host_stats = nil
16
+ local host_stats_data = redis.call('hget', key, host)
17
+ if host_stats_data then
18
+ host_stats = cmsgpack.unpack(host_stats_data)
19
+ else
20
+ host_stats = {}
21
+ end
22
+ for name, values in pairs(data) do
23
+ local name_stats = host_stats[name]
24
+ if name_stats == nil then
25
+ name_stats = {}
26
+ host_stats[name] = name_stats
27
+ end
28
+ name_stats[#name_stats + 1] = values
29
+ end
30
+ redis.call('hset', key, host, cmsgpack.pack(host_stats))
31
+ redis.call('zincrby', actions_key, total_time, action)
32
+ redis.call('expire', key, ttl)
33
+ redis.call('expire', actions_key, ttl)
34
+ LUA
35
+
36
+ MAX_KEEP_AGGREGATED_STATS = 365 * 24 * 60 * 60
37
+
38
+ class << self
39
+ def store!(bucket, measures)
40
+ store_measure_stats(bucket, measures)
41
+ store_measure_errors(bucket, measures)
42
+ measures.collect(&:app).uniq.each do |app|
43
+ store_hour_stats(app, bucket)
44
+ store_day_stats(app, bucket)
45
+ end
46
+ end
47
+
48
+ # Truncate a time to the hour (UTC time).
49
+ # @param time [Time]
50
+ # @return [Time]
51
+ def truncate_to_hour(time)
52
+ time = Time.at(time.to_f).utc
53
+ Time.utc(time.year, time.month, time.day, time.hour)
54
+ end
55
+
56
+ # Truncate a time to the date (UTC time).
57
+ # @param time [Time]
58
+ # @return [Time]
59
+ def truncate_to_date(time)
60
+ time = Time.at(time.to_f).utc
61
+ Time.utc(time.year, time.month, time.day)
62
+ end
63
+
64
+ private
65
+
66
+ def redis_key(env, app, action, bucket)
67
+ app = app.to_s.tr("~", "-")
68
+ action = action.to_s
69
+ action = "~#{action}" unless action.empty?
70
+ "SpartanAPM:metrics:#{env}:#{bucket}:#{app}#{action}"
71
+ end
72
+
73
+ def actions_key(env, app, bucket)
74
+ "SpartanAPM:actions:#{env}:#{bucket}:#{app}"
75
+ end
76
+
77
+ def errors_key(env, app, bucket)
78
+ "SpartanAPM:errors:#{env}:#{bucket}:#{app}"
79
+ end
80
+
81
+ def hours_key(env, app)
82
+ "SpartanAPM:hours:#{env}:#{app}"
83
+ end
84
+
85
+ def days_key(env, app)
86
+ "SpartanAPM:days:#{env}:#{app}"
87
+ end
88
+
89
+ def hours_semaphore(env, app, hour)
90
+ "SpartanAPM:hours_semaphore:#{env}:#{hour}:#{app}"
91
+ end
92
+
93
+ def days_semaphore(env, app, day)
94
+ "SpartanAPM:days_semaphore:#{env}:#{day}:#{app}"
95
+ end
96
+
97
+ def store_measure_stats(bucket, measures)
98
+ start_time = Time.now
99
+ action_values = {}
100
+ measures.each do |measure|
101
+ next if measure.timers.empty?
102
+ aggregate_values(action_values, [measure.app, measure.action], measure)
103
+ if measure.action
104
+ aggregate_values(action_values, [measure.app, nil], measure)
105
+ end
106
+ end
107
+
108
+ action_values.each do |action_key, action_measures|
109
+ app, action = action_key
110
+ data = aggregate_stats(action_measures)
111
+ store_metric(bucket, app, action, SpartanAPM.host, data)
112
+ end
113
+
114
+ SpartanAPM.logger&.info("SpartanAPM stored #{action_values.size} stats in #{((Time.now - start_time) * 1000).round}ms")
115
+ end
116
+
117
+ def aggregate_values(map, key, value)
118
+ values = map[key]
119
+ unless values
120
+ values = []
121
+ map[key] = values
122
+ end
123
+ values << value
124
+ end
125
+
126
+ def aggregate_stats(measures)
127
+ times = {}
128
+ counts = Hash.new(0)
129
+ total_times = []
130
+ error_count = 0
131
+ measures.each do |measure|
132
+ total_times << measure.timers.values.sum
133
+ error_count += 1 if measure.error
134
+ measure.timers.each do |name, value|
135
+ aggregate_values(times, name, value)
136
+ end
137
+ measure.counts.each do |name, value|
138
+ counts[name] += value.to_f
139
+ end
140
+ end
141
+
142
+ stats = {}
143
+ times.each do |name, values|
144
+ elapsed_time = (values.sum * 1000).round
145
+ stats[name.to_s] = [measures.size, elapsed_time, counts[name]]
146
+ end
147
+
148
+ count = (measures.size.to_f / SpartanAPM.sample_rate).round
149
+ total_times.sort!
150
+ time = ((total_times.sum * 1000).round / SpartanAPM.sample_rate).round
151
+ p50 = (total_times[(total_times.size * 0.5).floor] * 1000).round
152
+ p90 = (total_times[(total_times.size * 0.90).floor] * 1000).round
153
+ p99 = (total_times[(total_times.size * 0.99).floor] * 1000).round
154
+ errors = (error_count / SpartanAPM.sample_rate).round
155
+ stats["."] = [count, time, p50, p90, p99, errors]
156
+
157
+ stats
158
+ end
159
+
160
+ def store_metric(bucket, app, action, host, data)
161
+ script_keys = [redis_key(SpartanAPM.env, app, action, bucket), actions_key(SpartanAPM.env, app, bucket)]
162
+ script_args = [action, host, JSON.dump(data), data["."][1], SpartanAPM.ttl]
163
+ eval_script(STORE_STATS_SCRIPT, script_keys, script_args)
164
+ end
165
+
166
+ def store_measure_errors(bucket, measures)
167
+ start_time = Time.now
168
+ errors = {}
169
+ measures.each do |measure|
170
+ next unless measure.error
171
+
172
+ error_key = Digest::MD5.hexdigest("#{measure.error} #{measure.error_backtrace&.join}")
173
+ app_errors = errors[measure.app]
174
+ unless app_errors
175
+ app_errors = {}
176
+ errors[measure.app] = app_errors
177
+ end
178
+ error_info = app_errors[error_key]
179
+ unless error_info
180
+ error_info = [measure.error, measure.error_message, measure.error_backtrace, 0]
181
+ app_errors[error_key] = error_info
182
+ end
183
+ error_info[3] += 1
184
+ end
185
+
186
+ errors.each do |app, app_errors|
187
+ app_errors.each do |error_key, info|
188
+ class_name, message, backtrace, count = info
189
+ store_error(bucket, app, class_name, message, backtrace, count)
190
+ end
191
+ end
192
+
193
+ SpartanAPM.logger&.info("SpartanAPM stored #{errors.size} errors in #{((Time.now - start_time) * 1000).round}ms")
194
+ end
195
+
196
+ def store_error(bucket, app, class_name, message, backtrace, count)
197
+ key = errors_key(SpartanAPM.env, app, bucket)
198
+ payload = deflate(MessagePack.dump([class_name, message, backtrace]))
199
+ SpartanAPM.redis.multi do |transaction|
200
+ transaction.zincrby(key, count.round, payload)
201
+ transaction.expire(key, SpartanAPM.ttl)
202
+ end
203
+ end
204
+
205
+ def store_hour_stats(app, bucket)
206
+ redis = SpartanAPM.redis
207
+ hour = truncate_to_hour(SpartanAPM.bucket_time(bucket - 60)).to_i
208
+ hour_exists = redis.zrangebyscore(hours_key(SpartanAPM.env, app), hour, hour).first
209
+ return if hour_exists
210
+
211
+ semaphore = hours_semaphore(SpartanAPM.env, app, hour)
212
+ locked, _ = redis.multi do |transaction|
213
+ transaction.setnx(semaphore, "1")
214
+ transaction.expire(semaphore, 60)
215
+ end
216
+ return unless locked
217
+
218
+ begin
219
+ stats = hour_stats(app, hour)
220
+ unless stats.empty?
221
+ add_hourly_stats(app, stats)
222
+ base_bucket = SpartanAPM.bucket(Time.at(hour))
223
+ 60.times do |minute|
224
+ truncate_actions!(app, base_bucket + minute)
225
+ end
226
+ end
227
+ ensure
228
+ redis.del(semaphore)
229
+ end
230
+ end
231
+
232
+ def store_day_stats(app, bucket)
233
+ redis = SpartanAPM.redis
234
+ day = truncate_to_date(SpartanAPM.bucket_time(bucket - (60 * 24))).to_i
235
+ day_exists = redis.zrangebyscore(days_key(SpartanAPM.env, app), day, day).first
236
+ return if day_exists
237
+
238
+ semaphore = days_semaphore(SpartanAPM.env, app, day)
239
+ locked, _ = redis.multi do |transaction|
240
+ transaction.setnx(semaphore, "1")
241
+ transaction.expire(semaphore, 60)
242
+ end
243
+ return unless locked
244
+
245
+ begin
246
+ stats = day_stats(app, day)
247
+ add_daily_stats(app, stats) unless stats.empty?
248
+ ensure
249
+ redis.del(semaphore)
250
+ end
251
+ end
252
+
253
+ def hour_stats(app, hour)
254
+ report = Report.new(app, Time.at(hour), Time.at(hour + (59 * 60)))
255
+ error_count = 0
256
+ count = 0
257
+ report.each_time do |time|
258
+ error_count += report.error_count(time)
259
+ count += report.request_count(time)
260
+ end
261
+ components = {}
262
+ report.component_names.each do |name|
263
+ components[name] = [report.avg_component_time(name), report.avg_component_count(name)]
264
+ end
265
+ {
266
+ hour: hour,
267
+ components: components,
268
+ avg: report.avg_request_time(:avg),
269
+ p50: report.avg_request_time(:p50),
270
+ p90: report.avg_request_time(:p90),
271
+ p99: report.avg_request_time(:p99),
272
+ error_count: error_count,
273
+ count: count
274
+ }
275
+ end
276
+
277
+ def day_stats(app, day)
278
+ last_hour = day + (60 * 60 * 23)
279
+ hour_stats = SpartanAPM.redis.zrangebyscore(hours_key(SpartanAPM.env, app), day, last_hour).collect { |data| MessagePack.load(data) }
280
+ return [] if hour_stats.empty?
281
+
282
+ if hour_stats.last["hour"] != last_hour
283
+ next_stat = SpartanAPM.redis.zrevrange(hours_key(SpartanAPM.env, app), 0, 1).first
284
+ if next_stat && MessagePack.load(next_stat)["hour"] < last_hour
285
+ return []
286
+ end
287
+ end
288
+
289
+ components = {}
290
+ avgs = []
291
+ p50s = []
292
+ p90s = []
293
+ p99s = []
294
+ count = 0
295
+ error_count = 0
296
+ hour_stats.each do |stat|
297
+ request_count = stat["count"]
298
+ next if request_count.nil?
299
+ stat["components"].each do |name, values|
300
+ timing, avg_count = values
301
+ component_time, component_count = components[name]
302
+ unless component_time
303
+ component_time = 0
304
+ component_count = 0.0
305
+ end
306
+ component_time += timing.to_f * request_count
307
+ component_count += avg_count.to_f
308
+ components[name] = [component_time, component_count]
309
+ end
310
+ avgs << stat["avg"] * request_count
311
+ p50s << stat["p50"] * request_count
312
+ p90s << stat["p90"] * request_count
313
+ p99s << stat["p99"] * request_count
314
+ count += request_count
315
+ error_count += stat["error_count"]
316
+ end
317
+ aggregated_components = {}
318
+ components.each do |name, values|
319
+ weighted_timing, weighted_avg_count = values
320
+ aggregated_components[name] = if count > 0
321
+ [(weighted_timing / count).round, weighted_avg_count / hour_stats.size]
322
+ else
323
+ [0, 0.0]
324
+ end
325
+ end
326
+ {
327
+ day: day,
328
+ components: aggregated_components,
329
+ avg: (count > 0 ? (avgs.sum.to_f / count).round : 0),
330
+ p50: (count > 0 ? (p50s.sum.to_f / count).round : 0),
331
+ p90: (count > 0 ? (p90s.sum.to_f / count).round : 0),
332
+ p99: (count > 0 ? (p99s.sum.to_f / count).round : 0),
333
+ error_count: error_count,
334
+ count: count
335
+ }
336
+ end
337
+
338
+ def add_hourly_stats(app, stats)
339
+ key = hours_key(SpartanAPM.env, app)
340
+ hour = (stats["hour"] || stats[:hour])
341
+ SpartanAPM.redis.multi do |transaction|
342
+ transaction.zremrangebyscore(key, hour, hour)
343
+ transaction.zadd(key, hour, MessagePack.dump(stats))
344
+ transaction.zremrangebyscore(key, "-inf", hour - MAX_KEEP_AGGREGATED_STATS)
345
+ transaction.expire(key, MAX_KEEP_AGGREGATED_STATS)
346
+ end
347
+ end
348
+
349
+ def add_daily_stats(app, stats)
350
+ key = days_key(SpartanAPM.env, app)
351
+ day = (stats["day"] || stats[:day])
352
+ SpartanAPM.redis.multi do |transaction|
353
+ transaction.zremrangebyscore(key, day, day)
354
+ transaction.zadd(key, day, MessagePack.dump(stats))
355
+ transaction.zremrangebyscore(key, "-inf", day - MAX_KEEP_AGGREGATED_STATS)
356
+ transaction.expire(key, MAX_KEEP_AGGREGATED_STATS)
357
+ end
358
+ end
359
+
360
+ def truncate_actions!(app, bucket)
361
+ action_key = actions_key(SpartanAPM.env, app, bucket)
362
+ min_used_actions = SpartanAPM.redis.zrevrange(action_key, SpartanAPM.max_actions + 1, -1)
363
+ min_used_actions.delete(".")
364
+ return if min_used_actions.empty?
365
+ min_used_actions.each do |action|
366
+ SpartanAPM.redis.multi do |transaction|
367
+ transaction.zrem(action_key, action)
368
+ transaction.del(redis_key(SpartanAPM.env, app, action, bucket))
369
+ end
370
+ end
371
+ end
372
+
373
+ def deflate(string)
374
+ Zlib::Deflate.deflate(string, Zlib::DEFAULT_COMPRESSION)
375
+ end
376
+
377
+ def eval_script(script, keys, args)
378
+ script_sha = Digest::SHA1.hexdigest(script)
379
+ attempts = 0
380
+ redis = SpartanAPM.redis
381
+ begin
382
+ redis.evalsha(script_sha, keys, args)
383
+ rescue Redis::CommandError => e
384
+ if e.message.include?("NOSCRIPT") && attempts < 2
385
+ attempts += 1
386
+ script_sha = redis.script(:load, script)
387
+ retry
388
+ else
389
+ raise e
390
+ end
391
+ end
392
+ end
393
+ end
394
+
395
+ def initialize(app, env: SpartanAPM.env)
396
+ @app = app
397
+ @env = env
398
+ end
399
+
400
+ def report_info(time_range, action: nil, host: nil)
401
+ metrics = []
402
+ hosts = Set.new
403
+ read_range(time_range, action, host) do |bucket, metric_data, bucket_hosts|
404
+ metric = metric_from_values(bucket, metric_data)
405
+ metrics << metric if metric
406
+ bucket_hosts.each { |h| hosts.add(h) }
407
+ end
408
+ [metrics, hosts.to_a]
409
+ end
410
+
411
+ def metrics(time_range, action: nil, host: nil)
412
+ report_info(time_range, action: action, host: host).first
413
+ end
414
+
415
+ def hourly_metrics(time_range)
416
+ read_aggregated_metrics(:hour, time_range)
417
+ end
418
+
419
+ def daily_metrics(time_range)
420
+ read_aggregated_metrics(:day, time_range)
421
+ end
422
+
423
+ def errors(time_range)
424
+ backtrace_cache = {}
425
+ errors = []
426
+ empty_backtrace = [].freeze
427
+ redis = SpartanAPM.redis
428
+ each_bucket(time_range) do |bucket|
429
+ key = errors_key(bucket)
430
+ redis.zrevrange(key, 0, 99, withscores: true).each do |payload, count|
431
+ class_name, message, raw_backtrace = MessagePack.load(Zlib::Inflate.inflate(payload))
432
+ backtrace = empty_backtrace
433
+ if raw_backtrace
434
+ backtrace_key = Digest::MD5.hexdigest(raw_backtrace.join("\n"))
435
+ backtrace = backtrace_cache[backtrace_key]
436
+ if backtrace.nil? && !raw_backtrace.nil?
437
+ backtrace = raw_backtrace.freeze
438
+ backtrace_cache[backtrace_key] = backtrace
439
+ end
440
+ end
441
+ errors << ErrorInfo.new(SpartanAPM.bucket_time(bucket), class_name, message, backtrace, count)
442
+ end
443
+ end
444
+ errors
445
+ end
446
+
447
+ def actions(time_range, limit: 100, interval: 1)
448
+ action_times = Hash.new(0.0)
449
+ total_time = 0.0
450
+ redis = SpartanAPM.redis
451
+ each_bucket(time_range, interval: interval) do |bucket|
452
+ redis.zrevrange(actions_key(bucket), 0, limit, withscores: true).each do |action, time_spent|
453
+ if action == ""
454
+ total_time += time_spent
455
+ else
456
+ action_times[action] += time_spent
457
+ end
458
+ end
459
+ end
460
+ actions = {}
461
+ action_times.each do |action, time_spent|
462
+ actions[action] = (total_time > 0 ? time_spent / total_time : 0.0)
463
+ end
464
+ actions.sort_by { |action, time_spent| -time_spent }.take(limit)
465
+ end
466
+
467
+ def hosts(time_range, action: nil)
468
+ uniq_hosts = Set.new
469
+ redis = SpartanAPM.redis
470
+ each_bucket(time_range) do |bucket|
471
+ redis.hkeys(redis_key(action, bucket)).each do |host|
472
+ uniq_hosts << host
473
+ end
474
+ end
475
+ uniq_hosts.to_a.sort
476
+ end
477
+
478
+ def clear!(time_range)
479
+ clear_minute_stats!(time_range)
480
+ clear_hourly_stats!(time_range)
481
+ clear_daily_stats!(time_range)
482
+ end
483
+
484
+ def clear_minute_stats!(time_range)
485
+ redis = SpartanAPM.redis
486
+ each_bucket(time_range) do |bucket|
487
+ action_key = actions_key(bucket)
488
+ actions = redis.zrevrange(action_key, 0, 1_000_000)
489
+ keys = (actions + ["."]).collect { |action| redis_key(action, bucket) }
490
+ redis.del(keys + [action_key, errors_key(bucket)])
491
+ end
492
+ end
493
+
494
+ def clear_hourly_stats!(time_range)
495
+ time_range = [time_range] unless time_range.respond_to?(:last)
496
+ start_hour_bucket = self.class.truncate_to_hour(time_range.first).to_f
497
+ end_hour_bucket = self.class.truncate_to_hour(time_range.last).to_f
498
+ SpartanAPM.redis.zremrangebyscore(hours_key, start_hour_bucket, end_hour_bucket)
499
+ end
500
+
501
+ def clear_daily_stats!(time_range)
502
+ time_range = [time_range] unless time_range.respond_to?(:last)
503
+ start_date_bucket = self.class.truncate_to_date(time_range.first).to_f
504
+ end_date_bucket = self.class.truncate_to_hour(time_range.last).to_f
505
+ SpartanAPM.redis.zremrangebyscore(days_key, start_date_bucket, end_date_bucket)
506
+ end
507
+
508
+ def delete_hourly_stats!
509
+ SpartanAPM.redis.del(hours_key)
510
+ end
511
+
512
+ def delete_daily_stats!
513
+ SpartanAPM.redis.del(days_key)
514
+ end
515
+
516
+ private
517
+
518
+ def redis_key(action, bucket)
519
+ self.class.send(:redis_key, @env, @app, action, bucket)
520
+ end
521
+
522
+ def actions_key(bucket)
523
+ self.class.send(:actions_key, @env, @app, bucket)
524
+ end
525
+
526
+ def errors_key(bucket)
527
+ self.class.send(:errors_key, @env, @app, bucket)
528
+ end
529
+
530
+ def hours_key
531
+ self.class.send(:hours_key, @env, @app)
532
+ end
533
+
534
+ def days_key
535
+ self.class.send(:days_key, @env, @app)
536
+ end
537
+
538
+ def actions_at(bucket)
539
+ SpartanAPM.redis.zrevrange(actions_key(bucket), 0, 1_000_000)
540
+ end
541
+
542
+ def metric_from_values(bucket, combined_data)
543
+ metric = Metric.new(SpartanAPM.bucket_time(bucket))
544
+
545
+ totals = combined_data.delete(".")
546
+ return metric if totals.nil?
547
+
548
+ metric.count = totals.sum(&:first) if totals
549
+ return metric if metric.count == 0
550
+
551
+ times = []
552
+ p50s = []
553
+ p90s = []
554
+ p99s = []
555
+ error_count = 0
556
+ totals.each do |values|
557
+ count = values[0]
558
+ weight = count.to_f / metric.count.to_f
559
+ times << (values[1] * weight) * totals.size
560
+ p50s << values[2]
561
+ p90s << values[3]
562
+ p99s << values[4]
563
+ error_count += values[5].to_i
564
+ end
565
+ metric.avg = (times.compact.sum.to_f / metric.count).round
566
+ metric.p50 = (p50s.compact.sum.to_f / p50s.size).round
567
+ metric.p90 = (p90s.compact.sum.to_f / p90s.size).round
568
+ metric.p99 = (p99s.compact.sum.to_f / p99s.size).round
569
+ metric.error_count = error_count
570
+
571
+ combined_data.each do |name, component_times|
572
+ weighted_component_times = []
573
+ weighted_component_counts = []
574
+ component_times.each do |component_count, component_time, call_count|
575
+ weight = component_count.to_f / metric.count.to_f
576
+ weighted_component_times << (component_time * weight) * totals.size
577
+ weighted_component_counts << (call_count * weight) * totals.size
578
+ end
579
+ metric.components[name] = [(weighted_component_times.sum.to_f / metric.count).round, weighted_component_counts.sum.to_f / metric.count]
580
+ end
581
+
582
+ metric
583
+ end
584
+
585
+ def each_bucket(time_range, interval: 1, &block)
586
+ start_time = (time_range.is_a?(Enumerable) ? time_range.first : time_range)
587
+ end_time = (time_range.is_a?(Enumerable) ? time_range.last : time_range)
588
+ start_bucket = SpartanAPM.bucket(start_time)
589
+ end_bucket = SpartanAPM.bucket(end_time)
590
+ (start_bucket..end_bucket).step(interval).each(&block)
591
+ end
592
+
593
+ def read_range(time_range, action, host)
594
+ redis = SpartanAPM.redis
595
+ each_bucket(time_range) do |bucket|
596
+ key = redis_key(action, bucket)
597
+ host_data = redis.hgetall(key)
598
+
599
+ next if host_data.empty?
600
+
601
+ host_values = (host ? Array(host_data[host]) : host_data.values)
602
+
603
+ combined_data = {}
604
+ host_values.each do |packed_data|
605
+ data = MessagePack.load(packed_data)
606
+ data.each do |name, values|
607
+ combined_values = combined_data[name]
608
+ unless combined_values
609
+ combined_values = []
610
+ combined_data[name] = combined_values
611
+ end
612
+ combined_values.concat(values)
613
+ end
614
+ end
615
+
616
+ yield(bucket, combined_data, host_data.keys)
617
+ end
618
+ end
619
+
620
+ def read_aggregated_metrics(unit, time_range)
621
+ key = (unit == :hour ? hours_key : days_key)
622
+ start_time = Time.at((time_range.respond_to?(:first) ? time_range.first : time_range).to_f)
623
+ end_time = Time.at((time_range.respond_to?(:last) ? time_range.last : time_range).to_f)
624
+
625
+ if unit == :hour
626
+ start_time = self.class.truncate_to_hour(start_time)
627
+ end_time = self.class.truncate_to_hour(end_time)
628
+ else
629
+ start_time = self.class.truncate_to_date(start_time)
630
+ end_time = self.class.truncate_to_date(end_time)
631
+ end
632
+
633
+ SpartanAPM.redis.zrangebyscore(key, start_time.to_f, end_time.to_f).collect do |raw_data|
634
+ data = MessagePack.load(raw_data)
635
+ time = Time.at(data[unit.to_s])
636
+ metric = Metric.new(time)
637
+ metric.count = data["count"]
638
+ metric.error_count = data["error_count"]
639
+ metric.components = data["components"]
640
+ metric.avg = data["avg"]
641
+ metric.p50 = data["p50"]
642
+ metric.p90 = data["p90"]
643
+ metric.p99 = data["p99"]
644
+ metric
645
+ end
646
+ end
647
+ end
648
+ end