chainsaw 0.0.4
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/.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
|