dumbstats 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/.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
+