dumbstats 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dumbstats.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Kurt Stephens
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Dumbstats
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'dumbstats'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install dumbstats
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/dumbstats.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/dumbstats/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Kurt Stephens"]
6
+ gem.email = ["ks.ruby@kurtstephens.com"]
7
+ gem.description = %q{Collect data, generate stats, draw histograms, send to Graphite, do stuff in Ruby. }
8
+ gem.summary = %q{Simple stats collection}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "dumbstats"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Dumbstats::VERSION
17
+ end
@@ -0,0 +1,129 @@
1
+ module Dumbstats
2
+ # Collects details.
3
+ class Bucket
4
+ include Initialization
5
+
6
+ KEYS =
7
+ [
8
+ :count,
9
+ :min,
10
+ :median,
11
+ :avg,
12
+ :stddev,
13
+ :max,
14
+ :sum,
15
+ :dt,
16
+ ]
17
+
18
+ attr_accessor *KEYS
19
+ attr_accessor :name, :values
20
+
21
+ def initialize *args
22
+ super
23
+ @count = 0
24
+ end
25
+
26
+ def to_a
27
+ h = [ ]
28
+ KEYS.each do | k |
29
+ v = instance_variable_get("@#{k}")
30
+ h << [ k, v ] if v
31
+ end
32
+ h
33
+ end
34
+
35
+ def count! x
36
+ @count += 1
37
+ self
38
+ end
39
+
40
+ def count_only?
41
+ ! @sum
42
+ end
43
+
44
+ def add! x
45
+ @values << x if @values
46
+ x = x.to_numeric if x.respond_to?(:to_numeric)
47
+ unless @min
48
+ @min = @max = x
49
+ else
50
+ @min = x if x < @min
51
+ @max = x if x > @max
52
+ end
53
+ @sum ||= 0
54
+ s = @sum += x
55
+ c = @count += 1
56
+ @avg = s.to_f / c
57
+ self
58
+ end
59
+
60
+ # Adds stat as a positive delta.
61
+ def add_delta! x0, x1
62
+ if x0 and (dx = x1 - x0) >= 0
63
+ add! dx
64
+ end
65
+ self
66
+ end
67
+
68
+ def empty?
69
+ ! @min || @max == @min
70
+ end
71
+
72
+ # Converts this to a rate over time.
73
+ def rate! dt
74
+ @dt = dt
75
+ @min = @max = nil
76
+ @avg = (@count || @sum).to_f / dt
77
+ self
78
+ end
79
+ alias_method :rate, :avg
80
+
81
+ def rate?
82
+ ! ! @dt
83
+ end
84
+
85
+ def finish!
86
+ if @dt
87
+ return self
88
+ end
89
+ if @count == 1
90
+ @min = @max = @avg = nil
91
+ end
92
+ if @avg && @values && ! @values.empty?
93
+ @values.sort!
94
+ n = @values.size
95
+ @median = @values[n / 2]
96
+ v = @values.map{|e| e = (e.to_numeric - @avg); e * e}
97
+ v.sort!
98
+ s = 0
99
+ v.each {|e| s += e }
100
+ @stddev = Math.sqrt(s.to_f / n)
101
+ end
102
+ self
103
+ end
104
+
105
+ def histogram *opts
106
+ opts << { :values => finish!.values }
107
+ Histogram.new(*opts).generate
108
+ end
109
+
110
+ def h opts = nil
111
+ h = histogram(opts)
112
+ $stdout.puts "# #{self.class} #{@name}"
113
+ to_a.each do | k, v |
114
+ $stdout.puts "#{k.inspect}: #{v}" if v
115
+ end
116
+ $stdout.puts h * "\n"
117
+ nil
118
+ end
119
+
120
+ def to_s opts = nil
121
+ h({:width => 50, :height => 40}.update(opts || {}))
122
+ end
123
+
124
+ def inspect
125
+ to_s
126
+ end
127
+ end # class
128
+ end # module
129
+
@@ -0,0 +1,82 @@
1
+ require 'thread' # Queue
2
+ require 'socket'
3
+
4
+ module Dumbstats
5
+ # Agent to dump Buckets into Graphite.
6
+ class Graphite
7
+ include Initialization
8
+
9
+ attr_accessor :host, :port
10
+
11
+ attr_accessor :prefix, :now
12
+
13
+ attr_accessor :running, :thread
14
+
15
+ attr_accessor :output_io
16
+
17
+ def initialize *opts
18
+ super
19
+ @q = Queue.new
20
+ end
21
+
22
+ def encode_path name
23
+ name.to_s.gsub(/[^a-z0-9_]/i, '-')
24
+ end
25
+
26
+ def add! name, value, now = nil, o = nil
27
+ o ||= EMPTY_Hash
28
+ now ||= self.now || Time.now.utc
29
+ name = encode_path(name)
30
+ enqueue! "#{prefix}#{o[:prefix]}#{name}#{o[:suffix]} #{value} #{now.to_i}\n"
31
+ self
32
+ end
33
+
34
+ def add_bucket! b, opts = nil
35
+ opts ||= EMPTY_Hash
36
+ now = opts[:now]
37
+ now ||= self.now || Time.now.utc
38
+ if b.rate?
39
+ add! b.name, b.rate, now, :prefix => opts[:prefix], :suffix => '.per_sec'
40
+ else
41
+ b.to_a.each do | k, v |
42
+ next if a = opts[:ignore] and a.include?(k)
43
+ add! b.name, v, now, :prefix => opts[:prefix], :suffix => '.' << encode_path(k)
44
+ end
45
+ end
46
+ end
47
+
48
+ def enqueue! data
49
+ @q << data
50
+ end
51
+
52
+ def run!
53
+ @running = true
54
+ @thread = Thread.current
55
+ while @running
56
+ send! @q.pop
57
+ end
58
+ self
59
+ end
60
+
61
+ def send! data
62
+ output_io.write data
63
+ end
64
+
65
+ def output_io
66
+ @output_io || socket
67
+ end
68
+
69
+ def socket
70
+ unless @socket
71
+ s = TCPSocket.new(host || '127.0.0.1', port || 2003) # CORRECT DEFAULT PORT?
72
+ @socket = s
73
+ end
74
+ @socket
75
+ rescue ::Exception => exc
76
+ STDERR.puts "#{self} socket: failed #{exc.inspect}\n #{exc.backtrace * "\n "}"
77
+ sleep 10
78
+ retry
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,137 @@
1
+ require 'dumbstats/stats'
2
+
3
+ gem 'terminal-table'
4
+ require 'terminal-table'
5
+
6
+ module Dumbstats
7
+ # Renders a Histogram of values.
8
+ class Histogram
9
+ include Initialization
10
+
11
+ attr_accessor :name, :values
12
+ attr_accessor :min, :max
13
+ attr_accessor :width, :height, :show_sum
14
+
15
+ def initialize *args
16
+ @min = @max = nil
17
+ @show_sum = true
18
+ super
19
+ @width ||= 15
20
+ @height ||= 20
21
+ end
22
+
23
+ def generate
24
+ raise TypeError, "@values not set" unless @values
25
+ return [ ] if @values.size < 2
26
+ @x_graph = Graph.new(:min => @min, :max => @max, :values => @values, :width => @width)
27
+ return [ ] if @x_graph.empty?
28
+ @x_graph.fix_width!
29
+
30
+ @buckets = Hash.new { |h, k| b = Bucket.new; b.name = k; h[k] = b }
31
+ @values.each do | v |
32
+ next if @min and v < @min
33
+ next if @max and v > @max
34
+ i = @x_graph.v_to_x(v).to_i
35
+ if i >= 0 and i < @x_graph.width
36
+ @buckets[i].add! v
37
+ end
38
+ end
39
+
40
+ cnt = @buckets.values.map { |b| b.count }
41
+ cnt << 0
42
+ @cnt_graph = Graph.new(:values => cnt, :width => @height)
43
+ return [ ] if @cnt_graph.empty?
44
+ # @cnt_graph.fix_width!
45
+
46
+ if @show_sum
47
+ sum = @buckets.values.map { |b| b.sum }
48
+ sum << 0
49
+ @sum_graph = Graph.new(:values => sum, :width => @height)
50
+ # @sum_graph.fix_width!
51
+ end
52
+
53
+ # binding.pry
54
+
55
+ rows = [ ]
56
+ table =
57
+ Terminal::Table.new() do | t |
58
+ t.title = @name
59
+ s = t.style
60
+ s.border_x =
61
+ s.border_y =
62
+ s.border_i = ''
63
+ s.padding_left = 0
64
+ s.padding_right = 1
65
+
66
+ # Header:
67
+ h = [ '<', '>', 'cnt', '%', "cnt h", "min", "avg", "max" ]
68
+ align_right = [ 0, 1, 2, 3, 5, 6, 7 ]
69
+ if @show_sum
70
+ h.push('sum', '%', 'sum h')
71
+ align_right.push(8, 9)
72
+ end
73
+ rows << h
74
+
75
+ cnt_sum = sum_sum = 0
76
+ @width.times do | i |
77
+ x0 = @x_graph.x_to_v(i).to_i
78
+ x1 = @x_graph.x_to_v(i + 1).to_i - 1
79
+ b = @buckets[i]
80
+ b.finish!
81
+
82
+ cnt_sum += b.count
83
+ r = [ ]
84
+ r << x0
85
+ r << x1
86
+ r << b.count
87
+ r << @cnt_graph.percent(b.count)
88
+ r << @cnt_graph.bar(b.count)
89
+ r << b.min
90
+ r << (b.avg && (@cnt_graph.values_are_integers ? b.avg.to_i : b.avg))
91
+ r << b.max
92
+ if @show_sum
93
+ sum_sum += b.sum || 0
94
+ r << b.sum
95
+ r << @sum_graph.percent(b.sum || 0)
96
+ r << @sum_graph.bar(b.sum || 0)
97
+ end
98
+ rows << r
99
+ end
100
+
101
+ f = [ '', '=', cnt_sum, '', '', '', '', '' ]
102
+ if @show_sum
103
+ f.push(sum_sum, '', '')
104
+ end
105
+ rows << f
106
+
107
+ rows.each do | r |
108
+ r.map! do | c |
109
+ case c
110
+ when nil
111
+ ''
112
+ when Integer
113
+ thousands(c)
114
+ else
115
+ c
116
+ end
117
+ end
118
+ t << r
119
+ end
120
+
121
+ raise unless h.size == f.size
122
+
123
+ align_right.each { | c | t.align_column(c, :right) }
124
+ end
125
+
126
+ formatted = table.to_s.split("\n")
127
+
128
+ formatted
129
+ end
130
+
131
+ def thousands x, sep = '_'
132
+ x && x.to_s.reverse!.gsub(/(\d{3})/, "\\1#{sep}").reverse!.sub(/^(\D|\A)#{sep}/, '')
133
+ end
134
+ end # class
135
+
136
+ end # module
137
+
@@ -0,0 +1,180 @@
1
+ require 'dumbstats/bucket'
2
+ require 'dumbstats/value'
3
+
4
+ module Dumbstats
5
+ # Collects stats by name into Buckets.
6
+ class Stats
7
+ include Initialization
8
+
9
+ attr_accessor :name
10
+ attr_accessor :chain
11
+ attr_accessor :verbose
12
+
13
+ def inspect
14
+ to_s
15
+ end
16
+
17
+ def initialize *opts
18
+ super
19
+ @s = { }
20
+ end
21
+
22
+ def stat k
23
+ @s[k] ||= Bucket.new(:name => k, :values => [ ])
24
+ end
25
+ def [] k
26
+ @s[k] || @s[k.to_sym]
27
+ end
28
+ def keys
29
+ @s.keys.sort_by{|s| s.to_s}
30
+ end
31
+ def each
32
+ keys.each do | k |
33
+ yield k, @s[k]
34
+ end
35
+ end
36
+ def clear!
37
+ @s.clear
38
+ end
39
+ def method_missing sel, *args
40
+ super unless args.empty? and ! block_given? and @s[sel]
41
+ end
42
+
43
+ def count! k, v = 1
44
+ $stderr.puts " count! #{k.inspect} #{v.inspect}" if @verbose
45
+ b = stat(k)
46
+ b.count! v
47
+ @chain.count! k, v if @chain
48
+ self
49
+ end
50
+
51
+ def add! k, v
52
+ $stderr.puts " add! #{k.inspect} #{v.inspect}" if @verbose
53
+ b = stat(k)
54
+ b.add! v
55
+ @chain.add! k, v if @chain
56
+ self
57
+ end
58
+
59
+ def add_delta! k, v0, v1
60
+ $stderr.puts " add_delta! #{k.inspect} #{v0.inspect}, #{v1.inspect}" if @verbose
61
+ b = stat(k)
62
+ b.add_delta! v0, v1
63
+ @chain.add_delta! k, v0, v1 if @chain
64
+ self
65
+ end
66
+
67
+ def finish!
68
+ @s.values.each{|b| b.finish!}
69
+ self
70
+ end
71
+
72
+ def put opts = { }
73
+ o = opts[:output] || $stdout
74
+ show_histogram = opts[:show_histogram]
75
+ ks = @s.keys.sort_by{|e| e.to_s}
76
+ ks.each do | k |
77
+ c = @s[k]
78
+ c.finish!
79
+ if c.count_only?
80
+ o.puts " :'#{k}': #{c.count}"
81
+ next
82
+ end
83
+ histogram = nil
84
+ if show_histogram and values = c.values and ! values.empty?
85
+ histogram = c.histogram(:width => 30, :height => 15)
86
+ histogram = nil if histogram.empty?
87
+ end
88
+ o.puts " #{k.to_sym.inspect}:"
89
+ c.to_a.each do | k, v |
90
+ o.puts " #{k.to_sym.inspect}: #{v.inspect}"
91
+ end
92
+ if histogram
93
+ o.puts " :histogram:"
94
+ histogram.each do | l |
95
+ o.puts " - #{l.inspect}"
96
+ end
97
+ end
98
+ end
99
+ self
100
+ end
101
+
102
+ def h
103
+ put
104
+ nil
105
+ end
106
+
107
+ # A Graph of values.
108
+ class Graph < Bucket
109
+ include Initialization
110
+ attr_accessor :values, :width
111
+ attr_accessor :values_are_integers
112
+
113
+ def initialize *args
114
+ super
115
+ @force_min, @force_max = @min, @max
116
+ if @values
117
+ values = @values
118
+ @values = [ ]
119
+ values.each { | v | add! v }
120
+ finish!
121
+ end
122
+ @width ||= 20
123
+ end
124
+
125
+ def fix_width!
126
+ @width = 1 if @width < 1
127
+ @max = x_to_v(@width + 1)
128
+ @max_min = (@max - @min).to_f
129
+ self
130
+ end
131
+
132
+ def finish!
133
+ super
134
+ return nil if empty?
135
+ @min = @force_min if @force_min
136
+ @max = @force_max if @force_max
137
+ @max_min = @max - @min
138
+ @values_are_integers = @values.all?{|e| Integer === e.to_numeric}
139
+ if @values_are_integers
140
+ if @width > @max_min
141
+ @width = @max_min.to_i
142
+ else
143
+ @max_min = @max_min.to_f
144
+ end
145
+ end
146
+ self
147
+ end
148
+
149
+ def sum!
150
+ finish! unless @sum
151
+ @sum
152
+ end
153
+
154
+ def percent value
155
+ '%5.1f%%' % (value * 100 / sum!.to_f)
156
+ end
157
+
158
+ def bar value
159
+ x = v_to_x(value).to_i
160
+ binding.pry if x < 0
161
+ if value > @min and x < 1
162
+ bar = '.'
163
+ else
164
+ bar = "*" * x
165
+ end
166
+ bar = "#{bar}#{' ' * (@width - bar.size)}"
167
+ bar
168
+ end
169
+
170
+ def v_to_x v
171
+ (v - @min) * @width / @max_min # = x
172
+ end
173
+
174
+ def x_to_v x
175
+ (x * @max_min / @width) + @min # = v
176
+ end
177
+ end
178
+ end # class
179
+ end # module
180
+
@@ -0,0 +1,18 @@
1
+ module Dumbstats
2
+ # A value that associates a numeric value with an aribitrary object.
3
+ class Value < Struct.new(:value, :object)
4
+ include Comparable
5
+ def <=> x
6
+ value <=> x.value
7
+ end
8
+ alias :to_numeric :value
9
+ end
10
+ end # module
11
+
12
+ # Support for Value#to_numeric.
13
+ class Numeric
14
+ def to_numeric
15
+ self
16
+ end
17
+ end
18
+
@@ -0,0 +1,3 @@
1
+ module Dumbstats
2
+ VERSION = "0.0.1"
3
+ end
data/lib/dumbstats.rb ADDED
@@ -0,0 +1,35 @@
1
+ require "dumbstats/version"
2
+
3
+ module Dumbstats
4
+ module Initialization
5
+ def update_from_hash! opts
6
+ if opts
7
+ opts.each do | k , v |
8
+ send(:"#{k}=", v)
9
+ end
10
+ end
11
+ self
12
+ end
13
+
14
+ def initialize *args
15
+ super()
16
+ opts = nil
17
+ if args.size == 1
18
+ opts = args.first
19
+ else
20
+ args.each do | a |
21
+ opts ||= { }
22
+ opts.update(a) if a
23
+ end
24
+ end
25
+ update_from_hash! opts
26
+ end
27
+ end # module
28
+
29
+ EMPTY_Hash = {}.freeze
30
+ EMPTY_Array = [].freeze
31
+ EMPTY_String = ''.freeze
32
+ end # module
33
+
34
+ require 'dumbstats/stats'
35
+
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dumbstats
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Kurt Stephens
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-08-03 00:00:00 Z
19
+ dependencies: []
20
+
21
+ description: "Collect data, generate stats, draw histograms, send to Graphite, do stuff in Ruby. "
22
+ email:
23
+ - ks.ruby@kurtstephens.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - .gitignore
32
+ - Gemfile
33
+ - LICENSE
34
+ - README.md
35
+ - Rakefile
36
+ - dumbstats.gemspec
37
+ - lib/dumbstats.rb
38
+ - lib/dumbstats/bucket.rb
39
+ - lib/dumbstats/graphite.rb
40
+ - lib/dumbstats/histogram.rb
41
+ - lib/dumbstats/stats.rb
42
+ - lib/dumbstats/value.rb
43
+ - lib/dumbstats/version.rb
44
+ homepage: ""
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options: []
49
+
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ hash: 3
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.17
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Simple stats collection
77
+ test_files: []
78
+