varnishops 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ @regexs = {
2
+ }
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
@@ -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
@@ -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: []