ntail 0.0.11 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -5,13 +5,18 @@ source "http://rubygems.org"
5
5
  gem "rainbow", ">= 0"
6
6
  gem "user-agent", ">= 0"
7
7
  gem "treetop", "~> 1.4.9"
8
+ gem 'sequel'
9
+ gem 'mongoid'
10
+ gem 'sqlite3-ruby', :require => 'sqlite3'
8
11
 
9
12
  # Add dependencies to develop your gem here.
10
13
  # Include everything needed to run rake, tests, features, etc.
11
14
  group :development do
15
+ gem "rake", ">= 0.9.2"
12
16
  gem "shoulda", ">= 0"
13
17
  gem "bundler", "~> 1.0.0"
14
18
  gem "jeweler", "~> 1.5.1"
15
19
  gem "rcov", ">= 0"
16
20
  gem "geoip", ">= 0"
21
+ gem "rspec", ">= 2.5"
17
22
  end
data/Gemfile.lock CHANGED
@@ -1,19 +1,47 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
- geoip (0.8.7)
4
+ activemodel (3.0.9)
5
+ activesupport (= 3.0.9)
6
+ builder (~> 2.1.2)
7
+ i18n (~> 0.5.0)
8
+ activesupport (3.0.9)
9
+ bson (1.3.1)
10
+ builder (2.1.2)
11
+ diff-lcs (1.1.2)
12
+ geoip (1.0.0)
5
13
  git (1.2.5)
6
- jeweler (1.5.1)
14
+ i18n (0.5.0)
15
+ jeweler (1.5.2)
7
16
  bundler (~> 1.0.0)
8
17
  git (>= 1.2.5)
9
18
  rake
19
+ mongo (1.3.1)
20
+ bson (>= 1.3.1)
21
+ mongoid (2.1.2)
22
+ activemodel (~> 3.0)
23
+ mongo (~> 1.3)
24
+ tzinfo (~> 0.3.22)
10
25
  polyglot (0.3.1)
11
- rainbow (1.1)
12
- rake (0.8.7)
26
+ rainbow (1.1.1)
27
+ rake (0.9.2)
13
28
  rcov (0.9.9)
29
+ rspec (2.5.0)
30
+ rspec-core (~> 2.5.0)
31
+ rspec-expectations (~> 2.5.0)
32
+ rspec-mocks (~> 2.5.0)
33
+ rspec-core (2.5.1)
34
+ rspec-expectations (2.5.0)
35
+ diff-lcs (~> 1.1.2)
36
+ rspec-mocks (2.5.0)
37
+ sequel (3.25.0)
14
38
  shoulda (2.11.3)
39
+ sqlite3 (1.3.4)
40
+ sqlite3-ruby (1.3.3)
41
+ sqlite3 (>= 1.3.3)
15
42
  treetop (1.4.9)
16
43
  polyglot (>= 0.3.1)
44
+ tzinfo (0.3.29)
17
45
  user-agent (1.0.0)
18
46
 
19
47
  PLATFORMS
@@ -23,8 +51,13 @@ DEPENDENCIES
23
51
  bundler (~> 1.0.0)
24
52
  geoip
25
53
  jeweler (~> 1.5.1)
54
+ mongoid
26
55
  rainbow
56
+ rake (>= 0.9.2)
27
57
  rcov
58
+ rspec (>= 2.5)
59
+ sequel
28
60
  shoulda
61
+ sqlite3-ruby
29
62
  treetop (~> 1.4.9)
30
63
  user-agent
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ ntail
2
+ =====
3
+
4
+ A `tail(1)`-like utility for nginx log files that supports parsing, filtering and formatting of individual
5
+ log lines (in nginx's so-called ["combined" log format](http://wiki.nginx.org/NginxHttpLogModule#log_format)).
6
+
7
+ <a name="intro"/>
8
+
9
+ Check it out, yo!
10
+ -----------------
11
+
12
+ Instead of this...
13
+
14
+ <pre style="background-color: black; color: white; padding: 15px; width: 1100px; overflow: hidden; text-overflow: ellipsis;">
15
+ <span style="color:white;">$ tail -f /var/log/nginx/access.log</span>
16
+ <span style="color: green;">192.0.32.10 - - [21/Jan/2011:14:07:34 +0000] "GET / HTTP/1.1" 200 3700 "-" "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10" "-"</span>
17
+ <span style="color: green;">192.0.32.10 - - [21/Jan/2011:14:07:34 +0000] "GET /nginx-logo.png HTTP/1.1" 200 370 "http://localhost/" "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10" "-"</span>
18
+ <span style="color: green;">192.0.32.10 - - [21/Jan/2011:14:07:34 +0000] "GET /poweredby.png HTTP/1.1" 200 3034 "http://localhost/" "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10" "-"</span>
19
+ <span style="color: green;">192.0.32.10 - - [21/Jan/2011:14:07:34 +0000] "GET /favicon.ico HTTP/1.1" 404 3650 "-" "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10" "-"</span>
20
+ <span style="color: green;">192.0.32.10 - - [21/Jan/2011:14:19:04 +0000] "GET /nginx-logo.png HTTP/1.1" 304 0 "-" "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10" "-"</span>
21
+ <span style="color:white;">$ _</span>
22
+ </pre>
23
+
24
+ ... you get this:
25
+
26
+ <pre style="background-color: black; padding: 15px; width: 800px;">
27
+ <span style="color:white;">$ tail -f /var/log/nginx/access.log <strong>| ntail</strong></span>
28
+ <span style="color: green;">2011-01-21 14:07:34 - 192.0.32.10 - 200 - GET / - (Chrome, Linux) - -</span>
29
+ <span style="color: green;">2011-01-21 14:07:34 - 192.0.32.10 - 200 - GET /nginx-logo.png - (Chrome, Linux) - localhost</span>
30
+ <span style="color: green;">2011-01-21 14:07:34 - 192.0.32.10 - 200 - GET /spanoweredby.png - (Chrome, Linux) - localhost</span>
31
+ <span style="color: red;">2011-01-21 14:07:34 - 192.0.32.10 - 404 - GET /favicon.ico - (Chrome, Linux) - -</span>
32
+ <span style="color: orange;">2011-01-21 14:19:04 - 192.0.32.10 - 304 - GET /nginx-logo.png - (Chrome, Linux) - -</span>
33
+ <span style="color:white;">$ _</span>
34
+ </pre>
35
+
36
+ <a name="installation"/>
37
+
38
+ Installation
39
+ ------------
40
+
41
+ Installing the gem also installs the `ntail` executable, typically as `/usr/bin/ntail` or `/usr/local/bin/ntail`:
42
+
43
+ $ gem install ntail
44
+
45
+ To ensure easy execution of the `ntail` script, add the actual installation directory to your shell's `$PATH` variable.
46
+
47
+ <a name="basic"/>
48
+
49
+ Basic Usage
50
+ -----------
51
+
52
+ * process an entire nginx log file and print each parsed and formatted line to STDOUT
53
+
54
+ $ ntail /var/log/nginx/access.log
55
+
56
+ * tail an "active" nginx log file and print each new line to STDOUT _(stop with ^C)_
57
+
58
+ $ tail -f /var/log/nginx/access.log | ntail
59
+
60
+ <a name="advanced"/>
61
+
62
+ Advanced Examples
63
+ -----------------
64
+
65
+ * read from STDIN and print each line to STDOUT _(stop with ^D)_
66
+
67
+ $ ntail
68
+
69
+ * read from STDIN and print out the length of each line _(to illustrate -e option)_
70
+
71
+ $ ntail -e 'puts size'
72
+
73
+ * read from STDIN but only print out non-empty lines _(to illustrate -f option)_
74
+
75
+ $ ntail -f 'size != 0'
76
+
77
+ * the following invocations behave exactly the same _(to illustrate -e and -f options)_
78
+
79
+ $ ntail
80
+ $ ntail -f 'true' -e 'puts self'
81
+
82
+ * print out all HTTP requests that are coming from a given IP address
83
+
84
+ $ ntail -f 'remote_address == "208.67.222.222"' /var/log/nginx/access.log
85
+
86
+ * find all HTTP requests that resulted in a '5xx' HTTP error/status code _(e.g. Rails 500 errors)_
87
+
88
+ $ gunzip -S .gz -c access.log-20101216.gz | ntail -f 'server_error_status?'
89
+
90
+ * generate a summary report of HTTP status codes, for all non-200 HTTP requests
91
+
92
+ $ ntail -f 'status != "200"' -e 'puts status' access.log | sort | uniq -c
93
+ 76 301
94
+ 16 302
95
+ 2 304
96
+ 1 406
97
+
98
+ * print out GeoIP country and city information for each HTTP request _(depends on the optional `geoip` gem)_
99
+
100
+ $ ntail -e 'puts [to_country_s, to_city_s].join("\t")' /var/log/nginx/access.log
101
+ United States Los Angeles
102
+ United States Houston
103
+ Germany Berlin
104
+ United Kingdom London
105
+
106
+ * print out the IP address and the corresponding host name for each HTTP request _(slows things down considerably, due to `nslookup` call)_
107
+
108
+ $ ntail -e 'puts [remote_address, to_host_s].join("\t")' /var/log/nginx/access.log
109
+ 66.249.72.196 crawl-66-249-72-196.googlebot.com
110
+ 67.192.120.134 s402.pingdom.com
111
+ 75.31.109.144 adsl-75-31-109-144.dsl.irvnca.sbcglobal.net
112
+
113
+ * parse an access log file, and pipe its raw output (indirectly - via the `parsed.log` file) into the `gltail` realtime logfile visualizer
114
+
115
+ $ ntail -v --raw --sleep 0.1 /var/log/nginx/access.log > parsed.log
116
+
117
+ <a name="todo"/>
118
+
119
+ TODO
120
+ ----
121
+
122
+ * implement a native `"-f"` option for ntail, similar to that of `tail(1)`, using e.g. flori's [file-tail gem](https://github.com/flori/file-tail)
123
+ * implement a `"-i"` option ("ignore exceptions"/"continue processing"), if handling a single line raises an exception
124
+ * or indeed a reverse `"-r"` option ("re-raise exception"), to immediately stop processing and raising the exception for investigation
125
+ * implement (better) support for custom nginx log formats, in addition to [nginx's default "combined" log format](http://wiki.nginx.org/NginxHttpLogModule#log_format).
126
+
127
+ <a name="acknowledgements"/>
128
+
129
+ Acknowledgements
130
+ ----------------
131
+
132
+ * ntail's parsing feature is inspired by an nginx log parser written by [Richard Taylor (moomerman)](https://github.com/moomerman)
133
+ * parsing and expanding ntail's formatting string is done using nathansobo's quite brilliant [treetop gem](https://github.com/nathansobo/treetop)
134
+ * ntail's raw line output is compatible with Fudge's fun and useful [gltail gem](https://github.com/Fudge/gltail)
135
+ * Kudos to [Ed James (edjames)](https://github.com/edjames) for recommending the use of [instance_eval][eval] to clean up the DSL
136
+
137
+ [eval]: https://github.com/pvdb/ntail/commit/b0f40522012b9858c433808cd1f5c21cb455fadd "use instance_eval to simplify the DSL"
138
+
139
+ <a name="contributing"/>
140
+
141
+ Contributing to ntail
142
+ ---------------------
143
+
144
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
145
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
146
+ * Fork the project
147
+ * Start a feature/bugfix branch
148
+ * Commit and push until you are happy with your contribution
149
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
150
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
151
+
152
+ <a name="copyright"/>
153
+
154
+ Copyright
155
+ ---------
156
+
157
+ Copyright (c) 2011 Peter Vandenberk. See LICENSE.txt for further details.
158
+
data/Rakefile CHANGED
@@ -26,6 +26,16 @@ Jeweler::Tasks.new do |gem|
26
26
  end
27
27
  Jeweler::RubygemsDotOrgTasks.new
28
28
 
29
+ require 'rake/rdoctask'
30
+ Rake::RDocTask.new do |rdoc|
31
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
32
+
33
+ rdoc.rdoc_dir = 'rdoc'
34
+ rdoc.title = "ntail #{version}"
35
+ rdoc.rdoc_files.include('README*')
36
+ rdoc.rdoc_files.include('lib/**/*.rb')
37
+ end
38
+
29
39
  require 'rake/testtask'
30
40
  Rake::TestTask.new(:test) do |test|
31
41
  test.libs << 'lib' << 'test'
@@ -33,21 +43,24 @@ Rake::TestTask.new(:test) do |test|
33
43
  test.verbose = true
34
44
  end
35
45
 
46
+ require 'rspec/core/rake_task'
47
+ RSpec::Core::RakeTask.new(:spec) do |test|
48
+ test.verbose = true
49
+ end
50
+
36
51
  require 'rcov/rcovtask'
37
- Rcov::RcovTask.new do |test|
52
+ Rcov::RcovTask.new(:test_rcov) do |test|
38
53
  test.libs << 'test'
39
54
  test.pattern = 'test/**/test_*.rb'
55
+ test.rcov_opts = %w{--exclude test\/,spec\/ -T}
56
+ test.verbose = true
57
+ end
58
+ Rcov::RcovTask.new(:spec_rcov) do |test|
59
+ test.libs << 'spec'
60
+ test.pattern = 'spec/**/*_spec.rb'
61
+ test.rcov_opts = %w{--exclude test\/,spec\/ -T}
40
62
  test.verbose = true
41
63
  end
42
64
 
43
- task :default => :test
44
-
45
- require 'rake/rdoctask'
46
- Rake::RDocTask.new do |rdoc|
47
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
65
+ task :default => [:test, :spec]
48
66
 
49
- rdoc.rdoc_dir = 'rdoc'
50
- rdoc.title = "ntail #{version}"
51
- rdoc.rdoc_files.include('README*')
52
- rdoc.rdoc_files.include('lib/**/*.rb')
53
- end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.11
1
+ 0.0.12
data/bin/ntail CHANGED
@@ -9,6 +9,6 @@ rescue LoadError
9
9
  require 'ntail'
10
10
  end
11
11
 
12
- exit NginxTail::Application.run!
12
+ exit NginxTail::Application.new(ARGV).run!
13
13
 
14
14
  # That's all, Folks!
@@ -3,145 +3,122 @@ require 'optparse'
3
3
 
4
4
  module NginxTail
5
5
  class Application
6
-
7
- def self.options
8
- # application options from the command line
9
- @@options ||= OpenStruct.new
10
- end
11
-
12
- def self.ntail_options
13
- # shamelessly copied from lib/rake.rb (rake gem)
14
- [
15
- ['--verbose', '-v', "Run verbosely (log messages to STDERR).",
16
- lambda { |value|
17
- self.options.verbose = true
18
- }
19
- ],
20
- ['--dry-run', '-n', "Dry-run: process files, but don't actually parse the lines",
21
- lambda { |value|
22
- self.options.dry_run = true
23
- }
24
- ],
25
- ['--parse-only', '-p', "Parse only: parse all lines, but don't actually process them",
26
- lambda { |value|
27
- self.options.parse_only = true
28
- }
29
- ],
30
- ['--version', '-V', "Display the program version.",
31
- lambda { |value|
32
- puts "#{NTAIL_NAME}, version #{NTAIL_VERSION}"
33
- self.options.running = false
34
- }
35
- ],
36
- ['--line-number', '-l LINE_NUMBER', "Only process the line with the given line number",
37
- lambda { |value|
38
- self.options.line_number = value.to_i
39
- }
40
- ],
41
- ['--filter', '-f CODE', "Ruby code block for filtering (parsed) lines - needs to return true or false.",
42
- lambda { |value|
43
- self.options.filter = eval "Proc.new #{value}"
44
- }
45
- ],
46
- ['--execute', '-e CODE', "Ruby code block for processing each (parsed) line.",
47
- lambda { |value|
48
- self.options.code = eval "Proc.new #{value}"
49
- }
50
- ],
51
- ]
6
+
7
+ include NginxTail::Options
8
+
9
+ # default application options...
10
+ DEFAULT_OPTIONS = {
11
+ :interrupted => false,
12
+ :running => true,
13
+ :nginx => true,
14
+ :exit => 0,
15
+ }
16
+
17
+ # parsed application options...
18
+ @options = nil
19
+
20
+ def respond_to?(symbol, include_private = false)
21
+ @options.respond_to?(symbol) || super
52
22
  end
53
23
 
54
- def self.parse_options
55
-
56
- # application defaults...
57
- self.options.interrupted = false
58
- self.options.running = true
59
- self.options.exit = 0
60
-
61
- OptionParser.new do |opts|
62
- opts.banner = "ntail {options} {file(s)} ..."
63
- opts.separator ""
64
- opts.separator "Options are ..."
65
-
66
- opts.on_tail("-h", "--help", "-H", "Display this help message.") do
67
- puts opts
68
- self.options.running = false
69
- end
24
+ def method_missing(methodId)
25
+ respond_to?(methodId) ? @options.send(methodId.to_sym) : super
26
+ end
70
27
 
71
- self.ntail_options.each { |args| opts.on(*args) }
72
- end.parse!
28
+ def initialize(argv = [])
29
+ @options = parse_options(argv, DEFAULT_OPTIONS)
73
30
  end
74
-
75
- def self.run!
76
-
77
- self.parse_options
78
-
31
+
32
+ def run!
33
+
34
+ LogLine.set_log_pattern(@options.nginx)
35
+
79
36
  ['TERM', 'INT'].each do |signal|
80
37
  Signal.trap(signal) do
81
- self.options.running = false ; self.options.interrupted = true
38
+ @options.running = false ; @options.interrupted = true
82
39
  $stdin.close if ARGF.file == $stdin # ie. reading from STDIN
83
40
  end
84
41
  end
85
-
42
+
86
43
  files_read = lines_read = lines_processed = lines_ignored = parsable_lines = unparsable_lines = 0
87
-
88
- while self.options.running and ARGF.gets
44
+
45
+ current_filename = nil ; current_line_number = 0 ; file_count = ARGV.count
46
+
47
+ while @options.running and ARGF.gets
89
48
  if ARGF.file.lineno == 1
49
+ current_filename = ARGF.filename ; current_line_number = 0
90
50
  files_read += 1
91
- if self.options.verbose
51
+ if @options.verbose
92
52
  $stderr.puts "[INFO] now processing file #{ARGF.filename}"
93
53
  end
94
54
  end
95
- raw_line = $_.chomp ; lines_read += 1
96
- unless self.options.dry_run
97
- if !self.options.line_number or self.options.line_number == ARGF.lineno
55
+ raw_line = $_.chomp ; lines_read += 1 ; current_line_number += 1
56
+ unless @options.dry_run
57
+ if !@options.line_number or @options.line_number == ARGF.lineno
98
58
  begin
99
- log_line = NginxTail::LogLine.new(raw_line)
59
+ log_line = NginxTail::LogLine.new(raw_line, current_filename, current_line_number)
100
60
  if log_line.parsable
101
61
  parsable_lines += 1
102
- unless self.options.parse_only
103
- if !self.options.filter || self.options.filter.call(log_line)
62
+ unless @options.parse_only
63
+ if !@options.filter || log_line.instance_eval(@options.filter)
104
64
  lines_processed += 1
105
- if self.options.code
106
- self.options.code.call(log_line)
65
+ if @options.code
66
+ log_line.instance_eval(@options.code)
67
+ elsif @options.raw
68
+ $stdout.puts raw_line
69
+ sleep @options.sleep if @options.sleep
107
70
  else
108
- puts log_line
71
+ puts log_line.to_s(:color => true)
109
72
  end
110
73
  else
111
74
  lines_ignored += 1
112
- if self.options.verbose
75
+ if @options.verbose
113
76
  $stderr.puts "[WARNING] ignoring line ##{lines_read}"
114
77
  end
115
78
  end
116
79
  end
117
80
  else
118
81
  unparsable_lines += 1
119
- if self.options.verbose
82
+ if @options.verbose
120
83
  $stderr.puts "[ERROR] cannot parse '#{raw_line}'"
121
84
  end
122
85
  end
123
86
  rescue
124
87
  $stderr.puts "[ERROR] processing line #{ARGF.file.lineno} of file #{ARGF.filename} resulted in #{$!.message}"
125
88
  $stderr.puts "[ERROR] " + raw_line
126
- self.options.exit = -1
127
- self.options.running = false
89
+ @options.exit = -1
90
+ @options.running = false
128
91
  raise $! # TODO if the "re-raise exceptions" option has been set...
129
92
  end
130
93
  end
131
94
  end
95
+ if @options.progress
96
+ progress_line = [
97
+ " Processing file ".inverse + (" %d/%d" % [files_read, file_count]),
98
+ " Current filename ".inverse + " " + current_filename.to_s,
99
+ " Line number ".inverse + " " + current_line_number.to_s,
100
+ " Lines processed ".inverse + " " + lines_read.to_s
101
+ ].join(" \342\200\242 ")
102
+ max_length = [max_length || 0, progress_line.size].max
103
+ $stderr.print progress_line
104
+ $stderr.print " " * (max_length - progress_line.size)
105
+ $stderr.print "\r"
106
+ end
132
107
  end
133
-
134
- if self.options.verbose
135
- $stderr.puts if self.options.interrupted
136
- $stderr.print "[INFO] read #{lines_read} lines in #{files_read} files"
137
- $stderr.print " (interrupted)" if self.options.interrupted ; $stderr.puts
108
+
109
+ $stderr.puts if @options.progress
110
+
111
+ if @options.verbose
112
+ $stderr.puts if @options.interrupted
113
+ $stderr.print "[INFO] read #{lines_read} line(s) in #{files_read} file(s)"
114
+ $stderr.print " (interrupted)" if @options.interrupted ; $stderr.puts
138
115
  $stderr.puts "[INFO] #{parsable_lines} parsable lines, #{unparsable_lines} unparsable lines"
139
116
  $stderr.puts "[INFO] processed #{lines_processed} lines, ignored #{lines_ignored} lines"
140
117
  end
141
-
142
- return self.options.exit
143
-
144
- end # def run
145
-
118
+
119
+ return @options.exit
120
+
121
+ end
122
+
146
123
  end
147
124
  end
@@ -5,10 +5,10 @@ module NginxTail
5
5
  base.class_eval do
6
6
 
7
7
  # this ensures the below module methods actually make sense...
8
- raise "Class #{base.name} should implement instance method 'body_bytes_sent'" unless base.instance_methods.include? 'body_bytes_sent'
8
+ raise "Class #{base.name} should implement instance method 'body_bytes_sent'" unless base.instance_methods.map(&:to_s).include? 'body_bytes_sent'
9
9
 
10
10
  end
11
11
  end
12
12
 
13
13
  end
14
- end
14
+ end