logbox 0.2.10
Sign up to get free protection for your applications and to get access to all the features.
- data/.bundle/config +3 -0
- data/.rvmrc +2 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +30 -0
- data/README +14 -0
- data/Rakefile +74 -0
- data/VERSION +1 -0
- data/bin/download_logs +20 -0
- data/bin/obsstats +39 -0
- data/bin/rotate +17 -0
- data/bin/viewobs +198 -0
- data/lib/logbox.rb +9 -0
- data/lib/logbox/ansi_colors.rb +28 -0
- data/lib/logbox/log_parser.rb +79 -0
- data/lib/logbox/mockup_log.rb +44 -0
- data/lib/logbox/observation.rb +162 -0
- data/lib/logbox/observation_compiler.rb +311 -0
- data/lib/logbox/observation_mover.rb +142 -0
- data/lib/logbox/stream_wrapper.rb +20 -0
- data/lib/logbox/stream_wrapper/gzip_multi_file.rb +90 -0
- data/lib/logbox/stream_wrapper/observation_filter.rb +113 -0
- data/lib/logbox/stream_wrapper/order_blob_splitter.rb +96 -0
- data/lib/setup_environment.rb +15 -0
- data/logbox.gemspec +110 -0
- data/test/bin_viewobs_test.rb +42 -0
- data/test/fixtures/aws_keys_yaml.txt +3 -0
- data/test/fixtures/double-obs.log +1 -0
- data/test/fixtures/error_line.log +1 -0
- data/test/fixtures/log-for-md5.log +1 -0
- data/test/fixtures/log0.log +0 -0
- data/test/fixtures/log1.log +1 -0
- data/test/fixtures/log1.log.gz +0 -0
- data/test/fixtures/log2.log +2 -0
- data/test/fixtures/log2.log.gz +0 -0
- data/test/fixtures/log_invalid_mixed_encoding.log +1 -0
- data/test/fixtures/observation_filter.log +5 -0
- data/test/fixtures/unquoted_ugliness.log +2 -0
- data/test/log_parser_test.rb +84 -0
- data/test/observation_compiler_test.rb +216 -0
- data/test/observation_mover_test.rb +135 -0
- data/test/observation_test.rb +114 -0
- data/test/stream_wrapper/gzip_multi_file_test.rb +147 -0
- data/test/stream_wrapper/observation_filter_test.rb +171 -0
- data/test/stream_wrapper/order_blob_splitter_test.rb +129 -0
- data/test/test_helper.rb +23 -0
- metadata +177 -0
data/.bundle/config
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem 'right_aws', '~>2.0'
|
4
|
+
gem 'single_instance'
|
5
|
+
gem 'rake'
|
6
|
+
|
7
|
+
group :test do
|
8
|
+
gem 'shoulda', '2.10.3'
|
9
|
+
gem 'mocha', '0.9.8'
|
10
|
+
end
|
11
|
+
|
12
|
+
# required for jeweler
|
13
|
+
group :development do
|
14
|
+
gem "bundler", "~> 1.0.0"
|
15
|
+
gem "jeweler", "~> 1.5.1"
|
16
|
+
gem "rcov", ">= 0"
|
17
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
git (1.2.5)
|
5
|
+
jeweler (1.5.1)
|
6
|
+
bundler (~> 1.0.0)
|
7
|
+
git (>= 1.2.5)
|
8
|
+
rake
|
9
|
+
mocha (0.9.8)
|
10
|
+
rake
|
11
|
+
rake (0.8.7)
|
12
|
+
rcov (0.9.9)
|
13
|
+
right_aws (2.0.0)
|
14
|
+
right_http_connection (>= 1.2.1)
|
15
|
+
right_http_connection (1.2.4)
|
16
|
+
shoulda (2.10.3)
|
17
|
+
single_instance (0.2.0)
|
18
|
+
|
19
|
+
PLATFORMS
|
20
|
+
ruby
|
21
|
+
|
22
|
+
DEPENDENCIES
|
23
|
+
bundler (~> 1.0.0)
|
24
|
+
jeweler (~> 1.5.1)
|
25
|
+
mocha (= 0.9.8)
|
26
|
+
rake
|
27
|
+
rcov
|
28
|
+
right_aws (~> 2.0)
|
29
|
+
shoulda (= 2.10.3)
|
30
|
+
single_instance
|
data/README
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
== A Toolbox for Logs and Observations
|
2
|
+
|
3
|
+
Two main uses:
|
4
|
+
|
5
|
+
1. Use the tools in bin to download and view logs etc.
|
6
|
+
2. Include logbox.rb and use the code directly.
|
7
|
+
|
8
|
+
StreamWrapper, LogParser and Observation are the main entry points to the code.
|
9
|
+
|
10
|
+
== Development and testing
|
11
|
+
|
12
|
+
Running `rake` will run the test suite for ruby 1.8.7 and 1.9.2.
|
13
|
+
|
14
|
+
Run `rake setup` to generate shell commands that will enable you to run all the tests.
|
data/Rakefile
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "logbox"
|
16
|
+
gem.executables = %w(download_logs obsstats rotate viewobs)
|
17
|
+
gem.homepage = "https://github.com/icehouse/logbox"
|
18
|
+
gem.license = "MIT"
|
19
|
+
gem.summary = %Q{A Toolbox for Logs and Observations}
|
20
|
+
gem.description = %Q{Log-related code and tools that are used cross different applications}
|
21
|
+
gem.email = "dev@icehouse.se"
|
22
|
+
gem.authors = ["dvrensk", "enarsson", "Jell"]
|
23
|
+
gem.files.include(["lib/*"])
|
24
|
+
# Include your dependencies below. Runtime dependencies are required when using your gem,
|
25
|
+
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
|
26
|
+
# gem.add_runtime_dependency 'jabber4r', '> 0.1'
|
27
|
+
# gem.add_development_dependency 'rspec', '> 1.2.3'
|
28
|
+
end
|
29
|
+
Jeweler::RubygemsDotOrgTasks.new
|
30
|
+
|
31
|
+
require 'rake/testtask'
|
32
|
+
Rake::TestTask.new(:test) do |test|
|
33
|
+
test.libs << 'lib' << 'test'
|
34
|
+
test.pattern = 'test/**/*_test.rb'
|
35
|
+
test.verbose = true
|
36
|
+
end
|
37
|
+
|
38
|
+
require 'rcov/rcovtask'
|
39
|
+
Rcov::RcovTask.new do |test|
|
40
|
+
test.libs << 'test'
|
41
|
+
test.pattern = 'test/**/test_*.rb'
|
42
|
+
test.verbose = true
|
43
|
+
end
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "logbox #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
54
|
+
|
55
|
+
RUBIES = %w[1.8.7-p302 1.9.2-p0]
|
56
|
+
desc "Run tests with ruby 1.8.7 and 1.9.2"
|
57
|
+
task :default do
|
58
|
+
RUBIES.each do |ruby|
|
59
|
+
sh "rvm #{ruby}@logbox rake test"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Install bundler and gems for each environment in #{RUBIES.join','}"
|
64
|
+
task :setup do
|
65
|
+
cmd = []
|
66
|
+
RUBIES.each do |ruby|
|
67
|
+
cmd << "rvm --create #{ruby}@logbox"
|
68
|
+
cmd << "gem list | grep -q ^rake || gem install rake"
|
69
|
+
cmd << "gem list | grep -q ^bundler || gem install bundler"
|
70
|
+
cmd << "bundle install"
|
71
|
+
end
|
72
|
+
puts "# RUN THE FOLLOWING LINES IN YOUR SHELL"
|
73
|
+
puts cmd
|
74
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.10
|
data/bin/download_logs
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require 'setup_environment'
|
4
|
+
require 'optparse'
|
5
|
+
require 'observation_compiler'
|
6
|
+
|
7
|
+
option_parser = OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: download_logs number_of_days\n" +
|
9
|
+
"Downloads logs from all log servers and merge them to one log file per day. Observations are ordered."
|
10
|
+
end
|
11
|
+
|
12
|
+
ENV['OBSENTER_S3_KEY'] ||= "AKIAI6RLB45ZDVINPZ3Q"
|
13
|
+
|
14
|
+
if ARGV.size == 1 && ARGV[0] =~ /^\d+$/
|
15
|
+
start_date = Date.today - ARGV[0].to_i + 1
|
16
|
+
job = ObservationCompiler::Job.new(:processed_logs_path => File.exist?("/apps/observation_logs") ? "/apps/observation_logs" : "local_files")
|
17
|
+
job.fetch_and_merge(start_date..Date.today)
|
18
|
+
else
|
19
|
+
puts option_parser
|
20
|
+
end
|
data/bin/obsstats
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require 'setup_environment'
|
4
|
+
require 'observation'
|
5
|
+
require 'stream_wrapper'
|
6
|
+
require 'ansi_colors'
|
7
|
+
require 'set'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
filters = {
|
11
|
+
:valid => true,
|
12
|
+
:skip_debug_observations => true
|
13
|
+
}
|
14
|
+
observations = StreamWrapper.open(ARGV, filters)
|
15
|
+
|
16
|
+
shops = Hash.new do |h,k|
|
17
|
+
h2 = Hash.new(0)
|
18
|
+
h2[:u_items] = Set.new
|
19
|
+
h2[:u_users] = Set.new
|
20
|
+
h[k] = h2
|
21
|
+
end
|
22
|
+
|
23
|
+
observations.each do |obs|
|
24
|
+
stats = shops["#{obs[:account_id]}/#{obs[:shop_id]}"]
|
25
|
+
stats[obs.type] += 1
|
26
|
+
stats[:u_items] << obs[:item_id] if obs[:item_id]
|
27
|
+
stats[:u_users] << obs[:user_id] if obs[:user_id]
|
28
|
+
end
|
29
|
+
|
30
|
+
shops.each_pair do |k,stats|
|
31
|
+
[:u_items, :u_users].each do |u|
|
32
|
+
stats[u] = stats[u].size
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
shops.keys.sort.each do |shop_id|
|
37
|
+
stats = shops[shop_id]
|
38
|
+
puts stats.to_yaml.sub('---', "*** #{shop_id}")
|
39
|
+
end
|
data/bin/rotate
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require 'logbox/observation_mover'
|
4
|
+
|
5
|
+
if ARGV.first == '--act-as-fake-nginx'
|
6
|
+
ObservationMover.act_as_fake_nginx
|
7
|
+
else
|
8
|
+
ObservationMover.new(*ARGV).run
|
9
|
+
end
|
10
|
+
|
11
|
+
# To test, run with
|
12
|
+
# rm -rf testlogs
|
13
|
+
# bin/rotate --act-as-fake-nginx &
|
14
|
+
# and then
|
15
|
+
# bin/rotate testlogs/test.log $(pwd)/test.pid
|
16
|
+
# ls -l testlogs
|
17
|
+
# Repeat.
|
data/bin/viewobs
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require 'setup_environment'
|
4
|
+
require 'log_parser'
|
5
|
+
require 'observation'
|
6
|
+
require 'ansi_colors'
|
7
|
+
require 'stream_wrapper'
|
8
|
+
|
9
|
+
|
10
|
+
# We don't need a stack dump when interrupted with Ctrl-C
|
11
|
+
trap("INT", "EXIT")
|
12
|
+
|
13
|
+
# Make symbols sortable.
|
14
|
+
class Symbol
|
15
|
+
def <=>(other)
|
16
|
+
self.to_s <=> other.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Viewobs
|
21
|
+
attr_accessor :options
|
22
|
+
|
23
|
+
def parse_options
|
24
|
+
require 'optparse'
|
25
|
+
options = { :delimiter => "\t" }
|
26
|
+
option_parser = OptionParser.new do |opts|
|
27
|
+
opts.banner = "Usage: viewobs.rb [options] files"
|
28
|
+
opts.separator ""
|
29
|
+
opts.separator "Observation filters:"
|
30
|
+
opts.on("-i IP", "--ip IP", "Filter on IP.") { |v| options[:filter_ip] = v }
|
31
|
+
opts.on("-s SHOP_ID", "--shop SHOP_ID", "Filter on shop ID.") { |v| options[:filter_shop_id] = v }
|
32
|
+
opts.on("-a ACCOUNT_ID", "--account ACCOUNT_ID", "Filter on account ID.") { |v| options[:filter_account_id] = v }
|
33
|
+
opts.on("-t TYPE", "--type TYPE", "Filter on observation type.") { |v| options[:filter_type] = v }
|
34
|
+
opts.on("-v", "--valid", "Display only valid observations.") { |v| options[:filter_valid] = v }
|
35
|
+
opts.separator ""
|
36
|
+
opts.separator "Display filters:"
|
37
|
+
opts.on("-A", "--all", "Display all attributes.") { |v| options[:all_attributes] = v }
|
38
|
+
opts.on("-u", "--user", "Display user set attributes.") { |v| options[:user_attributes] = v }
|
39
|
+
opts.on("-o", "--observation", "Display observation attributes.") { |v| options[:observation_attributes] = v }
|
40
|
+
opts.on("-e", "--errors", "Display errors (if any).") { |v| options[:errors] = v }
|
41
|
+
opts.on("-r", "--request", "Display raw request.") { |v| options[:request] = v }
|
42
|
+
opts.separator ""
|
43
|
+
opts.separator "Other options:"
|
44
|
+
opts.on("-f", "--follow", "Waits at the end of the log for updates.") { |v| options[:follow] = v }
|
45
|
+
opts.on("-c", "--colorize", "Colorize output.") { |v| String.colorize }
|
46
|
+
opts.on("-T", "--table", "Output as table or csv.", "Does not work with data from stdin (reads twice).") { options[:table] = true }
|
47
|
+
opts.on("-d CHAR", "--delimiter CHAR", "Delimiter for table (default is tab).") { |v| options[:delimiter] = v[0,1] }
|
48
|
+
opts.separator ""
|
49
|
+
end
|
50
|
+
|
51
|
+
begin
|
52
|
+
# Parse will leave any filename argument intact in ARGV but it will remove all options.
|
53
|
+
option_parser.parse!(ARGV)
|
54
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
55
|
+
puts e
|
56
|
+
puts option_parser
|
57
|
+
exit 1
|
58
|
+
end
|
59
|
+
@options = options
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def view_line(attributes, observation)
|
64
|
+
# Print basic information.
|
65
|
+
out = ""
|
66
|
+
out << "#{observation.attributes[:account_id].to_s.red}" if observation.attributes[:account_id]
|
67
|
+
out << " - #{observation.attributes[:shop_id].to_s.red}" if observation.attributes[:shop_id]
|
68
|
+
out << " - #{attributes[:o].blue}" if attributes[:o] # Observation type.
|
69
|
+
out << " - #{attributes[:timestamp].strftime('%Y-%m-%d %H.%M.%S')}"
|
70
|
+
out << " - #{attributes[:ip]}"
|
71
|
+
out << " - #{attributes[:status]}"
|
72
|
+
out << " - #{'valid'.green}" if observation.valid?
|
73
|
+
puts out
|
74
|
+
|
75
|
+
# Print extended information depending on options.
|
76
|
+
if options[:all_attributes]
|
77
|
+
attributes.keys.sort.each do |key|
|
78
|
+
puts " #{key.to_s.blue}: #{attributes[key]}" unless [:ip, :request, :timestamp, :status].include?(key)
|
79
|
+
end
|
80
|
+
puts
|
81
|
+
end
|
82
|
+
if options[:user_attributes]
|
83
|
+
attributes.keys.sort.each do |key|
|
84
|
+
puts " #{key.to_s[1..-1].blue}: #{attributes[key]}" if key.to_s[0..0] == '_'
|
85
|
+
end
|
86
|
+
puts
|
87
|
+
end
|
88
|
+
if options[:observation_attributes]
|
89
|
+
observation.attributes.keys.sort.each do |key|
|
90
|
+
puts " #{key.to_s.blue}: #{observation.attributes[key]}" if observation.attributes.has_key?(key) && key != :request
|
91
|
+
end
|
92
|
+
puts
|
93
|
+
end
|
94
|
+
if options[:errors]
|
95
|
+
puts observation.errors.inspect unless observation.valid?
|
96
|
+
puts
|
97
|
+
end
|
98
|
+
if options[:request]
|
99
|
+
puts attributes[:request]
|
100
|
+
puts
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def view_table_line(attributes, observation)
|
105
|
+
# Print basic information.
|
106
|
+
out = []
|
107
|
+
out << observation.attributes[:account_id].to_s.red
|
108
|
+
out << observation.attributes[:shop_id].to_s.red
|
109
|
+
out << attributes[:o].blue # Observation type.
|
110
|
+
out << attributes[:timestamp].strftime('%Y-%m-%d %H.%M.%S')
|
111
|
+
out << attributes[:ip]
|
112
|
+
out << attributes[:status]
|
113
|
+
out << (observation.valid? ? 'valid' : 'invalid')
|
114
|
+
|
115
|
+
@all_keys.each do |key|
|
116
|
+
out << attributes[key] unless [:o, :ip, :request, :timestamp, :status].include?(key)
|
117
|
+
end
|
118
|
+
|
119
|
+
puts out.map { |s| s.to_s.tr("#{delimiter}\0- "," ") }.join(delimiter)
|
120
|
+
end
|
121
|
+
|
122
|
+
def view_until_end
|
123
|
+
# gets will take lines from any filename given in ARGV or from stdin.
|
124
|
+
pingdom_re = /pingdom/
|
125
|
+
filters = {
|
126
|
+
:account_id => options[:filter_account_id],
|
127
|
+
:shop_id => options[:filter_shop_id],
|
128
|
+
:observation_type => options[:filter_type]
|
129
|
+
}
|
130
|
+
stream = StreamWrapper.open(ARGV, filters)
|
131
|
+
while line = stream.gets
|
132
|
+
attributes = LogParser.parse_line(line)
|
133
|
+
observation = Observation.new(attributes)
|
134
|
+
|
135
|
+
# Skip lines from pingdom.
|
136
|
+
next if attributes[:request].match(pingdom_re)
|
137
|
+
|
138
|
+
# Filter depending on options.
|
139
|
+
if options[:filter_ip]
|
140
|
+
next unless attributes[:ip].match(options[:filter_ip])
|
141
|
+
end
|
142
|
+
if options[:filter_valid]
|
143
|
+
next unless observation.valid?
|
144
|
+
end
|
145
|
+
|
146
|
+
begin
|
147
|
+
if options[:gathering_keys]
|
148
|
+
gather_keys(attributes, observation)
|
149
|
+
elsif options[:table]
|
150
|
+
view_table_line(attributes, observation)
|
151
|
+
else
|
152
|
+
view_line(attributes, observation)
|
153
|
+
end
|
154
|
+
rescue Exception => e
|
155
|
+
$stderr.puts "Could not render line #{$.} (#{e.message})"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
if options[:gathering_keys]
|
159
|
+
@all_keys = @all_keys.sort
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def gather_keys (attributes, observation)
|
164
|
+
@all_keys ||= []
|
165
|
+
@all_keys = @all_keys | attributes.keys
|
166
|
+
end
|
167
|
+
|
168
|
+
def print_header
|
169
|
+
print %w[account_id shop_id o timestamp ip status valid].join(delimiter)
|
170
|
+
print delimiter
|
171
|
+
puts (@all_keys - [:o, :timestamp, :ip, :status, :request]).map { |sym| sym.to_s }.join(delimiter)
|
172
|
+
end
|
173
|
+
|
174
|
+
def delimiter
|
175
|
+
options[:delimiter]
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
if File.basename($0) == File.basename(__FILE__)
|
181
|
+
viewobs = Viewobs.new
|
182
|
+
viewobs.parse_options
|
183
|
+
|
184
|
+
if viewobs.options[:follow]
|
185
|
+
while true
|
186
|
+
viewobs.view_until_end
|
187
|
+
sleep(0.1)
|
188
|
+
end
|
189
|
+
else
|
190
|
+
if viewobs.options[:table]
|
191
|
+
viewobs.options[:gathering_keys] = true
|
192
|
+
viewobs.view_until_end
|
193
|
+
viewobs.print_header
|
194
|
+
viewobs.options[:gathering_keys] = false
|
195
|
+
end
|
196
|
+
viewobs.view_until_end
|
197
|
+
end
|
198
|
+
end
|
data/lib/logbox.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
class String
|
2
|
+
|
3
|
+
types = {
|
4
|
+
:bold => "\e[1m",
|
5
|
+
:underline => "\e[4m",
|
6
|
+
:black => "\e[30m",
|
7
|
+
:red => "\e[31m",
|
8
|
+
:green => "\e[32m",
|
9
|
+
:yellow => "\e[33m",
|
10
|
+
:blue => "\e[34m",
|
11
|
+
:magenta => "\e[35m",
|
12
|
+
:cyan => "\e[36m",
|
13
|
+
:white => "\e[37m",
|
14
|
+
}
|
15
|
+
|
16
|
+
types.each do |name, color_code|
|
17
|
+
define_method(name) do
|
18
|
+
@@colorize ? "#{color_code}#{self}\e[0m" : self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
@@colorize = false
|
24
|
+
def colorize
|
25
|
+
@@colorize = true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|