Permalink
Browse files

Add first pass at nginx-stat

  • Loading branch information...
1 parent caec822 commit c293bab69d99a77b077e56e9431aeb8ccd4004ed @brynary committed Mar 25, 2009
Showing with 208 additions and 0 deletions.
  1. +3 −0 README.rdoc
  2. +11 −0 bin/nginx_stat
  3. +157 −0 lib/nginx_stat.rb
  4. +37 −0 lib/nginx_stat/io_tail.rb
View
@@ -0,0 +1,3 @@
+Analyze live nginx access logs for reqs/sec and average time per request
+
+Based on RailsStat, from Rails Analyzer Tools by Seattle.rb
View
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby -w
+
+require File.expand_path(File.dirname(__FILE__) + '/../lib/nginx_stat')
+
+if ARGV.length < 1 then
+ $stderr.puts "Usage: #{$0} NGINX_LOG [...] [PRINT_INTERVAL]"
+ exit 1
+end
+
+NginxStat.start(*ARGV)
+
View
@@ -0,0 +1,157 @@
+require "English"
+
+unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
+end
+
+require 'thread'
+require 'curses'
+require "nginx_stat/io_tail"
+
+##
+# NginxStat displays the current requests per second and average request time.
+# Default interval is 10 seconds.
+
+class NginxStat
+
+ ##
+ # NginxStat.start 'online-43things.log', 'online-43people.log', 10
+ #
+ # Starts a new NginxStat for +filenames+ that prints every +interval+
+ # seconds.
+ #
+ # Stats for multiple log files requires curses.
+
+ def self.start(*args)
+ interval = 10
+ interval = Float(args.pop) if Float(args.last) rescue nil
+
+ stats = []
+
+ if args.length > 1 and not defined? Curses then
+ $stderr.puts "Multiple logfile support requires curses"
+ exit 1
+ end
+
+ if defined? Curses then
+ Curses.init_screen
+ Curses.clear
+ Curses.addstr "Collecting data...\n"
+ Curses.refresh
+ end
+
+ args.each_with_index do |filename, offset|
+ stat = self.new File.open(filename), interval, offset
+ stat.start
+ stats << stat
+ end
+
+ stats.each { |stat| stat.thread.join }
+ end
+
+ ##
+ # The log reading thread
+
+ attr_reader :thread
+
+ ##
+ # Current status line
+
+ attr_reader :status
+
+ ##
+ # Creates a new NginxStat that will listen on +io+ and print every
+ # +interval+ seconds. +offset+ is only used for multi-file support.
+
+ def initialize(io, interval, offset = 0)
+ @io = io
+ @io_path = File.basename io.path rescue 'unknown'
+ @interval = interval.to_f
+ @offset = offset
+
+ @mutex = Mutex.new
+ @status = ''
+ @last_len = 0
+ @count = 0
+ @time = 0.0
+ @thread = nil
+ end
+
+ ##
+ # Starts the NginxStat running. This method never returns.
+
+ def start
+ trap 'INT' do
+ Curses.close_screen if defined? Curses
+ exit
+ end
+ start_printer
+ read_log
+ end
+
+ def print
+ if defined? Curses then
+ Curses.setpos @offset, 0
+ Curses.addstr ' ' * @last_len
+ Curses.setpos @offset, 0
+ Curses.addstr "#{@io_path}\t#{@status}"
+ Curses.refresh
+ else
+ print "\r"
+ print ' ' * @last_len
+ print "\r"
+ print @status
+ $stdout.flush
+ end
+ end
+
+ private
+
+ ##
+ # Starts a thread that prints log information every +interval+ seconds.
+
+ def start_printer
+ Thread.start do
+ count_sec = 0
+ average_time = 0.0
+
+ loop do
+ sleep @interval
+
+ @mutex.synchronize do
+ count_sec = @count / @interval
+ average_time = @time / @count.to_f
+ @count = 0
+ @time = 0.0
+ end
+
+ @status = "%5.1f req/sec, %.2f sec per req" % [count_sec, average_time]
+
+ print
+
+ @last_len = status.length
+ end
+ end
+ end
+
+ ##
+ # Starts a thread that reads from +io+, updating NginxStat counters as it
+ # goes.
+
+ def read_log
+ @thread = Thread.start do
+ @io.tail_lines do |line|
+ unless exclude?(line)
+ @mutex.synchronize { @time += line.strip.split.last.to_f }
+ @mutex.synchronize { @count += 1 }
+ end
+ end
+ end
+ end
+
+ def exclude?(line)
+ line =~ /\.(gif|jpg|jpeg|png|ico)/
+ end
+
+end
+
View
@@ -0,0 +1,37 @@
+##
+# IOTail provides a tail_lines method as a mixin. By default it is included
+# into IO and StringIO, if present. If you require StringIO after IOTail,
+# then simply open StringIO and include IOTail.
+
+module IOTail
+
+ ##
+ # Jumps to near the end of the IO, then yields each line, waiting for new
+ # lines if it reaches eof?
+
+ def tail_lines(&block) # :yields: line
+ self.seek(-1, IO::SEEK_END)
+ self.gets
+
+ loop do
+ self.each_line(&block)
+
+ if self.eof? then
+ sleep 0.25
+ self.pos = self.pos # reset eof?
+ end
+ end
+ end
+
+end
+
+class IO # :nodoc:
+ include IOTail
+end
+
+if defined? StringIO then
+ class StringIO # :nodoc:
+ include IOTail
+ end
+end
+

0 comments on commit c293bab

Please sign in to comment.