ezmetrics 2.0.1 → 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.gitignore +3 -0
  4. data/.rspec +2 -0
  5. data/CHANGELOG.md +21 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +41 -0
  8. data/LICENSE +21 -674
  9. data/README.md +68 -143
  10. data/_config.yml +1 -0
  11. data/ezmetrics.gemspec +21 -0
  12. data/lib/ezmetrics.rb +7 -281
  13. data/lib/ezmetrics/benchmark.rb +2 -2
  14. data/lib/ezmetrics/dashboard/app/assets/config/dashboard_manifest.js +2 -0
  15. data/lib/ezmetrics/dashboard/app/assets/images/dashboard/.keep +0 -0
  16. data/lib/ezmetrics/dashboard/app/assets/javascripts/dashboard/application.js +15 -0
  17. data/lib/ezmetrics/dashboard/app/assets/javascripts/dashboard/main.js +1 -0
  18. data/lib/ezmetrics/dashboard/app/assets/stylesheets/dashboard/application.css +16 -0
  19. data/lib/ezmetrics/dashboard/app/assets/stylesheets/dashboard/main.css +6 -0
  20. data/lib/ezmetrics/dashboard/app/controllers/dashboard/application_controller.rb +5 -0
  21. data/lib/ezmetrics/dashboard/app/controllers/dashboard/metrics_controller.rb +41 -0
  22. data/lib/ezmetrics/dashboard/app/views/dashboard/metrics/index.html.erb +3 -0
  23. data/lib/ezmetrics/dashboard/app/views/layouts/dashboard/application.html.erb +12 -0
  24. data/lib/ezmetrics/dashboard/bin/rails +14 -0
  25. data/lib/ezmetrics/dashboard/config/routes.rb +5 -0
  26. data/lib/ezmetrics/dashboard/lib/dashboard.rb +4 -0
  27. data/lib/ezmetrics/dashboard/lib/dashboard/ezmetrics.rb +6 -0
  28. data/lib/ezmetrics/dashboard/lib/dashboard/version.rb +3 -0
  29. data/lib/ezmetrics/dashboard/react-dashboard/.gitignore +23 -0
  30. data/lib/ezmetrics/dashboard/react-dashboard/.rescriptsrc.js +9 -0
  31. data/lib/ezmetrics/dashboard/react-dashboard/README.md +0 -0
  32. data/lib/ezmetrics/dashboard/react-dashboard/package-lock.json +15472 -0
  33. data/lib/ezmetrics/dashboard/react-dashboard/package.json +49 -0
  34. data/lib/ezmetrics/dashboard/react-dashboard/public/index.html +18 -0
  35. data/lib/ezmetrics/dashboard/react-dashboard/public/manifest.json +10 -0
  36. data/lib/ezmetrics/dashboard/react-dashboard/public/robots.txt +2 -0
  37. data/lib/ezmetrics/dashboard/react-dashboard/src/App.tsx +216 -0
  38. data/lib/ezmetrics/dashboard/react-dashboard/src/Constants.tsx +74 -0
  39. data/lib/ezmetrics/dashboard/react-dashboard/src/Graph.tsx +71 -0
  40. data/lib/ezmetrics/dashboard/react-dashboard/src/Metric.tsx +21 -0
  41. data/lib/ezmetrics/dashboard/react-dashboard/src/MetricsBlock.tsx +22 -0
  42. data/lib/ezmetrics/dashboard/react-dashboard/src/Nav.tsx +105 -0
  43. data/lib/ezmetrics/dashboard/react-dashboard/src/RequestsBlock.tsx +78 -0
  44. data/lib/ezmetrics/dashboard/react-dashboard/src/index.css +82 -0
  45. data/lib/ezmetrics/dashboard/react-dashboard/src/index.tsx +9 -0
  46. data/lib/ezmetrics/dashboard/react-dashboard/src/react-app-env.d.ts +1 -0
  47. data/lib/ezmetrics/dashboard/react-dashboard/src/setupTests.ts +5 -0
  48. data/lib/ezmetrics/dashboard/react-dashboard/tsconfig.json +25 -0
  49. data/lib/ezmetrics/dashboard/react-dashboard/yarn.lock +11651 -0
  50. data/lib/ezmetrics/storage.rb +289 -0
  51. data/lib/ezmetrics/version.rb +3 -0
  52. data/lib/generators/ezmetrics/initializer_generator.rb +39 -0
  53. data/scripts/compile_assets +20 -0
  54. metadata +51 -3
@@ -0,0 +1,289 @@
1
+ class Ezmetrics::Storage
2
+ METRICS = [:duration, :views, :db, :queries].freeze
3
+ AGGREGATION_FUNCTIONS = [:max, :avg].freeze
4
+ PARTITION_UNITS = [:second, :minute, :hour, :day].freeze
5
+
6
+ def initialize(interval_seconds=60)
7
+ @interval_seconds = interval_seconds.to_i
8
+ @redis = Redis.new(driver: :hiredis)
9
+ @schema = redis_schema
10
+ end
11
+
12
+ def log(payload={duration: 0.0, views: 0.0, db: 0.0, queries: 0, status: 200, store_each_value: false})
13
+ @safe_payload = {
14
+ duration: payload[:duration].to_f,
15
+ views: payload[:views].to_f,
16
+ db: payload[:db].to_f,
17
+ queries: payload[:queries].to_i,
18
+ status: payload[:status].to_i,
19
+ store_each_value: payload[:store_each_value].to_s == "true"
20
+ }
21
+
22
+ this_second = Time.now.to_i
23
+ status_group = "#{payload[:status].to_s[0]}xx"
24
+ @this_second_metrics = redis.get(this_second)
25
+
26
+ if this_second_metrics
27
+ @this_second_metrics = Oj.load(this_second_metrics)
28
+
29
+ METRICS.each do |metrics_type|
30
+ update_sum(metrics_type)
31
+ update_max(metrics_type)
32
+ store_value(metrics_type) if safe_payload[:store_each_value]
33
+ end
34
+
35
+ this_second_metrics[schema["all"]] += 1
36
+ this_second_metrics[schema[status_group]] += 1
37
+ else
38
+ @this_second_metrics = {
39
+ "second" => this_second,
40
+ "duration_sum" => safe_payload[:duration],
41
+ "duration_max" => safe_payload[:duration],
42
+ "views_sum" => safe_payload[:views],
43
+ "views_max" => safe_payload[:views],
44
+ "db_sum" => safe_payload[:db],
45
+ "db_max" => safe_payload[:db],
46
+ "queries_sum" => safe_payload[:queries],
47
+ "queries_max" => safe_payload[:queries],
48
+ "2xx" => 0,
49
+ "3xx" => 0,
50
+ "4xx" => 0,
51
+ "5xx" => 0,
52
+ "all" => 1
53
+ }
54
+
55
+ if safe_payload[:store_each_value]
56
+ this_second_metrics.merge!(
57
+ "duration_values" => [safe_payload[:duration]],
58
+ "views_values" => [safe_payload[:views]],
59
+ "db_values" => [safe_payload[:db]],
60
+ "queries_values" => [safe_payload[:queries]]
61
+ )
62
+ end
63
+
64
+ this_second_metrics[status_group] = 1
65
+
66
+ @this_second_metrics = this_second_metrics.values
67
+ end
68
+
69
+ redis.setex(this_second, interval_seconds, Oj.dump(this_second_metrics))
70
+ true
71
+ rescue => error
72
+ formatted_error(error)
73
+ end
74
+
75
+ def show(options=nil)
76
+ @options = options || default_options
77
+ partitioned_metrics ? aggregate_partitioned_data : aggregate_data
78
+ end
79
+
80
+ def flatten
81
+ @flat = true
82
+ self
83
+ end
84
+
85
+ def partition_by(time_unit=:minute)
86
+ time_unit = PARTITION_UNITS.include?(time_unit) ? time_unit : :minute
87
+ @partitioned_metrics = interval_metrics.group_by { |array| second_to_partition_unit(time_unit, array[schema["second"]]) }
88
+ self
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :redis, :interval_seconds, :interval_metrics, :requests, :flat, :schema,
94
+ :storage_key, :safe_payload, :this_second_metrics, :partitioned_metrics, :options
95
+
96
+ def aggregate_data
97
+ return {} unless interval_metrics.any?
98
+ @requests = interval_metrics.sum { |array| array[schema["all"]] }
99
+ build_result
100
+ rescue
101
+ {}
102
+ end
103
+
104
+ def aggregate_partitioned_data
105
+ partitioned_metrics.map do |partition, metrics|
106
+ @interval_metrics = metrics
107
+ @requests = interval_metrics.sum { |array| array[schema["all"]] }
108
+ METRICS.each { |metrics_type| instance_variable_set("@sorted_#{metrics_type}_values", nil) }
109
+ flat ? { timestamp: partition, **build_result } : { timestamp: partition, data: build_result }
110
+ end
111
+ rescue
112
+ self
113
+ end
114
+
115
+ def build_result
116
+ result = {}
117
+
118
+ if options[:requests]
119
+ append_requests_to_result(result, { all: requests, grouped: count_all_status_groups })
120
+ end
121
+
122
+ options.each do |metrics, aggregation_functions|
123
+ next unless METRICS.include?(metrics)
124
+ aggregation_functions = [aggregation_functions] unless aggregation_functions.is_a?(Array)
125
+ next unless aggregation_functions.any?
126
+
127
+ aggregation_functions.each do |aggregation_function|
128
+ aggregated_metrics = aggregate(metrics, aggregation_function)
129
+ append_metrics_to_result(result, metrics, aggregation_function, aggregated_metrics)
130
+ end
131
+ end
132
+ result
133
+ ensure
134
+ result
135
+ end
136
+
137
+ def append_requests_to_result(result, aggregated_requests)
138
+ return result[:requests] = aggregated_requests unless flat
139
+
140
+ result[:requests_all] = aggregated_requests[:all]
141
+ aggregated_requests[:grouped].each do |group, counter|
142
+ result[:"requests_#{group}"] = counter
143
+ end
144
+ end
145
+
146
+ def append_metrics_to_result(result, metrics, aggregation_function, aggregated_metrics)
147
+ return result[:"#{metrics}_#{aggregation_function}"] = aggregated_metrics if flat
148
+
149
+ result[metrics] ||= {}
150
+ result[metrics][aggregation_function] = aggregated_metrics
151
+ end
152
+
153
+ def second_to_partition_unit(time_unit, second)
154
+ return second if time_unit == :second
155
+ time = Time.at(second)
156
+ return (time - time.sec - time.min * 60 - time.hour * 3600).to_i if time_unit == :day
157
+ return (time - time.sec - time.min * 60).to_i if time_unit == :hour
158
+ (time - time.sec).to_i
159
+ end
160
+
161
+ def interval_metrics
162
+ @interval_metrics ||= begin
163
+ interval_start = Time.now.to_i - interval_seconds
164
+ interval_keys = (interval_start..Time.now.to_i).to_a
165
+ redis.mget(interval_keys).compact.map { |array| Oj.load(array) }
166
+ end
167
+ end
168
+
169
+ def aggregate(metrics, aggregation_function)
170
+ return avg("#{metrics}_sum") if aggregation_function == :avg
171
+ return max("#{metrics}_max") if aggregation_function == :max
172
+
173
+ if aggregation_function == :percentile_distribution
174
+ sorted_values = send("sorted_#{metrics}_values")
175
+ return (1..99).to_a.inject({}) do |result, number|
176
+ result[number] = percentile(sorted_values, number)
177
+ result
178
+ end
179
+ end
180
+
181
+ percentile = aggregation_function.match(/percentile_(?<value>\d+)/)
182
+
183
+ if percentile && percentile["value"].to_i.between?(1, 99)
184
+ sorted_values = send("sorted_#{metrics}_values")
185
+ percentile(sorted_values, percentile["value"].to_i)
186
+ end
187
+ end
188
+
189
+ METRICS.each do |metrics|
190
+ define_method "sorted_#{metrics}_values" do
191
+ instance_variable_get("@sorted_#{metrics}_values") || instance_variable_set(
192
+ "@sorted_#{metrics}_values", interval_metrics.map { |array| array[schema["#{metrics}_values"]] }.flatten.compact.sort
193
+ )
194
+ end
195
+ end
196
+
197
+ def redis_schema
198
+ [
199
+ "second",
200
+ "duration_sum",
201
+ "duration_max",
202
+ "views_sum",
203
+ "views_max",
204
+ "db_sum",
205
+ "db_max",
206
+ "queries_sum",
207
+ "queries_max",
208
+ "2xx",
209
+ "3xx",
210
+ "4xx",
211
+ "5xx",
212
+ "all",
213
+ "duration_values",
214
+ "views_values",
215
+ "db_values",
216
+ "queries_values"
217
+ ].each_with_index.inject({}){ |result, pair| result[pair[0]] = pair[1] ; result }
218
+ end
219
+
220
+ def update_sum(metrics)
221
+ this_second_metrics[schema["#{metrics}_sum"]] += safe_payload[metrics]
222
+ end
223
+
224
+ def store_value(metrics)
225
+ this_second_metrics[schema["#{metrics}_values"]] << safe_payload[metrics]
226
+ end
227
+
228
+ def update_max(metrics)
229
+ max_value = [safe_payload[metrics], this_second_metrics[schema["#{metrics}_max"]]].max
230
+ this_second_metrics[schema["#{metrics}_max"]] = max_value
231
+ end
232
+
233
+ def avg(metrics)
234
+ (interval_metrics.sum { |array| array[schema[metrics]] }.to_f / requests).round
235
+ end
236
+
237
+ def max(metrics)
238
+ interval_metrics.max { |array| array[schema[metrics]] }[schema[metrics]].round
239
+ end
240
+
241
+ def percentile(sorted_array, pcnt)
242
+ array_length = sorted_array.length
243
+
244
+ return "not enough data (requests: #{array_length}, required: #{pcnt})" if array_length < pcnt
245
+
246
+ rank = (pcnt.to_f / 100) * (array_length + 1)
247
+ whole = rank.truncate
248
+
249
+ # if has fractional part
250
+ if whole != rank
251
+ s0 = sorted_array[whole - 1]
252
+ s1 = sorted_array[whole]
253
+
254
+ f = (rank - rank.truncate).abs
255
+
256
+ return ((f * (s1 - s0)) + s0)&.round
257
+ else
258
+ return (sorted_array[whole - 1])&.round
259
+ end
260
+ end
261
+
262
+ def count_all_status_groups
263
+ interval_metrics.inject({ "2xx" => 0, "3xx" => 0, "4xx" => 0, "5xx" => 0 }) do |result, array|
264
+ result["2xx"] += array[schema["2xx"]]
265
+ result["3xx"] += array[schema["3xx"]]
266
+ result["4xx"] += array[schema["4xx"]]
267
+ result["5xx"] += array[schema["5xx"]]
268
+ result
269
+ end
270
+ end
271
+
272
+ def default_options
273
+ {
274
+ duration: AGGREGATION_FUNCTIONS,
275
+ views: AGGREGATION_FUNCTIONS,
276
+ db: AGGREGATION_FUNCTIONS,
277
+ queries: AGGREGATION_FUNCTIONS,
278
+ requests: true
279
+ }
280
+ end
281
+
282
+ def formatted_error(error)
283
+ {
284
+ error: error.class.name,
285
+ message: error.message,
286
+ backtrace: error.backtrace.reject { |line| line.match(/ruby|gems/) }
287
+ }
288
+ end
289
+ end
@@ -0,0 +1,3 @@
1
+ module Ezmetrics
2
+ VERSION = "3.0.2"
3
+ end
@@ -0,0 +1,39 @@
1
+ require 'rails/generators/base'
2
+
3
+ module Ezmetrics
4
+ module Generators
5
+ class InitializerGenerator < Rails::Generators::Base
6
+ desc "This generator creates an initializer file at config/initializers"
7
+
8
+ def create_initializer_file
9
+ create_file "config/initializers/ezmetrics.rb", config_content
10
+ end
11
+
12
+ def config_content
13
+ <<RUBY
14
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
15
+ event = ActiveSupport::Notifications::Event.new(*args)
16
+ unless event.payload[:name] == "SCHEMA" || event.payload[:sql] =~ /\ABEGIN|COMMIT|ROLLBACK\z/
17
+ Thread.current[:queries] ||= 0
18
+ Thread.current[:queries] += 1
19
+ end
20
+ end
21
+
22
+ ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
23
+ event = ActiveSupport::Notifications::Event.new(*args)
24
+ Ezmetrics::Storage.new(24.hours).log(
25
+ duration: event.duration.to_f,
26
+ views: event.payload[:view_runtime].to_f,
27
+ db: event.payload[:db_runtime].to_f,
28
+ status: event.payload[:exception] ? 500 : event.payload[:status].to_i,
29
+ queries: Thread.current[:queries].to_i,
30
+ store_each_value: true
31
+ )
32
+
33
+ Thread.current[:queries] = 0
34
+ end
35
+ RUBY
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+
3
+ cd lib/ezmetrics/dashboard/react-dashboard
4
+
5
+ rm -r build
6
+
7
+ yarn build:clean
8
+
9
+ cd build
10
+
11
+ rm -f main.js main.css
12
+
13
+ cp static/js/*.js main.js
14
+ cp static/css/*.css main.css
15
+
16
+ sed '$d' main.css > main.css.upd && mv main.css.upd main.css
17
+ sed '$d' main.js > main.js.upd && sed '1d' main.js.upd > main.js
18
+
19
+ cp main.js ../../app/assets/javascripts/dashboard/main.js
20
+ cp main.css ../../app/assets/stylesheets/dashboard/main.css
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ezmetrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 3.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolae Rotaru
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-22 00:00:00.000000000 Z
11
+ date: 2020-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -72,13 +72,61 @@ executables: []
72
72
  extensions: []
73
73
  extra_rdoc_files: []
74
74
  files:
75
+ - ".github/FUNDING.yml"
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - CHANGELOG.md
79
+ - Gemfile
80
+ - Gemfile.lock
75
81
  - LICENSE
76
82
  - README.md
83
+ - _config.yml
84
+ - ezmetrics.gemspec
77
85
  - lib/ezmetrics.rb
78
86
  - lib/ezmetrics/benchmark.rb
87
+ - lib/ezmetrics/dashboard/app/assets/config/dashboard_manifest.js
88
+ - lib/ezmetrics/dashboard/app/assets/images/dashboard/.keep
89
+ - lib/ezmetrics/dashboard/app/assets/javascripts/dashboard/application.js
90
+ - lib/ezmetrics/dashboard/app/assets/javascripts/dashboard/main.js
91
+ - lib/ezmetrics/dashboard/app/assets/stylesheets/dashboard/application.css
92
+ - lib/ezmetrics/dashboard/app/assets/stylesheets/dashboard/main.css
93
+ - lib/ezmetrics/dashboard/app/controllers/dashboard/application_controller.rb
94
+ - lib/ezmetrics/dashboard/app/controllers/dashboard/metrics_controller.rb
95
+ - lib/ezmetrics/dashboard/app/views/dashboard/metrics/index.html.erb
96
+ - lib/ezmetrics/dashboard/app/views/layouts/dashboard/application.html.erb
97
+ - lib/ezmetrics/dashboard/bin/rails
98
+ - lib/ezmetrics/dashboard/config/routes.rb
99
+ - lib/ezmetrics/dashboard/lib/dashboard.rb
100
+ - lib/ezmetrics/dashboard/lib/dashboard/ezmetrics.rb
101
+ - lib/ezmetrics/dashboard/lib/dashboard/version.rb
102
+ - lib/ezmetrics/dashboard/react-dashboard/.gitignore
103
+ - lib/ezmetrics/dashboard/react-dashboard/.rescriptsrc.js
104
+ - lib/ezmetrics/dashboard/react-dashboard/README.md
105
+ - lib/ezmetrics/dashboard/react-dashboard/package-lock.json
106
+ - lib/ezmetrics/dashboard/react-dashboard/package.json
107
+ - lib/ezmetrics/dashboard/react-dashboard/public/index.html
108
+ - lib/ezmetrics/dashboard/react-dashboard/public/manifest.json
109
+ - lib/ezmetrics/dashboard/react-dashboard/public/robots.txt
110
+ - lib/ezmetrics/dashboard/react-dashboard/src/App.tsx
111
+ - lib/ezmetrics/dashboard/react-dashboard/src/Constants.tsx
112
+ - lib/ezmetrics/dashboard/react-dashboard/src/Graph.tsx
113
+ - lib/ezmetrics/dashboard/react-dashboard/src/Metric.tsx
114
+ - lib/ezmetrics/dashboard/react-dashboard/src/MetricsBlock.tsx
115
+ - lib/ezmetrics/dashboard/react-dashboard/src/Nav.tsx
116
+ - lib/ezmetrics/dashboard/react-dashboard/src/RequestsBlock.tsx
117
+ - lib/ezmetrics/dashboard/react-dashboard/src/index.css
118
+ - lib/ezmetrics/dashboard/react-dashboard/src/index.tsx
119
+ - lib/ezmetrics/dashboard/react-dashboard/src/react-app-env.d.ts
120
+ - lib/ezmetrics/dashboard/react-dashboard/src/setupTests.ts
121
+ - lib/ezmetrics/dashboard/react-dashboard/tsconfig.json
122
+ - lib/ezmetrics/dashboard/react-dashboard/yarn.lock
123
+ - lib/ezmetrics/storage.rb
124
+ - lib/ezmetrics/version.rb
125
+ - lib/generators/ezmetrics/initializer_generator.rb
126
+ - scripts/compile_assets
79
127
  homepage: https://github.com/nyku/ezmetrics
80
128
  licenses:
81
- - GPL-3.0
129
+ - MIT
82
130
  metadata: {}
83
131
  post_install_message:
84
132
  rdoc_options: []