#!/usr/bin/env ruby # ncping [HOSTS...] - parallel ping with UTF-8 graphs # Use -4 / -6 to toggle between ping/ping6 for the next hosts. # # Written by Christian Neukirchen . # ncping is in the public domain. # # To the extent possible under law, the creator of this work has # waived all copyright and related or neighboring rights to this work. # http://creativecommons.org/publicdomain/zero/1.0/ Thread.abort_on_exception = true class Ping attr_reader :host, :ping def initialize(host, opts={}) @host = host @width = opts[:width] || 60 @buffer = opts[:buffer] || 60**3 @backlog = Array.new(@buffer) @ping = opts[:ping] || "ping" end def run loop { IO.popen([@ping, "-DO", @host, :err=>[:child, :out]]) { |rd,wr| while line = rd.gets if line =~ /^connect: Network is unreachable/ # e.g. dns failed, restart ping @backlog.shift @backlog << :down sleep 1 break elsif line =~ /^\[([\d.]+)\].*time=([\d.]+)/ ts, delay = $1.to_f, $2.to_f @backlog.shift @backlog << delay elsif line =~ /^\[([\d.]+)\].*no answer yet/ @backlog.shift @backlog << :loss elsif line =~ /Network is unreachable/ @backlog[-1] = :down end end } } end SYMBOLS = "\u2581\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2587" COLORS = %W{\e[1;32m \e[1;32m \e[0;32m \e[1;33m \e[0;33m \e[0;31m \e[1;31m \e[1;32m} OFFSETS = [ 0.08, 5, 12, 25, 50, 100, 250, 1000 ] def avg(v) v.empty? ? 0 : v.inject(:+) / v.size end def stat(v) v2 = v.compact.find_all{|x| x!=:loss && x!=:down} [v2.min || 0.0, avg(v2), v2.max || 0.0] end def to_s(res=1) v = aggreg(res) min, avg, max = stat(v) line = v.last(@width).map { |d| if d == :loss "\e[1;30mx" elsif d == :down "\e[1;30mD" elsif d c = COLORS[OFFSETS.index { |cc| d < cc } || COLORS.size-1] if max == min c + SYMBOLS[SYMBOLS.size/2] else c + SYMBOLS[(((d-min) / (max-min))*SYMBOLS.size - 0.5).to_i] end else "\e[0;37m\u00B7" end }.join "\e[0;37m%s%s%s %s\e[0;37m" % [fmt(min), fmt(avg), fmt(max), line] end def fmt(ms) if ms >= 100_000 "%5.0fs" % (ms/1000) elsif ms >= 10_000 "%5.1fs" % (ms/1000) elsif ms >= 1000 "%5.2fs" % (ms/1000) else "%6.1f" % ms end end def aggreg(res) @backlog.last(res*@width).each_slice(res).map { |s| char s } end def char(ary) if ary.count(:down) == ary.size :down elsif ary.size > 0 && ary.count(:loss).to_f / ary.compact.size >= 0.05 :loss elsif ary.count(nil) == ary.size nil else avg(ary.grep(Numeric)) end end def desc @ping + " " + @host end end class Traf attr_reader :iface def initialize(iface, tx, opts={}) @iface = iface @width = opts[:width] || 60 @buffer = opts[:buffer] || 60**3 @backlog = Array.new(@buffer) @tx = tx @field = tx ? 6 : 5 end def run loop { IO.popen(["sar", "-n", "DEV", "1", :err=>[:child, :out]]) { |rd,wr| while line = rd.gets fields = line.split if fields[2] == @iface @backlog.shift @backlog << fields[@field].to_f end end } } end SYMBOLS = "\u2581\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2587" COLORS = %W{\e[1;32m \e[1;32m \e[0;32m \e[1;33m \e[0;33m \e[0;31m \e[1;31m \e[1;32m}.reverse OFFSETS = [ 0.9, 2, 50, 100, 1_000, 10_000, 20_000, 70_000 ] def avg(v) v.empty? ? 0 : v.inject(:+) / v.size end def stat(v) v2 = v.compact.find_all{|x| x!=:loss && x!=:down} [v2.min || 0.0, avg(v2), v2.max || 0.0] end def to_s(res=1) v = aggreg(res) min, avg, max = stat(v) line = v.last(@width).map { |d| if d == :loss "\e[1;30mx" elsif d == :down "\e[1;30mD" elsif d c = COLORS[OFFSETS.index { |cc| d < cc } || COLORS.size-1] if max == min c + SYMBOLS[SYMBOLS.size/2] else c + SYMBOLS[(((d-min) / (max-min))*SYMBOLS.size - 0.5).to_i] end else "\e[0;37m\u00B7" end }.join "\e[0;37m%s%s%s %s\e[0;37m" % [fmt(min), fmt(avg), fmt(max), line] end def fmt(kb) if kb >= 1000 "%5.1fM" % (kb/1024) else "%5.1fk" % kb end end def aggreg(res) @backlog.last(res*@width).each_slice(res).map { |s| char s } end def char(ary) if ary.count(:down) == ary.size :down elsif ary.size > 0 && ary.count(:loss).to_f / ary.compact.size >= 0.05 :loss elsif ary.count(nil) == ary.size nil else avg(ary.grep(Numeric)) end end def desc "#{iface} #{@tx ? "tx" : "rx"}" end end hosts = [] ping = "ping" ARGV.each { |h| case h when "-4" ping = "ping" when "-6" ping = "ping6" when /^traf:/ hosts << Traf.new($', true) hosts << Traf.new($', false) else hosts << Ping.new(h, :ping => ping) end } hosts.each { |h| Thread.new { h.run } } begin print "\e[H\e[2J\e[?25l" hostname = `hostname`.strip def puts(*s) $stdout.print("\e[K") $stdout.puts(*s) end loop { print "\e[H" puts "\e[1;37m%s" % "ncping 0.1 on #{hostname} - #{Time.now}".center(80) puts hosts.each { |h| puts "\e[1;37m%s" % h.desc puts("\e[1K" + h.to_s) puts h.to_s(60) puts h.to_s(60*60) # puts } print "\e[J" sleep 1 } rescue Interrupt exit ensure puts "\e[0;37m\e[?25h" end