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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chainsaw.gemspec
4
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+ require 'rake/testtask'
3
+ require 'bundler/gem_tasks'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.test_files = FileList['test/*_test.rb']
8
+ end
data/bin/chainsaw ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
3
+
4
+ require 'chainsaw'
5
+
6
+ Chainsaw::CLI.run
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
@@ -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