pig-ci-rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|