pig-ci-rails 0.1.0

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 (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'