varnishops 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|