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 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: []