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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +24 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +124 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/locales/pig_ci/en.yml +43 -0
- data/lib/pig_ci.rb +146 -0
- data/lib/pig_ci/api.rb +14 -0
- data/lib/pig_ci/api/reports.rb +36 -0
- data/lib/pig_ci/decorator.rb +9 -0
- data/lib/pig_ci/decorator/report_terminal_decorator.rb +19 -0
- data/lib/pig_ci/metric.rb +4 -0
- data/lib/pig_ci/metric/current.rb +30 -0
- data/lib/pig_ci/metric/historial/change_percentage.rb +37 -0
- data/lib/pig_ci/metric/historical.rb +52 -0
- data/lib/pig_ci/profiler.rb +41 -0
- data/lib/pig_ci/profiler/database_request.rb +5 -0
- data/lib/pig_ci/profiler/memory.rb +16 -0
- data/lib/pig_ci/profiler/request_time.rb +17 -0
- data/lib/pig_ci/profiler_engine.rb +40 -0
- data/lib/pig_ci/profiler_engine/rails.rb +61 -0
- data/lib/pig_ci/report.rb +64 -0
- data/lib/pig_ci/report/database_request.rb +2 -0
- data/lib/pig_ci/report/memory.rb +15 -0
- data/lib/pig_ci/report/request_time.rb +2 -0
- data/lib/pig_ci/summary.rb +4 -0
- data/lib/pig_ci/summary/html.rb +49 -0
- data/lib/pig_ci/summary/terminal.rb +30 -0
- data/lib/pig_ci/version.rb +3 -0
- data/lib/pig_ci/views/index.erb +70 -0
- data/lib/pig_ci/views/report.erb +30 -0
- data/pig_ci.gemspec +47 -0
- data/public/assets/application.css +3 -0
- data/public/assets/application.js +109 -0
- metadata +266 -0
data/lib/pig_ci/api.rb
ADDED
@@ -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,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,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,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'
|