pig-ci-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rubocop.yml +24 -0
  4. data/.travis.yml +21 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE +21 -0
  9. data/README.md +124 -0
  10. data/Rakefile +10 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/config/locales/pig_ci/en.yml +43 -0
  14. data/lib/pig_ci.rb +146 -0
  15. data/lib/pig_ci/api.rb +14 -0
  16. data/lib/pig_ci/api/reports.rb +36 -0
  17. data/lib/pig_ci/decorator.rb +9 -0
  18. data/lib/pig_ci/decorator/report_terminal_decorator.rb +19 -0
  19. data/lib/pig_ci/metric.rb +4 -0
  20. data/lib/pig_ci/metric/current.rb +30 -0
  21. data/lib/pig_ci/metric/historial/change_percentage.rb +37 -0
  22. data/lib/pig_ci/metric/historical.rb +52 -0
  23. data/lib/pig_ci/profiler.rb +41 -0
  24. data/lib/pig_ci/profiler/database_request.rb +5 -0
  25. data/lib/pig_ci/profiler/memory.rb +16 -0
  26. data/lib/pig_ci/profiler/request_time.rb +17 -0
  27. data/lib/pig_ci/profiler_engine.rb +40 -0
  28. data/lib/pig_ci/profiler_engine/rails.rb +61 -0
  29. data/lib/pig_ci/report.rb +64 -0
  30. data/lib/pig_ci/report/database_request.rb +2 -0
  31. data/lib/pig_ci/report/memory.rb +15 -0
  32. data/lib/pig_ci/report/request_time.rb +2 -0
  33. data/lib/pig_ci/summary.rb +4 -0
  34. data/lib/pig_ci/summary/html.rb +49 -0
  35. data/lib/pig_ci/summary/terminal.rb +30 -0
  36. data/lib/pig_ci/version.rb +3 -0
  37. data/lib/pig_ci/views/index.erb +70 -0
  38. data/lib/pig_ci/views/report.erb +30 -0
  39. data/pig_ci.gemspec +47 -0
  40. data/public/assets/application.css +3 -0
  41. data/public/assets/application.js +109 -0
  42. metadata +266 -0
data/lib/pig_ci/api.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'httparty'
2
+
3
+ class PigCI::Api
4
+ include HTTParty
5
+
6
+ def headers
7
+ {
8
+ 'Content-Type': 'application/json',
9
+ 'X-ApiKey': PigCI.api_key
10
+ }
11
+ end
12
+ end
13
+
14
+ require 'pig_ci/api/reports'
@@ -0,0 +1,36 @@
1
+ class PigCI::Api::Reports < PigCI::Api
2
+ def initialize(reports: [])
3
+ @reports = reports
4
+ end
5
+
6
+ def share!
7
+ response = post_payload
8
+ return if response.success?
9
+
10
+ puts I18n.t('pig_ci.api.reports.error', error: JSON.parse(response.parsed_response || '{}')['error'])
11
+ rescue Net::OpenTimeout => e
12
+ puts I18n.t('pig_ci.api.reports.error', error: e.inspect)
13
+ end
14
+
15
+ private
16
+
17
+ def post_payload
18
+ self.class.post(
19
+ '/v1/reports',
20
+ base_uri: PigCI.api_base_uri,
21
+ verify: PigCI.api_verify_ssl?,
22
+ body: payload,
23
+ headers: headers
24
+ )
25
+ end
26
+
27
+ def payload
28
+ {
29
+ library: 'pig-ci-rails',
30
+ library_version: PigCI::VERSION,
31
+ commit_sha1: PigCI.commit_sha1,
32
+ head_branch: PigCI.head_branch,
33
+ reports: @reports.collect { |report| report.to_payload_for(PigCI.run_timestamp) }
34
+ }.to_json
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ class PigCI::Decorator
2
+ attr_accessor :object
3
+
4
+ def initialize(object)
5
+ @object = object
6
+ end
7
+ end
8
+
9
+ require 'pig_ci/decorator/report_terminal_decorator'
@@ -0,0 +1,19 @@
1
+ require 'colorized_string'
2
+
3
+ class PigCI::Decorator::ReportTerminalDecorator < PigCI::Decorator
4
+ %i[key max min mean number_of_requests].each do |field|
5
+ define_method(field) do
6
+ @object[field]
7
+ end
8
+ end
9
+
10
+ def max_change_percentage
11
+ if @object[:max_change_percentage].start_with?('-')
12
+ ColorizedString[@object[:max_change_percentage]].colorize(:green)
13
+ elsif @object[:max_change_percentage].start_with?('0.0')
14
+ @object[:max_change_percentage]
15
+ else
16
+ ColorizedString[@object[:max_change_percentage]].colorize(:red)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ class PigCI::Metric; end
2
+
3
+ require 'pig_ci/metric/current'
4
+ require 'pig_ci/metric/historical'
@@ -0,0 +1,30 @@
1
+ class PigCI::Metric::Current
2
+ def initialize(log_file:)
3
+ @log_file = log_file
4
+ end
5
+
6
+ def to_h
7
+ @to_h = {}
8
+
9
+ File.foreach(@log_file) do |f|
10
+ key, value = f.strip.split('|')
11
+ value = value.to_i
12
+
13
+ @to_h[key] ||= {
14
+ key: key,
15
+ max: value,
16
+ min: value,
17
+ mean: 0,
18
+ total: 0,
19
+ number_of_requests: 0
20
+ }
21
+ @to_h[key][:max] = value if value > @to_h[key][:max]
22
+ @to_h[key][:min] = value if value < @to_h[key][:min]
23
+ @to_h[key][:total] += value
24
+ @to_h[key][:number_of_requests] += 1
25
+ @to_h[key][:mean] = @to_h[key][:total] / @to_h[key][:number_of_requests]
26
+ end
27
+
28
+ @to_h.collect { |_k, d| d }
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ class PigCI::Metric::Historical::ChangePercentage
2
+ def initialize(previous_data:, data:)
3
+ @previous_data = previous_data
4
+ @data = data
5
+ @timestamp = @data.keys.first
6
+ @profiler = @data[@timestamp].keys.first
7
+ end
8
+
9
+ def updated_data
10
+ @data[@timestamp][@profiler].collect do |data|
11
+ previous_run_data = previous_run_data_for_key(data[:key]) || data
12
+
13
+ data[:max_change_percentage] = (((BigDecimal(data[:max]) - BigDecimal(previous_run_data[:max])) / BigDecimal(previous_run_data[:max])) * 100).round(PigCI.max_change_percentage_precision)
14
+ data[:max_change_percentage] = BigDecimal('0') if data[:max_change_percentage].to_s == 'NaN' || data[:max_change_percentage] == BigDecimal('-0.0')
15
+ data[:max_change_percentage] = data[:max_change_percentage].to_f
16
+
17
+ data
18
+ end
19
+
20
+ @data
21
+ end
22
+
23
+ private
24
+
25
+ def previous_run_data_for_key(key)
26
+ previous_data_keys.each do |previous_run_key|
27
+ @previous_data[previous_run_key][@profiler.to_sym].each do |raw_previous_run_data|
28
+ return raw_previous_run_data if raw_previous_run_data[:key] == key
29
+ end
30
+ end
31
+ nil
32
+ end
33
+
34
+ def previous_data_keys
35
+ @previous_data_keys ||= @previous_data.keys.sort.reverse
36
+ end
37
+ end
@@ -0,0 +1,52 @@
1
+ class PigCI::Metric::Historical
2
+ def initialize(historical_log_file:)
3
+ @historical_log_file = historical_log_file
4
+ end
5
+
6
+ def to_h
7
+ @to_h ||= read_historical_log_file.sort_by { |timestamp, _data| timestamp.to_s.to_i * -1 }.to_h
8
+ end
9
+
10
+ # In future this might honour some limit.
11
+ def append!(timestamp:, metric:, data:)
12
+ to_h
13
+ @to_h[timestamp] ||= {}
14
+ @to_h[timestamp][metric] = data
15
+ remove_old_historical_data!
16
+ save!
17
+ end
18
+
19
+ def add_change_percentage_and_append!(timestamp:, metric:, data:)
20
+ max_change_percentage_data = {}
21
+ max_change_percentage_data[timestamp] = {}
22
+ max_change_percentage_data[timestamp][metric] = data
23
+
24
+ data = PigCI::Metric::Historical::ChangePercentage.new(previous_data: to_h, data: max_change_percentage_data).updated_data
25
+ append!(timestamp: timestamp, metric: metric, data: data[timestamp][metric])
26
+ end
27
+
28
+ private
29
+
30
+ def remove_old_historical_data!
31
+ new_historical_data = @to_h
32
+ .sort_by { |timestamp, _data| timestamp.to_s.to_i * -1 }[0..(PigCI.historical_data_run_limit - 1)]
33
+ .to_h
34
+ .sort_by { |timestamp, _data| timestamp.to_s.to_i * -1 }.to_h
35
+ @to_h = new_historical_data
36
+ end
37
+
38
+ def read_historical_log_file
39
+ if File.exist?(@historical_log_file)
40
+ JSON.parse(File.open(@historical_log_file, 'r').read, symbolize_names: true)
41
+ else
42
+ {}
43
+ end
44
+ end
45
+
46
+ def save!
47
+ File.write(@historical_log_file, @to_h.to_json)
48
+ @to_h = nil
49
+ end
50
+ end
51
+
52
+ require 'pig_ci/metric/historial/change_percentage'
@@ -0,0 +1,41 @@
1
+ class PigCI::Profiler
2
+ attr_accessor :log_value, :log_file, :historical_log_file, :i18n_key
3
+
4
+ def initialize(i18n_key: nil, log_file: nil, historical_log_file: nil)
5
+ @i18n_key = i18n_key || self.class.name.underscore.split('/').last
6
+ @log_file = log_file || PigCI.tmp_directory.join("#{@i18n_key}.txt")
7
+ @historical_log_file = historical_log_file || PigCI.tmp_directory.join("#{@i18n_key}.json")
8
+ @log_value = 0
9
+ end
10
+
11
+ def setup!
12
+ File.open(log_file, 'w') { |file| file.truncate(0) }
13
+ end
14
+
15
+ def reset!
16
+ @log_value = 0
17
+ end
18
+
19
+ def log_request!(request_key)
20
+ File.open(log_file, 'a+') do |f|
21
+ f.puts([request_key, log_value].join('|'))
22
+ end
23
+ end
24
+
25
+ def save!
26
+ historical_data = PigCI::Metric::Historical.new(historical_log_file: @historical_log_file)
27
+ historical_data.add_change_percentage_and_append!(
28
+ timestamp: PigCI.run_timestamp,
29
+ metric: i18n_key,
30
+ data: PigCI::Metric::Current.new(log_file: log_file).to_h
31
+ )
32
+ end
33
+
34
+ def increment!(*)
35
+ raise NotImplementedError
36
+ end
37
+ end
38
+
39
+ require 'pig_ci/profiler/memory'
40
+ require 'pig_ci/profiler/request_time'
41
+ require 'pig_ci/profiler/database_request'
@@ -0,0 +1,5 @@
1
+ class PigCI::Profiler::DatabaseRequest < PigCI::Profiler
2
+ def increment!(by: 1)
3
+ @log_value += by
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ require 'get_process_mem'
2
+
3
+ class PigCI::Profiler::Memory < PigCI::Profiler
4
+ def reset!
5
+ GC.disable
6
+ end
7
+
8
+ def log_request!(request_key)
9
+ GC.enable
10
+ super
11
+ end
12
+
13
+ def log_value
14
+ ::GetProcessMem.new.bytes
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ class PigCI::Profiler::RequestTime < PigCI::Profiler
2
+ attr_accessor :start_time, :end_time
3
+
4
+ def reset!
5
+ super
6
+ @start_time = Time.now.utc
7
+ end
8
+
9
+ def log_request!(request_key)
10
+ @end_time = Time.now.utc
11
+ super
12
+ end
13
+
14
+ def log_value
15
+ (@end_time - @start_time) * 1000.0
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ class PigCI::ProfilerEngine
2
+ attr_accessor :request_key, :profilers, :reports, :request_captured
3
+
4
+ def initialize(profilers: nil, reports: nil)
5
+ @profilers = profilers || []
6
+ @reports = reports || []
7
+ @request_captured = false
8
+ end
9
+
10
+ def request_key?
11
+ !@request_key.nil? && @request_key != ''
12
+ end
13
+
14
+ def request_captured?
15
+ @request_captured
16
+ end
17
+
18
+ def request_captured!
19
+ @request_captured = true
20
+ end
21
+
22
+ def setup!
23
+ Dir.mkdir(PigCI.tmp_directory) unless File.exist?(PigCI.tmp_directory)
24
+
25
+ yield if block_given?
26
+
27
+ profilers.collect(&:setup!)
28
+
29
+ # Attach listeners to the rails events.
30
+ attach_listeners!
31
+ end
32
+
33
+ private
34
+
35
+ def attach_listeners!
36
+ raise NotImplementedError
37
+ end
38
+ end
39
+
40
+ require 'pig_ci/profiler_engine/rails'
@@ -0,0 +1,61 @@
1
+ # This subscribes to the ActiveSupport::Notifications and passes it onto our profilers.
2
+ class PigCI::ProfilerEngine::Rails < ::PigCI::ProfilerEngine
3
+ def initialize(profilers: nil, reports: nil)
4
+ @profilers = profilers || [
5
+ PigCI::Profiler::Memory.new,
6
+ PigCI::Profiler::RequestTime.new,
7
+ PigCI::Profiler::DatabaseRequest.new
8
+ ]
9
+ @reports = reports || [
10
+ PigCI::Report::Memory.new,
11
+ PigCI::Report::RequestTime.new,
12
+ PigCI::Report::DatabaseRequest.new
13
+ ]
14
+ @request_captured = false
15
+ end
16
+
17
+ def request_key_from_payload!(payload)
18
+ @request_key = "#{payload[:method]} #{payload[:controller]}##{payload[:action]}{format:#{payload[:format]}}"
19
+ end
20
+
21
+ def setup!
22
+ super do
23
+ eager_load_rails!
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def eager_load_rails!
30
+ # Eager load rails to give more accurate memory levels.
31
+ ::Rails.application.eager_load!
32
+ ::Rails::Engine.subclasses.map(&:instance).each(&:eager_load!)
33
+ ::ActiveRecord::Base.descendants
34
+
35
+ # Make a call to the root path to load up as much of rails as possible
36
+ ::Rails.application.call(::Rack::MockRequest.env_for('/'))
37
+ end
38
+
39
+ def attach_listeners!
40
+ ::ActiveSupport::Notifications.subscribe 'start_processing.action_controller' do |_name, _started, _finished, _unique_id, payload|
41
+ request_key_from_payload!(payload)
42
+
43
+ profilers.each(&:reset!)
44
+ end
45
+
46
+ ::ActiveSupport::Notifications.subscribe 'sql.active_record' do |_name, _started, _finished, _unique_id, _payload|
47
+ if request_key?
48
+ profilers.select { |profiler| profiler.class == PigCI::Profiler::DatabaseRequest }.each(&:increment!)
49
+ end
50
+ end
51
+
52
+ ::ActiveSupport::Notifications.subscribe 'process_action.action_controller' do |_name, _started, _finished, _unique_id, _payload|
53
+ profilers.each do |profiler|
54
+ profiler.log_request!(request_key)
55
+ end
56
+
57
+ request_captured!
58
+ self.request_key = nil
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,64 @@
1
+ class PigCI::Report
2
+ attr_accessor :historical_log_file, :i18n_key
3
+
4
+ def initialize(historical_log_file: nil, i18n_key: nil, timestamp: nil)
5
+ @i18n_key = i18n_key || self.class.name.underscore.split('/').last
6
+ @historical_log_file = historical_log_file || PigCI.tmp_directory.join("#{@i18n_key}.json")
7
+ @timestamp = timestamp || PigCI.run_timestamp
8
+ end
9
+
10
+ def headings
11
+ column_keys.collect { |key| I18n.t(".attributes.#{key}", scope: i18n_scope, locale: PigCI.locale) }
12
+ end
13
+
14
+ def i18n_name
15
+ I18n.t('.name', scope: i18n_scope, locale: PigCI.locale)
16
+ end
17
+
18
+ def sorted_and_formatted_data_for(timestamp)
19
+ data_for(timestamp)[@i18n_key.to_sym].sort_by do |data|
20
+ PigCI.report_row_sort_by(data)
21
+ end.collect do |data|
22
+ self.class.format_row(data)
23
+ end
24
+ end
25
+
26
+ def to_payload_for(timestamp)
27
+ {
28
+ profiler: @i18n_key.to_sym,
29
+ data: data_for(timestamp)[@i18n_key.to_sym]
30
+ }
31
+ end
32
+
33
+ def historical_data
34
+ @historical_data ||= PigCI::Metric::Historical.new(historical_log_file: @historical_log_file).to_h
35
+ end
36
+
37
+ def timestamps
38
+ historical_data.keys
39
+ end
40
+
41
+ def column_keys
42
+ %i[key max min mean number_of_requests max_change_percentage]
43
+ end
44
+
45
+ def i18n_scope
46
+ @i18n_scope ||= "pig_ci.report.#{i18n_key}"
47
+ end
48
+
49
+ def self.format_row(row)
50
+ row = row.dup
51
+ row[:max_change_percentage] = "#{row[:max_change_percentage]}%"
52
+ row
53
+ end
54
+
55
+ private
56
+
57
+ def data_for(timestamp)
58
+ historical_data[timestamp.to_sym]
59
+ end
60
+ end
61
+
62
+ require 'pig_ci/report/memory'
63
+ require 'pig_ci/report/request_time'
64
+ require 'pig_ci/report/database_request'