#!/usr/bin/env ruby $: << File.join(File.dirname(__FILE__), '..', 'lib') require 'setup_environment' require 'log_parser' require 'observation' require 'ansi_colors' require 'stream_wrapper' # We don't need a stack dump when interrupted with Ctrl-C trap("INT", "EXIT") # Make symbols sortable. class Symbol def <=>(other) self.to_s <=> other.to_s end end class Viewobs attr_accessor :options def parse_options require 'optparse' options = { :delimiter => "\t" } option_parser = OptionParser.new do |opts| opts.banner = "Usage: viewobs.rb [options] files" opts.separator "" opts.separator "Observation filters:" opts.on("-i IP", "--ip IP", "Filter on IP.") { |v| options[:filter_ip] = v } opts.on("-s SHOP_ID", "--shop SHOP_ID", "Filter on shop ID.") { |v| options[:filter_shop_id] = v } opts.on("-a ACCOUNT_ID", "--account ACCOUNT_ID", "Filter on account ID.") { |v| options[:filter_account_id] = v } opts.on("-t TYPE", "--type TYPE", "Filter on observation type.") { |v| options[:filter_type] = v } opts.on("-v", "--valid", "Display only valid observations.") { |v| options[:filter_valid] = v } opts.separator "" opts.separator "Display filters:" opts.on("-A", "--all", "Display all attributes.") { |v| options[:all_attributes] = v } opts.on("-u", "--user", "Display user set attributes.") { |v| options[:user_attributes] = v } opts.on("-o", "--observation", "Display observation attributes.") { |v| options[:observation_attributes] = v } opts.on("-e", "--errors", "Display errors (if any).") { |v| options[:errors] = v } opts.on("-r", "--request", "Display raw request.") { |v| options[:request] = v } opts.separator "" opts.separator "Other options:" opts.on("-f", "--follow", "Waits at the end of the log for updates.") { |v| options[:follow] = v } opts.on("-c", "--colorize", "Colorize output.") { |v| String.colorize } opts.on("-T", "--table", "Output as table or csv.", "Does not work with data from stdin (reads twice).") { options[:table] = true } opts.on("-d CHAR", "--delimiter CHAR", "Delimiter for table (default is tab).") { |v| options[:delimiter] = v[0,1] } opts.separator "" end begin # Parse will leave any filename argument intact in ARGV but it will remove all options. option_parser.parse!(ARGV) rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e puts e puts option_parser exit 1 end @options = options end def view_line(attributes, observation) # Print basic information. out = "" out << "#{observation.attributes[:account_id].to_s.red}" if observation.attributes[:account_id] out << " - #{observation.attributes[:shop_id].to_s.red}" if observation.attributes[:shop_id] out << " - #{attributes[:o].blue}" if attributes[:o] # Observation type. out << " - #{attributes[:timestamp].strftime('%Y-%m-%d %H.%M.%S')}" out << " - #{attributes[:ip]}" out << " - #{attributes[:status]}" out << " - #{'valid'.green}" if observation.valid? puts out # Print extended information depending on options. if options[:all_attributes] attributes.keys.sort.each do |key| puts " #{key.to_s.blue}: #{attributes[key]}" unless [:ip, :request, :timestamp, :status].include?(key) end puts end if options[:user_attributes] attributes.keys.sort.each do |key| puts " #{key.to_s[1..-1].blue}: #{attributes[key]}" if key.to_s[0..0] == '_' end puts end if options[:observation_attributes] observation.attributes.keys.sort.each do |key| puts " #{key.to_s.blue}: #{observation.attributes[key]}" if observation.attributes.has_key?(key) && key != :request end puts end if options[:errors] puts observation.errors.inspect unless observation.valid? puts end if options[:request] puts attributes[:request] puts end end def view_table_line(attributes, observation) # Print basic information. out = [] out << observation.attributes[:account_id].to_s.red out << observation.attributes[:shop_id].to_s.red out << attributes[:o].blue # Observation type. out << attributes[:timestamp].strftime('%Y-%m-%d %H.%M.%S') out << attributes[:ip] out << attributes[:status] out << (observation.valid? ? 'valid' : 'invalid') @all_keys.each do |key| out << attributes[key] unless [:o, :ip, :request, :timestamp, :status].include?(key) end puts out.map { |s| s.to_s.tr("#{delimiter}\0- "," ") }.join(delimiter) end def view_until_end # gets will take lines from any filename given in ARGV or from stdin. pingdom_re = /pingdom/ filters = { :account_id => options[:filter_account_id], :shop_id => options[:filter_shop_id], :observation_type => options[:filter_type] } stream = StreamWrapper.open(ARGV, filters) while line = stream.gets attributes = LogParser.parse_line(line) observation = Observation.new(attributes) # Skip lines from pingdom. next if attributes[:request].match(pingdom_re) # Filter depending on options. if options[:filter_ip] next unless attributes[:ip].match(options[:filter_ip]) end if options[:filter_valid] next unless observation.valid? end begin if options[:gathering_keys] gather_keys(attributes, observation) elsif options[:table] view_table_line(attributes, observation) else view_line(attributes, observation) end rescue Exception => e $stderr.puts "Could not render line #{$.} (#{e.message})" end end if options[:gathering_keys] @all_keys = @all_keys.sort end end def gather_keys (attributes, observation) @all_keys ||= [] @all_keys = @all_keys | attributes.keys end def print_header print %w[account_id shop_id o timestamp ip status valid].join(delimiter) print delimiter puts (@all_keys - [:o, :timestamp, :ip, :status, :request]).map { |sym| sym.to_s }.join(delimiter) end def delimiter options[:delimiter] end end if File.basename($0) == File.basename(__FILE__) viewobs = Viewobs.new viewobs.parse_options if viewobs.options[:follow] while true viewobs.view_until_end sleep(0.1) end else if viewobs.options[:table] viewobs.options[:gathering_keys] = true viewobs.view_until_end viewobs.print_header viewobs.options[:gathering_keys] = false end viewobs.view_until_end end end