ezmetrics 2.0.1 → 3.0.2

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.
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: []