varnishops 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/README.md +24 -0
- data/Rakefile +1 -0
- data/bin/varnishops +66 -0
- data/config.rb +2 -0
- data/ext/config.rb +17 -0
- data/lib/cmdline.rb +47 -0
- data/lib/ui.rb +193 -0
- data/lib/varnish_pipe.rb +75 -0
- data/varnishops.gemspec +17 -0
- metadata +57 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# varnishops
|
2
|
+
|
3
|
+
varnishops is a tool for analyzing and categorizing traffic on your varnish servers in realtime.
|
4
|
+
|
5
|
+
It gathers the output of varnishncsa and create statistics such as request rate, output bandwidth or hitratio.
|
6
|
+
|
7
|
+
URLs from varnishncsa are categorized using custom regular expressions, based on your assets' paths and classification (example provided in **ext/**)
|
8
|
+
|
9
|
+
varnishops is written in Ruby (tested with MRI 1.9.3) and depends only on varnishncsa.
|
10
|
+
|
11
|
+
## Setup
|
12
|
+
|
13
|
+
* git clone https://github.com/Fotolia/varnishops
|
14
|
+
OR
|
15
|
+
* gem install varnishops
|
16
|
+
|
17
|
+
|
18
|
+
By default, all URLs will be categorized as "other", and statistics will be computed based on the global varnish traffic
|
19
|
+
|
20
|
+
Categories can be added by defining filters (ruby regexps). See example file in **ext/**.
|
21
|
+
|
22
|
+
## Credits
|
23
|
+
|
24
|
+
varnishops is heavily inspired by [mctop](http://github.com/etsy/mctop) from Etsy's folks.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/varnishops
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__),'..','lib')
|
4
|
+
|
5
|
+
require 'cmdline'
|
6
|
+
require 'varnish_pipe'
|
7
|
+
require 'ui'
|
8
|
+
|
9
|
+
@config = CmdLine.parse(ARGV)
|
10
|
+
|
11
|
+
@regexs = {}
|
12
|
+
# load regex config file
|
13
|
+
begin
|
14
|
+
load @config[:config_file]
|
15
|
+
rescue Exception => e
|
16
|
+
puts "Unable to load config.rb (#{e.message})"
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
20
|
+
pipe = VarnishPipe.new(@config, @regexs)
|
21
|
+
ui = UI.new(@config)
|
22
|
+
|
23
|
+
done = false
|
24
|
+
|
25
|
+
# trap most of the typical signals
|
26
|
+
%w[ INT QUIT HUP KILL ].each do |sig|
|
27
|
+
Signal.trap(sig) do
|
28
|
+
puts "** Caught signal #{sig} - exiting"
|
29
|
+
done = true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# kick the pipe thread off
|
34
|
+
pipe_thr = Thread.new { pipe.start }
|
35
|
+
|
36
|
+
# main loop
|
37
|
+
until done do
|
38
|
+
ui.header
|
39
|
+
ui.footer
|
40
|
+
ui.render_stats(pipe)
|
41
|
+
refresh
|
42
|
+
|
43
|
+
key = ui.input_handler
|
44
|
+
case key
|
45
|
+
when /[Qq]/
|
46
|
+
done = true
|
47
|
+
when /[Kk]/
|
48
|
+
ui.sort_mode = :key
|
49
|
+
when /[Cc]/
|
50
|
+
ui.sort_mode = :calls
|
51
|
+
when /[Rr]/
|
52
|
+
ui.sort_mode = :reqps
|
53
|
+
when /[Bb]/
|
54
|
+
ui.sort_mode = :bps
|
55
|
+
when /[Hh]/
|
56
|
+
ui.sort_mode = :hitratio
|
57
|
+
when /[Pp]/
|
58
|
+
ui.show_percent = !ui.show_percent
|
59
|
+
when /[Tt]/
|
60
|
+
ui.sort_order = ui.sort_order == :desc ? :asc : :desc
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# stop threads
|
65
|
+
ui.stop
|
66
|
+
pipe.stop
|
data/config.rb
ADDED
data/ext/config.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Example regex file, expressions order matters !
|
2
|
+
@regexs = {
|
3
|
+
/^(images)\/.*_(\d+)\.(png)$/ => [1,3,2]
|
4
|
+
/^\/(\w+)\/.*\.(\w+)$/ => [1, 2],
|
5
|
+
}
|
6
|
+
|
7
|
+
# will match URLs and display them such as:
|
8
|
+
#
|
9
|
+
# /images/11/22/logo_400.png => images:png:400
|
10
|
+
#
|
11
|
+
# first, then:
|
12
|
+
#
|
13
|
+
# /assets/your/own/path/app.js => assets:js
|
14
|
+
#
|
15
|
+
# /images/00/11/banner.jpg => images:jpg
|
16
|
+
#
|
17
|
+
# validate these expressions using the fine http://rubular.com/
|
data/lib/cmdline.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class CmdLine
|
4
|
+
def self.parse(args)
|
5
|
+
@config = {}
|
6
|
+
|
7
|
+
opts = OptionParser.new do |opt|
|
8
|
+
@config[:config_file] = File.join(File.dirname(File.dirname(__FILE__)), 'config.rb')
|
9
|
+
opt.on '-c', '--config-file=FILE', String, 'Config file containing regular expressions to categorize traffic' do |config_file|
|
10
|
+
@config[:config_file] = File.expand_path(config_file)
|
11
|
+
end
|
12
|
+
|
13
|
+
@config[:refresh_rate] = 500
|
14
|
+
opt.on '-r', '--refresh=MS', Integer, 'Refresh the stats display every MS milliseconds' do |refresh_rate|
|
15
|
+
@config[:refresh_rate] = refresh_rate
|
16
|
+
end
|
17
|
+
|
18
|
+
@config[:avg_period] = 60
|
19
|
+
opt.on '-a', '--avg-period=SEC', Integer, 'Compute averages over SEC seconds' do |avg_period|
|
20
|
+
@config[:avg_period] = avg_period
|
21
|
+
end
|
22
|
+
|
23
|
+
opt.on_tail '-h', '--help', 'Show usage info' do
|
24
|
+
puts opts
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.parse!
|
30
|
+
|
31
|
+
# limit data structures size
|
32
|
+
@config[:avg_period] = 600 if @config[:avg_period] > 600
|
33
|
+
@config[:avg_period] = 10 if @config[:avg_period] < 10
|
34
|
+
|
35
|
+
unless File.exists?(@config[:config_file])
|
36
|
+
puts "#{@config[:config_file]}: no such file"
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
|
40
|
+
unless system("which varnishncsa")
|
41
|
+
puts "varnishncsa not found. install it or set a correct PATH"
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
|
45
|
+
@config
|
46
|
+
end
|
47
|
+
end
|
data/lib/ui.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
include Curses
|
4
|
+
|
5
|
+
class UI
|
6
|
+
attr_accessor :sort_order, :sort_mode, :show_percent
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
|
11
|
+
@stat_cols = %w[ calls req/s kB/s hitratio ]
|
12
|
+
@stat_col_width = 15
|
13
|
+
@key_col_width = 30
|
14
|
+
@avg_period = @config[:avg_period]
|
15
|
+
@sort_mode = :reqps
|
16
|
+
@sort_order = :desc
|
17
|
+
@show_percent = false
|
18
|
+
|
19
|
+
@commands = {
|
20
|
+
'B' => "sort by bandwidth",
|
21
|
+
'C' => "sort by call number",
|
22
|
+
'H' => "sort by hitratio",
|
23
|
+
'K' => "sort by key",
|
24
|
+
'P' => "display percentages",
|
25
|
+
'Q' => "quit",
|
26
|
+
'R' => "sort by request rate",
|
27
|
+
'T' => "toggle sort order (asc|desc)"
|
28
|
+
}
|
29
|
+
|
30
|
+
init_screen
|
31
|
+
cbreak
|
32
|
+
curs_set(0)
|
33
|
+
|
34
|
+
# set keyboard input timeout - sneaky way to manage refresh rate
|
35
|
+
Curses.timeout = @config[:refresh_rate]
|
36
|
+
|
37
|
+
if can_change_color?
|
38
|
+
start_color
|
39
|
+
init_pair(0, COLOR_WHITE, COLOR_BLACK)
|
40
|
+
init_pair(1, COLOR_WHITE, COLOR_BLUE)
|
41
|
+
init_pair(2, COLOR_WHITE, COLOR_RED)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def header
|
46
|
+
# pad stat columns to @stat_col_width
|
47
|
+
@stat_cols = @stat_cols.map { |c| sprintf("%#{@stat_col_width}s", c) }
|
48
|
+
|
49
|
+
@url_col_width = cols - @key_col_width - (@stat_cols.length * @stat_col_width)
|
50
|
+
|
51
|
+
attrset(color_pair(1))
|
52
|
+
setpos(0,0)
|
53
|
+
addstr(sprintf "%-#{@key_col_width}s%-#{@url_col_width}s%s", "request pattern", "last url", @stat_cols.join)
|
54
|
+
end
|
55
|
+
|
56
|
+
def footer
|
57
|
+
footer_text = @commands.map { |k,v| "#{k}:#{v}" }.join(' | ')
|
58
|
+
setpos(lines - 1, 0)
|
59
|
+
attrset(color_pair(2))
|
60
|
+
addstr(sprintf "%-#{cols}s", footer_text)
|
61
|
+
end
|
62
|
+
|
63
|
+
def render_stats(pipe)
|
64
|
+
render_start_t = Time.now.to_f * 1000
|
65
|
+
|
66
|
+
# subtract header + footer lines
|
67
|
+
maxlines = lines - 3
|
68
|
+
offset = 1
|
69
|
+
|
70
|
+
# construct and render footer stats line
|
71
|
+
setpos(lines - 2, 0)
|
72
|
+
attrset(color_pair(2))
|
73
|
+
header_summary = sprintf "%-24s %-14s %-14s %-14s %-14s",
|
74
|
+
"sort mode: #{@sort_mode.to_s} (#{@sort_order.to_s})",
|
75
|
+
"| requests: #{pipe.stats[:total_reqs]}",
|
76
|
+
"| bytes out: #{pipe.stats[:total_bytes]}",
|
77
|
+
"| duration: #{Time.now.to_i - pipe.start_time.to_i}s",
|
78
|
+
"| samples: #{@avg_period}"
|
79
|
+
addstr(sprintf "%-#{cols}s", header_summary)
|
80
|
+
|
81
|
+
# reset colours for main key display
|
82
|
+
attrset(color_pair(0))
|
83
|
+
|
84
|
+
top = []
|
85
|
+
totals = {}
|
86
|
+
|
87
|
+
pipe.semaphore.synchronize do
|
88
|
+
# we may have seen no packets received on the pipe thread
|
89
|
+
return if pipe.start_time.nil?
|
90
|
+
|
91
|
+
time_since_start = Time.now.to_f - pipe.start_time
|
92
|
+
elapsed = time_since_start < @avg_period ? time_since_start : @avg_period
|
93
|
+
|
94
|
+
# calculate hits+misses, request rate, bandwidth and hitratio
|
95
|
+
pipe.stats[:requests].each do |key,values|
|
96
|
+
total_hits = values[:hit].inject(:+)
|
97
|
+
total_calls = total_hits + values[:miss].inject(:+)
|
98
|
+
total_bytes = values[:bytes].inject(:+)
|
99
|
+
|
100
|
+
pipe.stats[:calls][key] = total_calls
|
101
|
+
pipe.stats[:reqps][key] = total_calls.to_f / elapsed
|
102
|
+
pipe.stats[:bps][key] = total_bytes.to_f / elapsed
|
103
|
+
pipe.stats[:hitratio][key] = total_hits.to_f * 100 / total_calls
|
104
|
+
end
|
105
|
+
|
106
|
+
if @sort_mode == :key
|
107
|
+
top = pipe.stats[:requests].sort { |a,b| a[0] <=> b[0] }
|
108
|
+
else
|
109
|
+
top = pipe.stats[@sort_mode].sort { |a,b| a[1] <=> b[1] }
|
110
|
+
end
|
111
|
+
|
112
|
+
totals = {
|
113
|
+
:calls => pipe.stats[:calls].values.inject(:+),
|
114
|
+
:reqps => pipe.stats[:reqps].values.inject(:+),
|
115
|
+
:bps => pipe.stats[:bps].values.inject(:+)
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
unless @sort_order == :asc
|
120
|
+
top.reverse!
|
121
|
+
end
|
122
|
+
|
123
|
+
for i in 0..maxlines-1
|
124
|
+
if i < top.length
|
125
|
+
k = top[i][0]
|
126
|
+
|
127
|
+
# if the key is too wide for the column truncate it and add an ellipsis
|
128
|
+
display_key = k.length > @key_col_width ? "#{k[0..@key_col_width-4]}..." : k
|
129
|
+
display_url = pipe.stats[:requests][k][:last]
|
130
|
+
display_url = display_url.length > @url_col_width ? "#{display_url[0..@url_col_width-4]}..." : display_url
|
131
|
+
|
132
|
+
# render each key
|
133
|
+
if @show_percent
|
134
|
+
line = sprintf "%-#{@key_col_width}s%-#{@url_col_width}s %14.2f %14.2f %14.2f %14.2f",
|
135
|
+
display_key, display_url,
|
136
|
+
pipe.stats[:calls][k].to_f / totals[:calls] * 100,
|
137
|
+
pipe.stats[:reqps][k] / totals[:reqps] * 100,
|
138
|
+
pipe.stats[:bps][k] / totals[:bps] * 100,
|
139
|
+
pipe.stats[:hitratio][k]
|
140
|
+
else
|
141
|
+
line = sprintf "%-#{@key_col_width}s%-#{@url_col_width}s %14.d %14.2f %14.2f %14.2f",
|
142
|
+
display_key, display_url,
|
143
|
+
pipe.stats[:calls][k],
|
144
|
+
pipe.stats[:reqps][k],
|
145
|
+
pipe.stats[:bps][k] / 1000,
|
146
|
+
pipe.stats[:hitratio][k]
|
147
|
+
end
|
148
|
+
else
|
149
|
+
# clear remaining lines
|
150
|
+
line = " "*cols
|
151
|
+
end
|
152
|
+
|
153
|
+
setpos(1 + i, 0)
|
154
|
+
addstr(line)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Display column totals
|
158
|
+
unless @show_percent
|
159
|
+
setpos(top.length + 2, 0)
|
160
|
+
line = sprintf "%-#{@key_col_width + @url_col_width}s %14.d %14.2f %14.2f",
|
161
|
+
"TOTAL", totals[:calls], totals[:reqps], totals[:bps] / 1000
|
162
|
+
addstr(line)
|
163
|
+
end
|
164
|
+
|
165
|
+
# print render time in status bar
|
166
|
+
runtime = (Time.now.to_f * 1000) - render_start_t
|
167
|
+
attrset(color_pair(2))
|
168
|
+
setpos(lines - 2, cols - 24)
|
169
|
+
addstr(sprintf "render time: %4.3f (ms)", runtime)
|
170
|
+
end
|
171
|
+
|
172
|
+
def input_handler
|
173
|
+
# Curses.getch has a bug in 1.8.x causing non-blocking
|
174
|
+
# calls to block reimplemented using IO.select
|
175
|
+
if RUBY_VERSION =~ /^1.8/
|
176
|
+
refresh_secs = @config[:refresh_rate].to_f / 1000
|
177
|
+
|
178
|
+
if IO.select([STDIN], nil, nil, refresh_secs)
|
179
|
+
c = getch
|
180
|
+
c.chr
|
181
|
+
else
|
182
|
+
nil
|
183
|
+
end
|
184
|
+
else
|
185
|
+
getch
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def stop
|
190
|
+
nocbreak
|
191
|
+
close_screen
|
192
|
+
end
|
193
|
+
end
|
data/lib/varnish_pipe.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
class VarnishPipe
|
2
|
+
attr_accessor :stats, :semaphore, :start_time
|
3
|
+
|
4
|
+
def initialize(config, regexs)
|
5
|
+
@stats = {
|
6
|
+
:total_reqs => 0,
|
7
|
+
:total_bytes => 0,
|
8
|
+
:requests => {},
|
9
|
+
:calls => {},
|
10
|
+
:hitratio => {},
|
11
|
+
:reqps => {},
|
12
|
+
:bps => {},
|
13
|
+
}
|
14
|
+
|
15
|
+
@semaphore = Mutex.new
|
16
|
+
@avg_period = config[:avg_period]
|
17
|
+
@default_key = "other"
|
18
|
+
|
19
|
+
@regexs = regexs
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
@stop = false
|
24
|
+
@start_time = Time.new.to_f
|
25
|
+
@start_ts = @start_time.to_i
|
26
|
+
|
27
|
+
IO.popen("varnishncsa -F '%U %{Varnish:hitmiss}x %b'").each_line do |line|
|
28
|
+
if line =~ /^(\S+) (\w+) (\d+)$/
|
29
|
+
url, status, bytes = $1, $2, $3
|
30
|
+
key = nil
|
31
|
+
|
32
|
+
@regexs.each do |k,v|
|
33
|
+
if k.match(url)
|
34
|
+
key = v.map{|x| "#{$~[x]}" }.join(":")
|
35
|
+
break
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
key = @default_key unless key
|
40
|
+
|
41
|
+
@semaphore.synchronize do
|
42
|
+
duration = (Time.now.to_i - @start_ts)
|
43
|
+
it = duration / @avg_period
|
44
|
+
idx = duration % @avg_period
|
45
|
+
|
46
|
+
@stats[:requests][key] ||= {
|
47
|
+
:hit => Array.new(@avg_period, 0),
|
48
|
+
:miss => Array.new(@avg_period, 0),
|
49
|
+
:bytes => Array.new(@avg_period, 0),
|
50
|
+
:it => Array.new(@avg_period, 0),
|
51
|
+
:last => ""
|
52
|
+
}
|
53
|
+
|
54
|
+
cur_req = @stats[:requests][key]
|
55
|
+
if cur_req[:it][idx] < it
|
56
|
+
cur_req[status.to_sym][idx] = 0
|
57
|
+
cur_req[:bytes][idx] = 0
|
58
|
+
end
|
59
|
+
cur_req[status.to_sym][idx] += 1
|
60
|
+
cur_req[:bytes][idx] += bytes.to_i
|
61
|
+
cur_req[:it][idx] = it
|
62
|
+
cur_req[:last] = url
|
63
|
+
@stats[:total_reqs] += 1
|
64
|
+
@stats[:total_bytes] += bytes.to_i
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
break if @stop
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def stop
|
73
|
+
@stop = true
|
74
|
+
end
|
75
|
+
end
|
data/varnishops.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "varnishops"
|
7
|
+
gem.version = "0.0.1"
|
8
|
+
gem.authors = ["Jonathan Amiez"]
|
9
|
+
gem.email = ["jonathan.amiez@fotolia.com"]
|
10
|
+
gem.description = %q{varnishops - a realtime varnish log analyzer}
|
11
|
+
gem.summary = %q{varnishops - an interactive terminal app for analyzing varnish activity with hitratio, bandwidth and request rate per type of files}
|
12
|
+
gem.homepage = "https://github.com/Fotolia/varnishops"
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: varnishops
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jonathan Amiez
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-10 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: varnishops - a realtime varnish log analyzer
|
15
|
+
email:
|
16
|
+
- jonathan.amiez@fotolia.com
|
17
|
+
executables:
|
18
|
+
- varnishops
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- Gemfile
|
23
|
+
- README.md
|
24
|
+
- Rakefile
|
25
|
+
- bin/varnishops
|
26
|
+
- config.rb
|
27
|
+
- ext/config.rb
|
28
|
+
- lib/cmdline.rb
|
29
|
+
- lib/ui.rb
|
30
|
+
- lib/varnish_pipe.rb
|
31
|
+
- varnishops.gemspec
|
32
|
+
homepage: https://github.com/Fotolia/varnishops
|
33
|
+
licenses: []
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ! '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
requirements: []
|
51
|
+
rubyforge_project:
|
52
|
+
rubygems_version: 1.8.25
|
53
|
+
signing_key:
|
54
|
+
specification_version: 3
|
55
|
+
summary: varnishops - an interactive terminal app for analyzing varnish activity with
|
56
|
+
hitratio, bandwidth and request rate per type of files
|
57
|
+
test_files: []
|