request-log-analyzer 1.1.0 → 1.1.1
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.
- data/README.rdoc +4 -3
- data/bin/request-log-analyzer +4 -5
- data/lib/cli/command_line_arguments.rb +2 -2
- data/lib/request_log_analyzer.rb +28 -6
- data/lib/request_log_analyzer/{aggregator/base.rb → aggregator.rb} +5 -1
- data/lib/request_log_analyzer/aggregator/summarizer.rb +2 -3
- data/lib/request_log_analyzer/controller.rb +11 -16
- data/lib/request_log_analyzer/file_format.rb +71 -38
- data/lib/request_log_analyzer/file_format/merb.rb +32 -26
- data/lib/request_log_analyzer/file_format/rails.rb +73 -71
- data/lib/request_log_analyzer/file_format/rails_development.rb +93 -95
- data/lib/request_log_analyzer/filter.rb +38 -0
- data/lib/request_log_analyzer/filter/anonimize.rb +1 -1
- data/lib/request_log_analyzer/line_definition.rb +1 -1
- data/lib/request_log_analyzer/output.rb +6 -8
- data/lib/request_log_analyzer/output/fixed_width.rb +133 -117
- data/lib/request_log_analyzer/output/html.rb +138 -60
- data/lib/request_log_analyzer/request.rb +3 -1
- data/lib/request_log_analyzer/{source/base.rb → source.rb} +5 -0
- data/lib/request_log_analyzer/source/{log_file.rb → log_parser.rb} +15 -6
- data/lib/request_log_analyzer/tracker.rb +58 -0
- data/lib/request_log_analyzer/tracker/category.rb +7 -8
- data/lib/request_log_analyzer/tracker/duration.rb +15 -12
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +8 -8
- data/lib/request_log_analyzer/tracker/timespan.rb +10 -10
- data/spec/controller_spec.rb +5 -4
- data/spec/database_inserter_spec.rb +5 -8
- data/spec/file_format_spec.rb +2 -2
- data/spec/file_formats/spec_format.rb +2 -1
- data/spec/filter_spec.rb +0 -3
- data/spec/log_parser_spec.rb +6 -6
- data/spec/merb_format_spec.rb +38 -38
- data/spec/rails_format_spec.rb +2 -2
- data/spec/request_spec.rb +2 -2
- data/spec/spec_helper.rb +3 -37
- data/tasks/github-gem.rake +2 -1
- metadata +7 -8
- data/lib/request_log_analyzer/filter/base.rb +0 -32
- data/lib/request_log_analyzer/log_parser.rb +0 -173
- data/lib/request_log_analyzer/tracker/base.rb +0 -54
@@ -1,114 +1,112 @@
|
|
1
|
-
|
1
|
+
module RequestLogAnalyzer::FileFormat
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
class RailsDevelopment < Base
|
4
|
+
|
5
|
+
# Processing EmployeeController#index (for 123.123.123.123 at 2008-07-13 06:00:00) [GET]
|
6
|
+
line_definition :processing do |line|
|
7
|
+
line.header = true # this line is the first log line for a request
|
8
|
+
line.teaser = /Processing /
|
9
|
+
line.regexp = /Processing ((?:\w+::)?\w+)#(\w+)(?: to (\w+))? \(for (\d+\.\d+\.\d+\.\d+) at (\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)\) \[([A-Z]+)\]/
|
10
|
+
line.captures << { :name => :controller, :type => :string } \
|
11
|
+
<< { :name => :action, :type => :string } \
|
12
|
+
<< { :name => :format, :type => :string } \
|
13
|
+
<< { :name => :ip, :type => :string, :anonymize => :ip } \
|
14
|
+
<< { :name => :timestamp, :type => :timestamp, :anonymize => :slightly } \
|
15
|
+
<< { :name => :method, :type => :string }
|
16
|
+
end
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
# Filter chain halted as [#<ActionController::Caching::Actions::ActionCacheFilter:0x2a999ad620 @check=nil, @options={:store_options=>{}, :layout=>nil, :cache_path=>#<Proc:0x0000002a999b8890@/app/controllers/cached_controller.rb:8>}>] rendered_or_redirected.
|
19
|
+
line_definition :cache_hit do |line|
|
20
|
+
line.regexp = /Filter chain halted as \[\#<ActionController::Caching::Actions::ActionCacheFilter:.+>\] rendered_or_redirected/
|
21
|
+
end
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
# Rendered layouts/_footer (2.9ms)
|
24
|
+
line_definition :rendered do |line|
|
25
|
+
line.teaser = /Rendered /
|
26
|
+
line.regexp = /Rendered (\w+(?:\/\w+)+) \((\d+\.\d+)ms\)/
|
27
|
+
line.captures << { :name => :render_file, :type => :string } \
|
28
|
+
<< { :name => :render_duration, :type => :msec }
|
29
|
+
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
# [4;36;1mUser Load (0.4ms)[0m [0;1mSELECT * FROM `users` WHERE (`users`.`id` = 18205844) [0m
|
32
|
+
line_definition :query_executed do |line|
|
33
|
+
line.regexp = /\s+(?:\e\[4;36;1m)?((?:\w+::)*\w+) Load \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0;1m)?(.+) (?:\e\[0m)?/
|
34
|
+
line.captures << { :name => :query_class, :type => :string } \
|
35
|
+
<< { :name => :query_duration, :type => :msec } \
|
36
|
+
<< { :name => :query_sql, :type => :string }
|
37
|
+
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
# [4;35;1mCACHE (0.0ms)[0m [0mSELECT * FROM `users` WHERE (`users`.`id` = 0) [0m
|
40
|
+
line_definition :query_cached do |line|
|
41
|
+
line.teaser = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?(.+) (?:\e\[0m)?/
|
42
|
+
line.regexp = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?(.+) (?:\e\[0m)?/
|
43
|
+
line.captures << { :name => :cached_duration, :type => :msec } \
|
44
|
+
<< { :name => :cached_sql, :type => :string }
|
45
|
+
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
47
|
+
# RuntimeError (Cannot destroy employee): /app/models/employee.rb:198:in `before_destroy'
|
48
|
+
line_definition :failed do |line|
|
49
|
+
line.footer = true
|
50
|
+
line.regexp = /((?:[A-Z]\w+\:\:)*[A-Z]\w+) \((.*)\)(?: on line #(\d+) of .+)?\:(.*)/
|
51
|
+
line.captures << { :name => :error, :type => :string } \
|
52
|
+
<< { :name => :message, :type => :string } \
|
53
|
+
<< { :name => :line, :type => :integer } \
|
54
|
+
<< { :name => :file, :type => :string } \
|
55
|
+
<< { :name => :stack_trace, :type => :string, :anonymize => true }
|
56
|
+
end
|
55
57
|
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
59
|
+
# Rails < 2.1 completed line example
|
60
|
+
# Completed in 0.21665 (4 reqs/sec) | Rendering: 0.00926 (4%) | DB: 0.00000 (0%) | 200 OK [http://demo.nu/employees]
|
61
|
+
RAILS_21_COMPLETED = /Completed in (\d+\.\d{5}) \(\d+ reqs\/sec\) (?:\| Rendering: (\d+\.\d{5}) \(\d+\%\) )?(?:\| DB: (\d+\.\d{5}) \(\d+\%\) )?\| (\d\d\d).+\[(http.+)\]/
|
60
62
|
|
61
|
-
|
62
|
-
|
63
|
-
|
63
|
+
# Rails > 2.1 completed line example
|
64
|
+
# Completed in 614ms (View: 120, DB: 31) | 200 OK [http://floorplanner.local/demo]
|
65
|
+
RAILS_22_COMPLETED = /Completed in (\d+)ms \((?:View: (\d+), )?DB: (\d+)\) \| (\d\d\d).+\[(http.+)\]/
|
64
66
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
67
|
+
# The completed line uses a kind of hack to ensure that both old style logs and new style logs
|
68
|
+
# are both parsed by the same regular expression. The format in Rails 2.2 was slightly changed,
|
69
|
+
# but the line contains exactly the same information.
|
70
|
+
line_definition :completed do |line|
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
-
|
72
|
+
line.footer = true
|
73
|
+
line.teaser = /Completed in /
|
74
|
+
line.regexp = Regexp.new("(?:#{RAILS_21_COMPLETED}|#{RAILS_22_COMPLETED})")
|
73
75
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
76
|
+
line.captures << { :name => :duration, :type => :sec, :anonymize => :slightly } \
|
77
|
+
<< { :name => :view, :type => :sec, :anonymize => :slightly } \
|
78
|
+
<< { :name => :db, :type => :sec, :anonymize => :slightly } \
|
79
|
+
<< { :name => :status, :type => :integer } \
|
80
|
+
<< { :name => :url, :type => :string, :anonymize => :url } # Old variant
|
79
81
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
82
|
+
line.captures << { :name => :duration, :type => :msec, :anonymize => :slightly } \
|
83
|
+
<< { :name => :view, :type => :msec, :anonymize => :slightly } \
|
84
|
+
<< { :name => :db, :type => :msec, :anonymize => :slightly } \
|
85
|
+
<< { :name => :status, :type => :integer} \
|
86
|
+
<< { :name => :url, :type => :string, :anonymize => :url } # 2.2 variant
|
87
|
+
end
|
86
88
|
|
89
|
+
REQUEST_CATEGORIZER = Proc.new do |request|
|
90
|
+
format = request[:format] || 'html'
|
91
|
+
"#{request[:controller]}##{request[:action]}.#{format} [#{request[:method]}]"
|
92
|
+
end
|
87
93
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
report do |analyze|
|
95
|
-
analyze.timespan :line_type => :processing
|
96
|
-
analyze.category :category => REQUEST_CATEGORIZER, :title => 'Top 20 hits', :amount => 20, :line_type => :processing
|
97
|
-
analyze.category :method, :title => 'HTTP methods'
|
98
|
-
analyze.category :status, :title => 'HTTP statuses returned'
|
99
|
-
analyze.category :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
|
94
|
+
report do |analyze|
|
95
|
+
analyze.timespan :line_type => :processing
|
96
|
+
analyze.category :category => REQUEST_CATEGORIZER, :title => 'Top 20 hits', :amount => 20, :line_type => :processing
|
97
|
+
analyze.category :method, :title => 'HTTP methods'
|
98
|
+
analyze.category :status, :title => 'HTTP statuses returned'
|
99
|
+
analyze.category :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
|
100
100
|
|
101
|
-
|
102
|
-
|
103
|
-
|
101
|
+
analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
|
102
|
+
analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
|
103
|
+
analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
|
104
104
|
|
105
|
-
|
106
|
-
|
105
|
+
analyze.category :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
|
106
|
+
:if => lambda { |request| request[:duration] && request[:duration] > 1.0 }, :amount => 20
|
107
107
|
|
108
|
-
|
109
|
-
|
108
|
+
analyze.hourly_spread :line_type => :processing
|
109
|
+
analyze.category :error, :title => 'Failed requests', :line_type => :failed, :amount => 20
|
110
|
+
end
|
110
111
|
end
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
end
|
112
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module RequestLogAnalyzer::Filter
|
2
|
+
|
3
|
+
# Filter class loader using const_missing
|
4
|
+
# This function will automatically load the class file based on the name of the class
|
5
|
+
def self.const_missing(const)
|
6
|
+
RequestLogAnalyzer::load_default_class_file(self, const)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Base filter class used to filter input requests.
|
10
|
+
# All filters should interit from this base.
|
11
|
+
class Base
|
12
|
+
|
13
|
+
include RequestLogAnalyzer::FileFormat::Awareness
|
14
|
+
|
15
|
+
attr_reader :log_parser
|
16
|
+
attr_reader :options
|
17
|
+
|
18
|
+
# Initializer
|
19
|
+
# <tt>format</tt> The file format
|
20
|
+
# <tt>options</tt> Are passed to the filters.
|
21
|
+
def initialize(format, options = {})
|
22
|
+
@options = options
|
23
|
+
register_file_format(format)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Initialize the filter
|
27
|
+
def prepare
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return the request if the request should be kept.
|
31
|
+
# Return nil otherwise.
|
32
|
+
def filter(request)
|
33
|
+
return nil unless request
|
34
|
+
return request
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -1,15 +1,13 @@
|
|
1
|
-
module RequestLogAnalyzer
|
1
|
+
module RequestLogAnalyzer::Output
|
2
|
+
|
3
|
+
def self.const_missing(const)
|
4
|
+
RequestLogAnalyzer::load_default_class_file(self, const)
|
5
|
+
end
|
2
6
|
|
3
|
-
class
|
7
|
+
class Base
|
4
8
|
|
5
9
|
attr_accessor :io, :options, :style
|
6
10
|
|
7
|
-
def self.const_missing(const)
|
8
|
-
filename = const.to_s.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
|
9
|
-
require File.dirname(__FILE__) + '/output/' + filename
|
10
|
-
self.const_get(const)
|
11
|
-
end
|
12
|
-
|
13
11
|
def initialize(io, options = {})
|
14
12
|
@io = io
|
15
13
|
@options = options
|
@@ -1,158 +1,174 @@
|
|
1
|
-
|
1
|
+
module RequestLogAnalyzer::Output
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
class FixedWidth < Base
|
4
|
+
|
5
|
+
module Monochrome
|
6
|
+
def colorize(text, *options)
|
7
|
+
text
|
8
|
+
end
|
6
9
|
end
|
7
|
-
end
|
8
10
|
|
9
|
-
|
11
|
+
module Color
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
+
STYLES = { :normal => 0, :bold => 1, :underscore => 4, :blink => 5, :inverse => 7, :concealed => 8 }
|
14
|
+
COLORS = { :black => 0, :blue => 4, :green => 2, :cyan => 6, :red => 1, :purple => 5, :brown => 3, :white => 7 }
|
13
15
|
|
14
|
-
|
16
|
+
def colorize(text, *options)
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
18
|
+
font_style = ''
|
19
|
+
foreground_color = '0'
|
20
|
+
background_color = ''
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
22
|
+
options.each do |option|
|
23
|
+
if option.kind_of?(Symbol)
|
24
|
+
foreground_color = "3#{COLORS[option]}" if COLORS.include?(option)
|
25
|
+
font_style = "#{STYLES[option]};" if STYLES.include?(option)
|
26
|
+
elsif option.kind_of?(Hash)
|
27
|
+
options.each do |key, value|
|
28
|
+
case key
|
29
|
+
when :color; foreground_color = "3#{COLORS[value]}" if COLORS.include?(value)
|
30
|
+
when :background; background_color = "4#{COLORS[value]};" if COLORS.include?(value)
|
31
|
+
when :on; background_color = "4#{COLORS[value]};" if COLORS.include?(value)
|
32
|
+
when :style; font_style = "#{STYLES[value]};" if STYLES.include?(value)
|
33
|
+
end
|
31
34
|
end
|
32
35
|
end
|
33
36
|
end
|
37
|
+
return "\e[#{background_color}#{font_style}#{foreground_color}m#{text}\e[0m"
|
34
38
|
end
|
35
|
-
return "\e[#{background_color}#{font_style}#{foreground_color}m#{text}\e[0m"
|
36
|
-
end
|
37
39
|
|
38
|
-
|
40
|
+
end
|
39
41
|
|
40
|
-
|
42
|
+
attr_reader :characters
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
CHARACTERS = {
|
45
|
+
:ascii => { :horizontal_line => '-', :vertical_line => '|', :block => '=' },
|
46
|
+
:utf => { :horizontal_line => '━', :vertical_line => '┃', :block => '░' }
|
47
|
+
}
|
46
48
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
def initialize(io, options = {})
|
50
|
+
super(io, options)
|
51
|
+
@options[:width] ||= 80
|
52
|
+
@options[:characters] ||= :utf
|
53
|
+
@characters = CHARACTERS[@options[:characters]]
|
52
54
|
|
53
|
-
|
54
|
-
|
55
|
-
|
55
|
+
color_module = @options[:color] ? Color : Monochrome
|
56
|
+
(class << self; self; end).send(:include, color_module)
|
57
|
+
end
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
59
|
+
def print(str)
|
60
|
+
@io << str
|
61
|
+
end
|
60
62
|
|
61
|
-
|
63
|
+
alias :<< :print
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
-
|
65
|
+
def puts(str = '')
|
66
|
+
@io << str << "\n"
|
67
|
+
end
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
69
|
+
def title(title)
|
70
|
+
puts
|
71
|
+
puts colorize(title, :bold, :white)
|
72
|
+
line(:green)
|
73
|
+
end
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
|
75
|
+
def line(*font)
|
76
|
+
puts colorize(characters[:horizontal_line] * @options[:width], *font)
|
77
|
+
end
|
76
78
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
79
|
+
def link(text, url = nil)
|
80
|
+
if url.nil?
|
81
|
+
colorize(text, :blue, :bold)
|
82
|
+
else
|
83
|
+
"#{text} (#{colorize(url, :blue, :bold)})"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def header
|
88
|
+
if io.kind_of?(File)
|
89
|
+
puts "Request-log-analyzer summary report"
|
90
|
+
line
|
91
|
+
puts "Version #{RequestLogAnalyzer::VERSION} - written by Willem van Bergen and Bart ten Brinke"
|
92
|
+
puts "Request-log-analyzer website: http://github.com/wvanbergen/request-log-analyzer"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def footer
|
97
|
+
puts
|
98
|
+
puts "Thanks for using request-log-analyzer!"
|
82
99
|
end
|
83
|
-
end
|
84
100
|
|
85
|
-
|
101
|
+
def table(*columns, &block)
|
86
102
|
|
87
|
-
|
88
|
-
|
103
|
+
rows = Array.new
|
104
|
+
yield(rows)
|
89
105
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
106
|
+
# determine maximum cell widths
|
107
|
+
max_cell_widths = rows.inject(Array.new(columns.length, 0)) do |result, row|
|
108
|
+
lengths = row.map { |column| column.to_s.length }
|
109
|
+
result.each_with_index { |length, index| result[index] = ([length, lengths[index]].max rescue length) }
|
110
|
+
end
|
111
|
+
columns.each_with_index { |col, index| col[:actual_width] ||= max_cell_widths[index] }
|
96
112
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
113
|
+
# determine actual column width
|
114
|
+
column_widths = columns.map do |column|
|
115
|
+
if column[:width] == :rest
|
116
|
+
nil
|
117
|
+
elsif column[:width]
|
118
|
+
column[:width]
|
119
|
+
elsif column[:min_width]
|
120
|
+
[column[:min_width], column[:actual_width]].max
|
121
|
+
elsif column[:max_width]
|
122
|
+
[column[:max_width], column[:actual_width]].min
|
123
|
+
else
|
124
|
+
column[:actual_width]
|
125
|
+
end
|
109
126
|
end
|
110
|
-
end
|
111
127
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
end
|
116
|
-
|
117
|
-
# Print table header
|
118
|
-
if table_has_header?(columns)
|
119
|
-
column_titles = []
|
120
|
-
columns.each_with_index do |column, index|
|
121
|
-
width = column_widths[index]
|
122
|
-
alignment = (column[:align] == :right ? '' : '-')
|
123
|
-
column_titles.push(colorize("%#{alignment}#{width}s" % column[:title].to_s[0...width], :bold))
|
128
|
+
if column_widths.include?(nil)
|
129
|
+
width_left = options[:width] - ((columns.length - 1) * (style[:cell_separator] ? 3 : 1)) - column_widths.compact.inject(0) { |sum, col| sum + col}
|
130
|
+
column_widths[column_widths.index(nil)] = width_left
|
124
131
|
end
|
132
|
+
|
133
|
+
# Print table header
|
134
|
+
if table_has_header?(columns)
|
135
|
+
column_titles = []
|
136
|
+
columns.each_with_index do |column, index|
|
137
|
+
width = column_widths[index]
|
138
|
+
alignment = (column[:align] == :right ? '' : '-')
|
139
|
+
column_titles.push(colorize("%#{alignment}#{width}s" % column[:title].to_s[0...width], :bold))
|
140
|
+
end
|
125
141
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
end
|
142
|
+
puts column_titles.join(style[:cell_separator] ? " #{characters[:vertical_line]} " : ' ')
|
143
|
+
line(:green)
|
144
|
+
end
|
130
145
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
146
|
+
rows.each do |row|
|
147
|
+
row_values = []
|
148
|
+
columns.each_with_index do |column, index|
|
149
|
+
width = column_widths[index]
|
150
|
+
case column[:type]
|
151
|
+
when :ratio
|
152
|
+
if width > 4
|
153
|
+
if column[:treshold] && column[:treshold] < row[index].to_f
|
154
|
+
bar = ''
|
155
|
+
bar << characters[:block] * (width.to_f * column[:treshold]).round
|
156
|
+
bar << colorize(characters[:block] * (width.to_f * (row[index].to_f - column[:treshold])).round, :red)
|
157
|
+
row_values.push(bar)
|
158
|
+
else
|
159
|
+
row_values.push(characters[:block] * (width.to_f * row[index].to_f).round)
|
160
|
+
end
|
143
161
|
else
|
144
|
-
row_values.push(
|
162
|
+
row_values.push('')
|
145
163
|
end
|
146
164
|
else
|
147
|
-
|
165
|
+
alignment = (columns[index][:align] == :right ? '' : '-')
|
166
|
+
row_values.push("%#{alignment}#{width}s" % row[index].to_s[0...width])
|
148
167
|
end
|
149
|
-
else
|
150
|
-
alignment = (columns[index][:align] == :right ? '' : '-')
|
151
|
-
row_values.push("%#{alignment}#{width}s" % row[index].to_s[0...width])
|
152
168
|
end
|
169
|
+
puts row_values.join(style[:cell_separator] ? " #{characters[:vertical_line]} " : ' ')
|
153
170
|
end
|
154
|
-
puts row_values.join(style[:cell_separator] ? " #{characters[:vertical_line]} " : ' ')
|
155
171
|
end
|
156
|
-
end
|
157
172
|
|
173
|
+
end
|
158
174
|
end
|