logbox 0.2.10
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/.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
|