chainsaw 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +57 -0
- data/Rakefile +8 -0
- data/bin/chainsaw +6 -0
- data/chainsaw.gemspec +22 -0
- data/lib/chainsaw.rb +15 -0
- data/lib/chainsaw/cli.rb +112 -0
- data/lib/chainsaw/detector.rb +91 -0
- data/lib/chainsaw/filter.rb +126 -0
- data/lib/chainsaw/format.rb +11 -0
- data/lib/chainsaw/version.rb +3 -0
- data/test/cli_test.rb +19 -0
- data/test/detector_test.rb +127 -0
- data/test/filter_test.rb +78 -0
- data/test/logs/apache_error.log +211 -0
- data/test/logs/clf.log +706 -0
- data/test/logs/django.log +14 -0
- data/test/logs/mongodb.log +722 -0
- data/test/logs/nginx_access.log +28 -0
- data/test/logs/nginx_error.log +1 -0
- data/test/logs/puppet.log +16 -0
- data/test/logs/python.log +10 -0
- data/test/logs/rack.log +4 -0
- data/test/logs/rails.log +249 -0
- data/test/logs/redis.log +4 -0
- data/test/logs/ruby_logger.log +30 -0
- data/test/logs/syslog.log +29 -0
- data/test/test_helper.rb +11 -0
- metadata +126 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 ljfauscett
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Chainsaw
|
2
|
+
|
3
|
+
Parses a log file and returns lines matching the time period provided.
|
4
|
+
|
5
|
+
Chronic is used to parse the time strings, so any format chronic
|
6
|
+
supports, chainsaw supports. A list of supported formats can
|
7
|
+
be found here: https://github.com/mojombo/chronic
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
gem install chainsaw
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
> chainsaw access.log 1 hour ago # entries from one hour ago to now
|
16
|
+
> chainsaw access.log august # entries from August to now
|
17
|
+
> chainsaw access.log 2012-08-06 # entries from August 6th to now
|
18
|
+
> chainsaw access.log 2012-08-27 10:00 # entries from August 27th at 10:00 to now
|
19
|
+
|
20
|
+
# You can use a hypen to specify a time range (you can mix and match formats)
|
21
|
+
|
22
|
+
> chainsaw access.log 2012-08-01 - 2012-09-17 # entries within August 1st and September 17th
|
23
|
+
> chainsaw access.log august - yesterday # entries within August and September
|
24
|
+
|
25
|
+
## Features
|
26
|
+
|
27
|
+
### Additional text filter
|
28
|
+
|
29
|
+
You can specify an additional simple text pattern to filter (in addition to the time arguments) with the `-f` option.
|
30
|
+
|
31
|
+
> chainsaw access.log -f GET 1 hour ago
|
32
|
+
|
33
|
+
### Colorize output
|
34
|
+
|
35
|
+
You can have chainsaw colorize the timestamp for easy scanning with the `-c` option.
|
36
|
+
|
37
|
+
> chainsaw access.log -c 1 hour ago
|
38
|
+
|
39
|
+
### Interactive mode
|
40
|
+
|
41
|
+
If you want to print a line and wait for input (press return) before moving to the next found line, you can use the `-i` option.
|
42
|
+
|
43
|
+
> chainsaw access.log -i 1 hour ago
|
44
|
+
|
45
|
+
### Output to a file
|
46
|
+
|
47
|
+
Chainsaw will output the found log lines to a file on the system if you specify with `-o FILENAME`.
|
48
|
+
|
49
|
+
> chainsaw access.log yesterday -o yesterday.log
|
50
|
+
|
51
|
+
## Contributing
|
52
|
+
|
53
|
+
1. Fork it
|
54
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
55
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
56
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
57
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/chainsaw
ADDED
data/chainsaw.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/chainsaw/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ['ljfauscett']
|
6
|
+
gem.email = ['ljfauscett@gmail.com']
|
7
|
+
gem.description = 'Cutting up logs like shelby stanga'
|
8
|
+
gem.summary = 'wahhhhh'
|
9
|
+
gem.homepage = ''
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = 'chainsaw'
|
15
|
+
gem.require_paths = ['lib']
|
16
|
+
gem.version = Chainsaw::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'chronic', '~> 0.7.0'
|
19
|
+
|
20
|
+
gem.add_development_dependency 'minitest', '~> 3.0.0'
|
21
|
+
gem.add_development_dependency 'mocha', '~> 0.11.4'
|
22
|
+
end
|
data/lib/chainsaw.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
require 'chronic'
|
6
|
+
|
7
|
+
require 'chainsaw/cli'
|
8
|
+
require 'chainsaw/format'
|
9
|
+
require 'chainsaw/detector'
|
10
|
+
require 'chainsaw/filter'
|
11
|
+
require 'chainsaw/version'
|
12
|
+
|
13
|
+
module Chainsaw
|
14
|
+
|
15
|
+
end
|
data/lib/chainsaw/cli.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module Chainsaw
|
2
|
+
class CLI
|
3
|
+
CHRONIC_OPTIONS = { :context => :past, :guess => false }
|
4
|
+
BANNER = <<-BANNER
|
5
|
+
Usage: chainsaw LOGFILE INTERVAL [OPTIONS]
|
6
|
+
|
7
|
+
Description:
|
8
|
+
Parses a log file and returns lines matching the time period provided.
|
9
|
+
|
10
|
+
Chronic is used to parse the time strings, so any format chronic
|
11
|
+
supports, chainsaw supports. A list of supported formats can
|
12
|
+
be found here: https://github.com/mojombo/chronic
|
13
|
+
|
14
|
+
Examples:
|
15
|
+
|
16
|
+
> chainsaw access.log 1 hour ago # entries from one hour ago to now
|
17
|
+
> chainsaw access.log august # entries from August to now
|
18
|
+
> chainsaw access.log 2012-08-06 # entries from August 6th to now
|
19
|
+
> chainsaw access.log 2012-08-27 10:00 # entries from August 27th at 10:00 to now
|
20
|
+
|
21
|
+
You can use a hypen to specify a time range (you can mix and match formats)
|
22
|
+
|
23
|
+
> chainsaw access.log 2012-08-01 - 2012-09-17 # entries within August 1st and September 17th
|
24
|
+
> chainsaw access.log august - yesterday # entries within August and September
|
25
|
+
|
26
|
+
BANNER
|
27
|
+
|
28
|
+
# Use OptionParser to parse options, then we remove them from
|
29
|
+
# ARGV to help ensure we're parsing times correctly
|
30
|
+
def self.parse_options
|
31
|
+
@options = OpenStruct.new({
|
32
|
+
:interactive => false,
|
33
|
+
:colorize => false
|
34
|
+
})
|
35
|
+
|
36
|
+
@opts = OptionParser.new do |opts|
|
37
|
+
opts.banner = BANNER.gsub(/^ {4}/, '')
|
38
|
+
|
39
|
+
opts.separator ''
|
40
|
+
opts.separator 'Options:'
|
41
|
+
|
42
|
+
opts.on('-f [FILTER]', 'Provide a regexp pattern to match on as well as the interval given') do |filter|
|
43
|
+
@options.filter = filter
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on('-i', 'Work in interactive mode, one line at a time') do
|
47
|
+
@options.interactive = true
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on('-c', 'Colorize output (dates and patterns given)') do
|
51
|
+
@options.colorize = true
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on('-o [FILE]', 'Output the filtered lines to a file') do |file|
|
55
|
+
@options.output_file = file
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on('-v', 'Print the version') do
|
59
|
+
puts Chainsaw::VERSION
|
60
|
+
exit
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on( '-h', '--help', 'Display this help.' ) do
|
64
|
+
puts opts
|
65
|
+
exit
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
@opts.parse!
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check the leftover arguments to see if we're given a range or not.
|
73
|
+
# If we have a range, parse them, if not, parse the single time arguments.
|
74
|
+
#
|
75
|
+
# args - an Array of String arguments
|
76
|
+
#
|
77
|
+
# Returns a Time object or Range representing the requested time
|
78
|
+
def self.parse_time_args(args)
|
79
|
+
delimiter = args.index('-')
|
80
|
+
|
81
|
+
if delimiter
|
82
|
+
starting = Chronic.parse(args[0..(delimiter - 1)].join(' '), CHRONIC_OPTIONS).begin
|
83
|
+
ending = Chronic.parse(args[(delimiter + 1)..-1].join(' '), CHRONIC_OPTIONS).begin
|
84
|
+
|
85
|
+
starting..ending
|
86
|
+
else
|
87
|
+
Chronic.parse(args.join(' '), CHRONIC_OPTIONS).begin
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.validate_logfile
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.print_usage_and_exit!
|
95
|
+
puts @opts
|
96
|
+
exit
|
97
|
+
end
|
98
|
+
|
99
|
+
# Called from the executable. Parses the command line arguments
|
100
|
+
# and passes them through to Filter.
|
101
|
+
def self.run
|
102
|
+
parse_options
|
103
|
+
print_usage_and_exit! if ARGV.empty?
|
104
|
+
|
105
|
+
logfile = ARGV.first
|
106
|
+
time = parse_time_args(ARGV[1..-1])
|
107
|
+
|
108
|
+
trap(:INT) { exit }
|
109
|
+
Filter.filter(logfile, time, @options)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Chainsaw
|
2
|
+
class Detector
|
3
|
+
|
4
|
+
CLF_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} (?:-|[^ ]+) (?:-|[^ ]+) \[(\d{2}\/[a-z]{3}\/\d{4}:\d{2}:\d{2}:\d{2} -\d{4})\]/i
|
5
|
+
APACHE_ERROR_PATTERN = /^\[([a-z]{3} [a-z]{3} \d{2} \d{2}:\d{2}:\d{2} \d{4})\]/i
|
6
|
+
NGINX_ERROR_PATTERN = /^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})/i
|
7
|
+
RUBY_LOGGER_PATTERN = /^[a-z]{1}, \[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\.\d+ #\d+\]/i
|
8
|
+
RAILS_PATTERN = /^started [a-z]+ "[^"]+" for \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} -\d{4})/i
|
9
|
+
SYSLOG_PATTERN = /^([a-z]{3} ?\d{1,2} \d{2}:\d{2}:\d{2})/i
|
10
|
+
REDIS_PATTERN = /^\[\d+\] ?(\d{1,2} [a-z]{3} \d{2}:\d{2}:\d{2})/i
|
11
|
+
PUPPET_PATTERN = /^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/i
|
12
|
+
MONGODB_PATTERN = /^(\w{3} \w{3} \d{2} \d{2}:\d{2}:\d{2})/i
|
13
|
+
RACK_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} (?:-|[^ ]+) (?:-|[^ ]+) \[(\d{2}\/[a-z]{3}\/\d{4} \d{2}:\d{2}:\d{2})\]/i
|
14
|
+
PYTHON_PATTERN = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/i
|
15
|
+
DJANGO_PATTERN = /^\[(\d{2}\/[a-z]{3}\/\d{4} \d{2}:\d{2}:\d{2})\]/i
|
16
|
+
|
17
|
+
def self.detect(log)
|
18
|
+
format = nil
|
19
|
+
|
20
|
+
log.each_line do |line|
|
21
|
+
format = _detect(line)
|
22
|
+
break unless format.type.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
if format.nil?
|
26
|
+
puts "\033[31mUnable to determine log format :(\033[0m"
|
27
|
+
exit
|
28
|
+
else
|
29
|
+
format
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self._detect(line)
|
34
|
+
format = Format.new
|
35
|
+
|
36
|
+
case line
|
37
|
+
when CLF_PATTERN
|
38
|
+
format.type = 'clf'
|
39
|
+
format.time_format = '%d/%b/%Y:%H:%M:%S %z'
|
40
|
+
format.pattern = CLF_PATTERN
|
41
|
+
when APACHE_ERROR_PATTERN
|
42
|
+
format.type = 'apache_error'
|
43
|
+
format.time_format = '%a %b %d %H:%M:%S %Y'
|
44
|
+
format.pattern = APACHE_ERROR_PATTERN
|
45
|
+
when NGINX_ERROR_PATTERN
|
46
|
+
format.type = 'nginx_error'
|
47
|
+
format.time_format = '%Y/%m/%d %H:%M:%S'
|
48
|
+
format.pattern = NGINX_ERROR_PATTERN
|
49
|
+
when RUBY_LOGGER_PATTERN
|
50
|
+
format.type = 'ruby_logger'
|
51
|
+
format.time_format = '%Y-%m-%dT%H:%M:%S'
|
52
|
+
format.pattern = RUBY_LOGGER_PATTERN
|
53
|
+
when RAILS_PATTERN
|
54
|
+
format.type = 'rails'
|
55
|
+
format.time_format = '%Y-%m-%d %H:%M:%S %z'
|
56
|
+
format.pattern = RAILS_PATTERN
|
57
|
+
when SYSLOG_PATTERN
|
58
|
+
format.type = 'syslog'
|
59
|
+
format.time_format = '%b %e %H:%M:%S'
|
60
|
+
format.pattern = SYSLOG_PATTERN
|
61
|
+
when REDIS_PATTERN
|
62
|
+
format.type = 'redis'
|
63
|
+
format.time_format = '%e %b %H:%M:%S'
|
64
|
+
format.pattern = REDIS_PATTERN
|
65
|
+
when PUPPET_PATTERN
|
66
|
+
format.type = 'puppet'
|
67
|
+
format.time_format = '%Y-%m-%d %H:%M:%S'
|
68
|
+
format.pattern = PUPPET_PATTERN
|
69
|
+
when MONGODB_PATTERN
|
70
|
+
format.type = 'mongodb'
|
71
|
+
format.time_format = '%a %b %d %H:%M:%S'
|
72
|
+
format.pattern = MONGODB_PATTERN
|
73
|
+
when RACK_PATTERN
|
74
|
+
format.type = 'rack'
|
75
|
+
format.time_format = '%d/%b/%Y %H:%M:%S'
|
76
|
+
format.pattern = RACK_PATTERN
|
77
|
+
when PYTHON_PATTERN
|
78
|
+
format.type = 'python'
|
79
|
+
format.time_format = '%Y-%m-%d %H:%M:%S'
|
80
|
+
format.pattern = PYTHON_PATTERN
|
81
|
+
when DJANGO_PATTERN
|
82
|
+
format.type = 'django'
|
83
|
+
format.time_format = '%d/%b/%Y %H:%M:%S'
|
84
|
+
format.pattern = DJANGO_PATTERN
|
85
|
+
end
|
86
|
+
|
87
|
+
format
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Chainsaw
|
2
|
+
class Filter
|
3
|
+
attr_reader :line_count
|
4
|
+
|
5
|
+
# Initialize a Filter instance. We read the logfile here and
|
6
|
+
# attempt to detect what type of logfile it is.
|
7
|
+
#
|
8
|
+
# logfile - the String path of the logfile to be filtered
|
9
|
+
# bounds - the "time" period to filter through (Time or Range)
|
10
|
+
# options - an OpenStruct representing the options
|
11
|
+
#
|
12
|
+
# Returns the Filter instance
|
13
|
+
def initialize(logfile, bounds, options = OpenStruct.new)
|
14
|
+
@logfile = logfile
|
15
|
+
@bounds = bounds
|
16
|
+
@options = options
|
17
|
+
@log = File.open(@logfile)
|
18
|
+
@format = Detector.detect(@log)
|
19
|
+
@line_count = 0
|
20
|
+
|
21
|
+
@log.rewind
|
22
|
+
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Start iterating through the log lines and filtering them accordingly.
|
27
|
+
def start
|
28
|
+
@log.each_line do |line|
|
29
|
+
filter = @options.filter
|
30
|
+
match = line.match(@format.pattern)
|
31
|
+
|
32
|
+
if match
|
33
|
+
timestamp = match[1]
|
34
|
+
time = Filter.parse_timestamp(timestamp, @format.time_format)
|
35
|
+
else
|
36
|
+
timestamp = time = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# a match was found if we are filtering additional text, check that too
|
40
|
+
if match && within_bounds?(time) && ( !filter || filter && line.include?(filter) )
|
41
|
+
found(line, timestamp)
|
42
|
+
# a match was found and we are outputting non-timestamped lines
|
43
|
+
elsif match && @outputting
|
44
|
+
@outputting = false
|
45
|
+
# outputting non-timestamped lines
|
46
|
+
elsif @outputting
|
47
|
+
out(line)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
unless @options.output_file
|
52
|
+
hind = (@line_count.zero? || @line_count > 1) ? 's' : ''
|
53
|
+
puts "\n\033[33mFound #{@line_count} line#{hind} \033[0m"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check to see if the parsed Time is within the bounds
|
58
|
+
# of our given Time or time Range.
|
59
|
+
#
|
60
|
+
# time - the parsed Time from the logline
|
61
|
+
#
|
62
|
+
# Returns true if within bounds
|
63
|
+
def within_bounds?(time)
|
64
|
+
if @bounds.is_a? Range
|
65
|
+
@bounds.cover?(time)
|
66
|
+
else
|
67
|
+
@bounds < time
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# A matching line was found, set @outputting incase we
|
72
|
+
# run into lines that aren't timestamped.
|
73
|
+
#
|
74
|
+
# line - the String logline
|
75
|
+
# timestamp - the String timestamp from the logline
|
76
|
+
def found(line, timestamp)
|
77
|
+
@outputting = true
|
78
|
+
@line_count += 1
|
79
|
+
|
80
|
+
out(line, timestamp)
|
81
|
+
|
82
|
+
STDIN.gets if @options.interactive && !@options.output_file
|
83
|
+
end
|
84
|
+
|
85
|
+
# Output the logline to STDOUT or File. We also colorize if requested.
|
86
|
+
#
|
87
|
+
# line - the String logline
|
88
|
+
# timestamp - the String timestamp from the logline
|
89
|
+
def out(line, timestamp = nil)
|
90
|
+
if @options.output_file
|
91
|
+
File.open(@options.output_file, 'a') { |f| f.write(line) }
|
92
|
+
elsif @options.colorize && timestamp
|
93
|
+
puts line.sub(timestamp, "\033[32m#{timestamp}\033[0m")
|
94
|
+
else
|
95
|
+
puts line
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Parse a timestamp using the given time_format. If a timezone
|
100
|
+
# isn't included in the timestamp, we'll set it to the current local
|
101
|
+
# timezone
|
102
|
+
#
|
103
|
+
# timestamp - the String timestamp
|
104
|
+
# time_format - the String time format used to parse
|
105
|
+
#
|
106
|
+
# Returns the parsed Time object
|
107
|
+
def self.parse_timestamp(timestamp, time_format)
|
108
|
+
if time_format.include?('%z')
|
109
|
+
dt = DateTime.strptime(timestamp, time_format)
|
110
|
+
else
|
111
|
+
# ugly, i know... find a better way
|
112
|
+
timestamp = timestamp + (Time.now.utc_offset / 3600).to_s
|
113
|
+
time_format = time_format + ' %z'
|
114
|
+
dt = DateTime.strptime(timestamp, time_format)
|
115
|
+
end
|
116
|
+
Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec)
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
# A convinence method to initialize a Filter object using the
|
121
|
+
# given args and start filtering it
|
122
|
+
def self.filter(*args)
|
123
|
+
new(*args).start
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|