multi-statsd 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,18 @@
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
18
+ dump.rdb
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ services:
3
+ - redis-server
4
+ rvm:
5
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in multi-statsd.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Kelley Reynolds
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,53 @@
1
+ # MultiStatsd [![Build Status](https://travis-ci.org/bigcartel/multi-statsd.png?branch=master)](https://travis-ci.org/bigcartel/multi-statsd) [![Code Climate](https://codeclimate.com/github/bigcartel/multi-statsd.png)](https://codeclimate.com/github/bigcartel/multi-statsd)
2
+
3
+ An eventmachine-based statsd server written in ruby with a modular backend system.
4
+ The motivation behind creating Yet Another Statsd Server was that while the existing ones allowed
5
+ you to specify different backends to flush data to, it was always aggregated the same way and flushed on the same interval.
6
+ This library incurs the overhead of multiple copies of your statistics with the tradeoff that you
7
+ can have backend-specific flush intervals and alternate aggregations of your data.
8
+
9
+ ## Installation
10
+
11
+ $ gem install multi-statsd
12
+
13
+ ## Configuration
14
+
15
+ Configuration is done via a YAML file which is specified on the command line. Example config file:
16
+
17
+ # Host and port to listen on
18
+ host: 127.0.0.1
19
+ port: 8125
20
+ verbosity: 1 # 0-4, 0 being the most verbose
21
+ logfile: /var/log/multi-statsd.log
22
+ pidfile: /var/run/multi-statsd.pid
23
+ daemonize: true
24
+ backends:
25
+ stdout_every_5:
26
+ backend: Stdout
27
+ flush_interval: 5
28
+ hottie:
29
+ backend: Hottie
30
+ host: 127.0.0.1
31
+ port: 6379
32
+ flush_interval: 1
33
+ seconds_to_retain: 60
34
+
35
+
36
+ ## Usage
37
+
38
+ Usage is straight forward:
39
+
40
+ $ multi-statsd -c /path/to/config
41
+
42
+ ## Todo
43
+
44
+ 1. Create graphite backend
45
+ 2. Create relay backend
46
+
47
+ ## Contributing
48
+
49
+ 1. Fork it
50
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
51
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
52
+ 4. Push to the branch (`git push origin my-new-feature`)
53
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |t|
5
+ t.rspec_opts = %w[--color]
6
+ t.pattern = 'spec/**/*_spec.rb'
7
+ end
8
+
9
+ task :default => [:spec]
data/bin/multi-statsd ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+ require 'yaml'
4
+ require 'optparse'
5
+
6
+ options = {}
7
+
8
+ parser = OptionParser.new do |opts|
9
+ opts.banner = "Usage: multi-statsd -c CONFIG_FILE"
10
+
11
+ opts.separator ""
12
+ opts.separator "Options:"
13
+
14
+ opts.on("-c CONFIG_FILE", "--config-file CONFIG_FILE", "Configuration file") do |config_file|
15
+ options = YAML.load_file(config_file).merge(options)
16
+ end
17
+
18
+ opts.on("-d", "--debug", "Shortcut for (daemonize: false, verbosity: 0, log: stdout)") do
19
+ options['daemonize'] = false
20
+ options['verbosity'] = 0
21
+ options['logfile'] = nil
22
+ end
23
+
24
+ opts.on_tail("-h", "--help", "Show this message") do
25
+ puts opts
26
+ exit
27
+ end
28
+
29
+ end
30
+
31
+ parser.parse!
32
+
33
+ raise OptionParser::MissingArgument, "A config file must be specified" if options.empty?
34
+ raise "At least one backend must be specified" if !options.has_key?('backends') or options['backends'].empty?
35
+
36
+ require 'multi-statsd'
37
+
38
+ Signal.trap("INT") { MultiStatsd.stop }
39
+ Signal.trap("TERM") { MultiStatsd.stop }
40
+
41
+ log = options['logfile'] ? Logger.new(options['logfile']) : Logger.new(STDOUT)
42
+ log.level = options['verbosity'] || 2
43
+ MultiStatsd.logger = log
44
+
45
+ # Daemonize if requested
46
+ Process.daemon if options['daemonize']
47
+
48
+ # Write pidfile if requested
49
+ if options['pidfile']
50
+ begin
51
+ File.open(options['pidfile'], File::WRONLY | File::APPEND | File::CREAT) { |fp| fp.write Process.pid }
52
+ rescue
53
+ log.error "Unable to write pid file: #{$!.to_s}"
54
+ end
55
+ end
56
+
57
+ MultiStatsd.start(options).join
@@ -0,0 +1,17 @@
1
+ # Host and port to listen on
2
+ host: 127.0.0.1
3
+ port: 8125
4
+ verbosity: 1 # 0-4, 0 being the most verbose
5
+ # logfile: /var/log/multi-statsd.log
6
+ # pidfile: /var/run/multi-statsd.pid
7
+ # daemonize: false
8
+ backends:
9
+ stdout_every_3:
10
+ backend: Stdout
11
+ flush_interval: 3
12
+ # hottie:
13
+ # backend: Hottie
14
+ # host: 127.0.0.1
15
+ # port: 6379
16
+ # flush_interval: 1
17
+ # seconds_to_retain: 60
@@ -0,0 +1,69 @@
1
+ require 'logger'
2
+ require 'eventmachine'
3
+ require 'em-logger'
4
+
5
+ require "multi-statsd/version"
6
+ require 'multi-statsd/backends/base'
7
+ require 'multi-statsd/server'
8
+
9
+ # MultiStatsd namespace
10
+ module MultiStatsd
11
+ # Assign a logger
12
+ # @param [Logger] logger
13
+ def self.logger=(logger)
14
+ @logger = logger.kind_of?(EM::Logger) ? logger : EM::Logger.new(logger)
15
+ end
16
+
17
+ # Return the logger
18
+ # @return [Logger]
19
+ def self.logger
20
+ return @logger if defined?(@logger)
21
+ log = Logger.new(STDOUT)
22
+ log.level = 2
23
+ @logger = EM::Logger.new(log)
24
+ end
25
+
26
+ # Start up the Eventmachine reactor loop in a separate thread give a set of options.
27
+ # This is a test
28
+ # This is another test
29
+ # @param [Hash] options Set of options
30
+ # @return [thread] Thread which is running the eventmachine loop
31
+ def self.start(options)
32
+ thread = Thread.new do
33
+ EM.run do
34
+ backends = []
35
+ options['backends'].each_pair do |name, options|
36
+ backend = options.delete('backend')
37
+ begin
38
+ require "multi-statsd/backends/#{backend.downcase}"
39
+ rescue LoadError
40
+ raise MultiStatsd::Backend::Error, "Cannot load file multi-statsd/backends/#{backend.downcase}"
41
+ end
42
+ if !MultiStatsd::Backend.const_defined?(backend)
43
+ raise MultiStatsd::Backend::Error, "No such back end: MultiStatsd::Backend::#{backend}"
44
+ else
45
+ logger.info "Adding backend #{backend} :: #{name}"
46
+ backends << MultiStatsd::Backend.const_get(backend).new(name, options)
47
+ end
48
+ end
49
+
50
+ EM::open_datagram_socket(
51
+ (options['host'] || '127.0.0.1'),
52
+ (options['port'] || 8125),
53
+ MultiStatsd::Server,
54
+ backends
55
+ )
56
+ logger.info "multi-statsd starting up on #{options['host']}:#{options['port']}"
57
+ end
58
+ end
59
+
60
+ thread.abort_on_exception = true
61
+ thread
62
+ end
63
+
64
+ # Stop the Eventmachine reactor loop
65
+ def self.stop
66
+ logger.info "multi-statsd shutting down"
67
+ EM.next_tick { EM.stop }
68
+ end
69
+ end
@@ -0,0 +1,105 @@
1
+ require 'benchmark'
2
+ require 'thread'
3
+
4
+ module MultiStatsd
5
+ # Various backends should be defined in this module
6
+ module Backend
7
+ # Error class for MultiStatsd-generated errors
8
+ class Error < StandardError; end
9
+
10
+ # @abstract Subclass and override {#flush} to implement a custom Backend class.
11
+ class Base
12
+ attr_reader :counters, :timers, :gauges, :name
13
+
14
+ def initialize(name, options = {})
15
+ @name, @options = name, options
16
+ @timers, @gauges, @counters = {}, {}, Hash.new(0)
17
+ @semaphore = Mutex.new
18
+
19
+ @options['flush_interval'] ||= 15
20
+
21
+ post_init
22
+
23
+ EventMachine::add_periodic_timer(@options['flush_interval']) do
24
+ EM.defer { flush }
25
+ end
26
+ end
27
+
28
+ # @return [Integer] the flush interval
29
+ def flush_interval
30
+ @options['flush_interval']
31
+ end
32
+
33
+ # Override in subclasses to execute code after initialization (eg. database connection setup)
34
+ def post_init
35
+ end
36
+
37
+ # Each backend must implement this method to flush its data
38
+ def flush
39
+ raise NotImplementedError
40
+ end
41
+
42
+ # Reset and return the generated data in a mutex to ensure none are lost
43
+ # @return [Array] An array consisting of [counters, timers, gauges]
44
+ def reset_stats
45
+ @semaphore.synchronize do
46
+ counters = @counters.dup
47
+ timers = @timers.dup
48
+ gauges = @gauges.dup
49
+ @counters.clear
50
+ @timers.clear
51
+ [counters, timers, gauges]
52
+ end
53
+ end
54
+
55
+ # Record data in statsd format
56
+ # Gauges - cpu:0.15|g
57
+ # Timers - api:12|ms
58
+ # Counters - bytes:123|c
59
+ # Counters with sampling - bytes:123|c|@0.1
60
+ # Multiple values - api:12|ms:15|ms:8|ms
61
+ # @param [String] msg string of data in statsd format
62
+ # @return [true]
63
+ def write(msg)
64
+ msg.each_line do |row|
65
+ # Fetch our key and records
66
+ key, *records = row.split(":")
67
+
68
+ # Clean up the key formatting
69
+ key = format_key(key)
70
+
71
+ # Iterate through each record and store the data
72
+ records.each do |record|
73
+ value, type, sample_rate = record.split('|')
74
+ next unless value and type and value =~ /^(?:[\d\.-]+)$/
75
+
76
+ case type
77
+ when "ms"
78
+ @timers[key] ||= []
79
+ @timers[key].push(value.to_f)
80
+ when "c"
81
+ if sample_rate
82
+ sample_rate = sample_rate.gsub(/[^\d\.]/, '').to_f
83
+ sample_rate = 1 if sample_rate <= 0
84
+ @counters[key] += value.to_f * (1.0 / sample_rate)
85
+ else
86
+ @counters[key] += value.to_f
87
+ end
88
+ when "g"
89
+ @gauges[key] = value.to_f
90
+ end
91
+ end
92
+ end
93
+
94
+ true
95
+ end
96
+
97
+ # Format a given key
98
+ # @param [String] key from the statsd record
99
+ # @return [String] formatted key
100
+ def format_key(key)
101
+ key.gsub(/\s+/, '_').gsub(/\//, '-').gsub(/[^a-zA-Z_\-0-9\.]/, '')
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,62 @@
1
+ require 'redis'
2
+
3
+ # Hottie backend
4
+ # Hottie is a redis-based short-term backend to enable real-time visibility into application behavior.
5
+ # It is specifically designed to enable real-time heatmap/histogram visualizations from timer data.
6
+ class MultiStatsd::Backend::Hottie < MultiStatsd::Backend::Base
7
+ attr_reader :seconds_to_retain, :samples_to_retain
8
+
9
+ # Initialize a connection to redis and configure our samples to retain
10
+ def post_init
11
+ @db = Redis.new(
12
+ :host => (@options['host'] || '127.0.0.1'),
13
+ :port => (@options['port'] || 6379),
14
+ :database => (@options['database'] || 1)
15
+ )
16
+ @seconds_to_retain = @options['seconds_to_retain'] || 60
17
+ @samples_to_retain = (@seconds_to_retain / @options['flush_interval']).floor
18
+ end
19
+
20
+ # Flush the data to redis in the format required for Hottie
21
+ # @return [Float] The number of seconds it took to aggregate/flush the data to redis
22
+ def flush
23
+ counters, timers, gauges = reset_stats
24
+ ts = Time.new.to_i
25
+ time = ::Benchmark.realtime do
26
+ @db.pipelined do
27
+ if !gauges.empty?
28
+ @db.hmset "gauges:#{ts}", *(gauges.map { |stat, gauge| [stat, gauge] }.flatten)
29
+ @db.expire "gauges:#{ts}", @seconds_to_retain + 5 # Retain a few extra seconds to avoid reporting race
30
+ @db.sadd "gauges", gauges.keys
31
+ end
32
+ @db.lpush "gauge_samples", "gauges:#{ts}"
33
+ @db.ltrim "gauge_samples", 0, @samples_to_retain - 1
34
+
35
+ if !counters.empty?
36
+ @db.hmset "counters:#{ts}", *(counters.map { |stat, counter|[stat, counter / @options['flush_interval']] }.flatten)
37
+ @db.expire "counters:#{ts}", @seconds_to_retain + 5 # Retain a few extra seconds to avoid reporting race
38
+ @db.sadd "counters", counters.keys
39
+ end
40
+ @db.lpush "counter_samples", "counters:#{ts}"
41
+ @db.ltrim "counter_samples", 0, @samples_to_retain - 1
42
+
43
+ if !timers.empty?
44
+ timer_hash = Hash.new(0)
45
+ @db.hmset "timers:#{ts}", *(timers.map { |stat, data|
46
+ timer_hash.clear
47
+ data.each { |d| timer_hash[d.round] += 1 }
48
+ [stat, Marshal.dump(timer_hash)]
49
+ })
50
+ @db.expire "timers:#{ts}", @seconds_to_retain + 5 # Retain a few extra seconds to avoid reporting race
51
+ @db.sadd "timers", timers.keys
52
+ end
53
+ @db.lpush "timer_samples", "timers:#{ts}"
54
+ @db.ltrim "timer_samples", 0, @samples_to_retain - 1
55
+ end
56
+ end
57
+ MultiStatsd.logger.debug "Hottie flushing took #{"%.3f" % (time * 1000)}ms"
58
+ time
59
+ rescue Redis::CannotConnectError
60
+ MultiStatsd.logger.warning "Unable to connect to redis, skipping flush"
61
+ end
62
+ end
@@ -0,0 +1,8 @@
1
+ # Example backend which prints out the all stats to stdout
2
+ class MultiStatsd::Backend::Stdout < MultiStatsd::Backend::Base
3
+ # Prints the name of this backend, the current time, and inspected stats to stdout
4
+ # @return [nil]
5
+ def flush
6
+ $stdout.puts "[#{@name}:#{Time.now.to_i}] #{reset_stats.inspect}"
7
+ end
8
+ end
@@ -0,0 +1,22 @@
1
+ module MultiStatsd
2
+ # Eventmachine connection which receives UDP data and writes it to various backends
3
+ class Server < EventMachine::Connection
4
+ # Initialize the server with one or more backends
5
+ # @param [MultiStatsd::Backend] backends One or more backends that will receive data
6
+ def initialize(backends = [], *args)
7
+ @backends = [backends].flatten
8
+ super
9
+ end
10
+
11
+ # Write out statsd data to each registered backend
12
+ # @param [String] data Data in statsd format
13
+ # @return [nil]
14
+ def receive_data(data)
15
+ @backends.each do |backend|
16
+ backend.write(data)
17
+ end
18
+
19
+ nil
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ module MultiStatsd
2
+ # Gem version
3
+ VERSION = "0.0.1"
4
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'multi-statsd/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "multi-statsd"
8
+ gem.version = MultiStatsd::VERSION
9
+ gem.authors = ["Kelley Reynolds"]
10
+ gem.email = ["kelley@bigcartel.com"]
11
+ gem.description = %q{Statsd Server with flexible aggregation and back-end support}
12
+ gem.summary = %q{Statsd Server with flexible aggregation and back-end support}
13
+ gem.homepage = "https://github.com/bigcartel/multi-statsd"
14
+ gem.rubyforge_project = "multi-statsd"
15
+
16
+ gem.add_dependency "eventmachine"
17
+ gem.add_dependency "em-logger"
18
+
19
+ gem.required_ruby_version = '>= 1.9.3'
20
+ gem.add_development_dependency "bundler", ">= 1.0.0"
21
+ gem.add_development_dependency "simplecov"
22
+ gem.add_development_dependency "rspec", ">= 2.6.0"
23
+ gem.add_development_dependency "yard", ">= 0.8"
24
+ gem.add_development_dependency "rake"
25
+ gem.add_development_dependency "redis"
26
+
27
+ gem.files = `git ls-files`.split($/)
28
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
29
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
30
+ gem.require_paths = ["lib"]
31
+ end
@@ -0,0 +1,251 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
3
+
4
+ module EventMachine
5
+ def self.add_periodic_timer(interval)
6
+ # Don't flush anything automatically
7
+ end
8
+ end
9
+
10
+ describe MultiStatsd::Backend::Base do
11
+ let(:backend) { MultiStatsd::Backend::Base.new('base') }
12
+
13
+ describe "default initialization" do
14
+ it "should have a name" do
15
+ backend.name.should == 'base'
16
+ end
17
+
18
+ it "should have a default flush_interval" do
19
+ backend.instance_variable_get(:@options)['flush_interval'].should == 15
20
+ end
21
+
22
+ it "should have a hash of counters, timers, and gauges" do
23
+ backend.counters.should be_kind_of(Hash)
24
+ backend.timers.should be_kind_of(Hash)
25
+ backend.gauges.should be_kind_of(Hash)
26
+ end
27
+ end
28
+
29
+ describe "custom initialization" do
30
+ let(:backend) { MultiStatsd::Backend::Base.new('base', 'flush_interval' => 20) }
31
+
32
+ it "should have a default flush_interval" do
33
+ backend.instance_variable_get(:@options)['flush_interval'].should == 20
34
+ end
35
+ end
36
+
37
+ describe "flush" do
38
+ it "should raise an error" do
39
+ lambda { backend.flush }.should raise_error NotImplementedError
40
+ end
41
+ end
42
+
43
+ describe "reset_stats" do
44
+ let(:timers) { {'api' => [22, 15] } }
45
+
46
+ it "should return an array of hashes" do
47
+ stats = backend.reset_stats
48
+ stats.should be_kind_of Array
49
+ stats.each { |stat|
50
+ stat.should be_kind_of Hash
51
+ }
52
+ end
53
+
54
+ describe "counters" do
55
+ let(:counters) { {'bytes' => 15 } }
56
+
57
+ before(:each) {
58
+ backend.instance_variable_set(:@counters, counters.dup)
59
+ @stats = backend.reset_stats[0]
60
+ }
61
+
62
+ it "should return the data" do
63
+ @stats.should == counters
64
+ end
65
+
66
+ it "should be a duplicated object" do
67
+ @stats.object_id.should_not == backend.counters.object_id
68
+ end
69
+
70
+ it "should clear the instance variable" do
71
+ backend.counters.should be_empty
72
+ end
73
+ end
74
+
75
+ describe "timers" do
76
+ let(:timers) { {'api' => [9, 5] } }
77
+
78
+ before(:each) {
79
+ backend.instance_variable_set(:@timers, timers.dup)
80
+ @stats = backend.reset_stats[1]
81
+ }
82
+
83
+ it "should return the data" do
84
+ @stats.should == timers
85
+ end
86
+
87
+ it "should be a duplicated object" do
88
+ @stats.object_id.should_not == backend.timers.object_id
89
+ end
90
+
91
+ it "should clear the instance variable" do
92
+ backend.timers.should be_empty
93
+ end
94
+ end
95
+
96
+ describe "gauges" do
97
+ let(:gauges) { {'cpu' => 0.15 } }
98
+
99
+ before(:each) {
100
+ backend.instance_variable_set(:@gauges, gauges.dup)
101
+ @stats = backend.reset_stats[2]
102
+ }
103
+
104
+ it "should return the data" do
105
+ @stats.should == gauges
106
+ end
107
+
108
+ it "should be a duplicated object" do
109
+ @stats.object_id.should_not == backend.gauges.object_id
110
+ end
111
+
112
+ it "should not clear the instance variable" do
113
+ backend.gauges.should_not be_empty
114
+ end
115
+ end
116
+ end
117
+
118
+ describe "write" do
119
+ describe "crap" do
120
+ it "should ignore garbage" do
121
+ backend.write "blah:123|foo"
122
+ backend.write "blah:snord|ms"
123
+ backend.counters.should be_empty
124
+ backend.gauges.should be_empty
125
+ backend.timers.should be_empty
126
+ end
127
+ end
128
+
129
+ describe "timers" do
130
+ describe "single" do
131
+ let(:record) { "api:3|ms" }
132
+ before(:each) { backend.write record }
133
+
134
+ it "should record from scratch" do
135
+ backend.timers.should == {'api' => [3]}
136
+ end
137
+
138
+ it "should append a time" do
139
+ backend.write "api:4|ms"
140
+ backend.timers.should == {'api' => [3, 4]}
141
+ end
142
+ end
143
+
144
+ describe "multiple" do
145
+ let(:record) { "api:3|ms:4|ms" }
146
+ before(:each) { backend.write record }
147
+
148
+ it "should record both values" do
149
+ backend.timers.should == {'api' => [3,4]}
150
+ end
151
+ end
152
+ end
153
+
154
+ describe "gauges" do
155
+ describe "single" do
156
+ let(:record) { "cpu:0.15|g" }
157
+ before(:each) { backend.write record }
158
+
159
+ it "should record from scratch" do
160
+ backend.gauges.should == {'cpu' => 0.15}
161
+ end
162
+
163
+ it "should update existing gauge" do
164
+ backend.write "cpu:0.17|g"
165
+ backend.gauges.should == {'cpu' => 0.17}
166
+ end
167
+ end
168
+
169
+ describe "multiple" do
170
+ let(:record) { "cpu:0.15|g:0.17|g" }
171
+ before(:each) { backend.write record }
172
+
173
+ it "should use the last value" do
174
+ backend.gauges.should == {'cpu' => 0.17}
175
+ end
176
+ end
177
+ end
178
+
179
+ describe "counters" do
180
+ describe "single without sample rate" do
181
+ let(:record) { "bytes:15.2|c" }
182
+ before(:each) { backend.write record }
183
+
184
+ it "should record from scratch" do
185
+ backend.counters.should == {'bytes' => 15.2}
186
+ end
187
+
188
+ it "should add existing record" do
189
+ backend.write record
190
+ backend.counters.should == {'bytes' => 30.4}
191
+ end
192
+ end
193
+
194
+ describe "single with sample rate" do
195
+ let(:record) { "bytes:15.2|c|@0.5" }
196
+ before(:each) { backend.write record }
197
+
198
+ it "should record from scratch" do
199
+ backend.counters.should == {'bytes' => 30.4}
200
+ end
201
+
202
+ it "should add existing record" do
203
+ backend.write record
204
+ backend.counters.should == {'bytes' => 60.8}
205
+ end
206
+ end
207
+
208
+ describe "multiple without sample rate" do
209
+ let(:record) { "bytes:15.5|c:20.2|c" }
210
+ before(:each) { backend.write record }
211
+
212
+ it "should record from scratch" do
213
+ backend.counters.should == {'bytes' => 35.7}
214
+ end
215
+
216
+ it "should add existing record" do
217
+ backend.write record
218
+ backend.counters.should == {'bytes' => 71.4}
219
+ end
220
+ end
221
+
222
+ describe "multiple with sample rate" do
223
+ let(:record) { "bytes:7.75|c|@0.5:10.1|c|@0.5" }
224
+ before(:each) { backend.write record }
225
+
226
+ it "should record from scratch" do
227
+ backend.counters.should == {'bytes' => 35.7}
228
+ end
229
+
230
+ it "should add existing record" do
231
+ backend.write record
232
+ backend.counters.should == {'bytes' => 71.4}
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ describe "format_key" do
239
+ it "should turn spaces into underscores" do
240
+ backend.format_key("blah foo").should == "blah_foo"
241
+ end
242
+
243
+ it "should turn slashes into hyphens" do
244
+ backend.format_key("blah/foo").should == "blah-foo"
245
+ end
246
+
247
+ it "should filter out non-alpha and a few selected special chars" do
248
+ backend.format_key("blahüfoo*!@&#^%}").should == "blahfoo"
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,126 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
3
+ require 'multi-statsd/backends/hottie'
4
+
5
+ module EventMachine
6
+ def self.add_periodic_timer(interval)
7
+ # Don't flush anything automatically
8
+ end
9
+ end
10
+
11
+ describe MultiStatsd::Backend::Hottie do
12
+ let(:backend) { MultiStatsd::Backend::Hottie.new('hottie') }
13
+
14
+ describe "post init" do
15
+ it "should have a connection to redis" do
16
+ backend.instance_variable_get(:@db).should be_kind_of Redis
17
+ end
18
+
19
+ it "should have a default @seconds_to_retain" do
20
+ backend.seconds_to_retain.should == 60
21
+ end
22
+
23
+ it "should have a default @seconds_to_retain" do
24
+ backend = MultiStatsd::Backend::Hottie.new('hottie', 'seconds_to_retain' => 80)
25
+ backend.seconds_to_retain.should == 80
26
+ end
27
+
28
+ it "should have an appropriate set of samples to retain" do
29
+ backend.samples_to_retain.should == 4
30
+ end
31
+
32
+ it "should have a default @seconds_to_retain" do
33
+ backend = MultiStatsd::Backend::Hottie.new('hottie', 'seconds_to_retain' => 80, 'flush_interval' => 10)
34
+ backend.samples_to_retain.should == 8
35
+ end
36
+ end
37
+
38
+ describe "flush" do
39
+ describe "gauges" do
40
+ before(:all) {
41
+ @db = backend.instance_variable_get(:@db)
42
+ @db.flushdb
43
+ backend.write "cpu:0.15|g"
44
+ backend.flush
45
+ }
46
+
47
+ it "should add the name to the set" do
48
+ @db.sismember("gauges", "cpu").should be_true
49
+ end
50
+
51
+ it "should have a sample" do
52
+ samples = @db.lrange "gauge_samples", 0, -1
53
+ samples.size.should == 1
54
+ @db.hlen(samples.first).should == 1
55
+ @db.hget(samples.first, 'cpu').should == "0.15"
56
+ end
57
+
58
+ it "should roll over on flush if there are too many samples" do
59
+ backend.samples_to_retain.times do |i|
60
+ @db.lpush "gauge_samples", "gauge:#{i}"
61
+ end
62
+ @db.llen("gauge_samples").should == (backend.samples_to_retain + 1)
63
+ backend.flush
64
+ @db.llen("gauge_samples").should == backend.samples_to_retain
65
+ end
66
+ end
67
+
68
+ describe "counters" do
69
+ before(:all) {
70
+ @db = backend.instance_variable_get(:@db)
71
+ @db.flushdb
72
+ backend.write "bytes:1200|c"
73
+ backend.flush
74
+ }
75
+
76
+ it "should add the name to the set" do
77
+ @db.sismember("counters", "bytes").should be_true
78
+ end
79
+
80
+ it "should have a sample" do
81
+ samples = @db.lrange "counter_samples", 0, -1
82
+ samples.size.should == 1
83
+ @db.hlen(samples.first).should == 1
84
+ (@db.hget(samples.first, 'bytes')).to_i.should == (1200 / backend.flush_interval)
85
+ end
86
+
87
+ it "should roll over on flush if there are too many samples" do
88
+ backend.samples_to_retain.times do |i|
89
+ @db.lpush "counter_samples", "counter:#{i}"
90
+ end
91
+ @db.llen("counter_samples").should == (backend.samples_to_retain + 1)
92
+ backend.flush
93
+ @db.llen("counter_samples").should == backend.samples_to_retain
94
+ end
95
+ end
96
+
97
+ describe "timers" do
98
+ before(:all) {
99
+ @db = backend.instance_variable_get(:@db)
100
+ @db.flushdb
101
+ backend.write "api:15.5|ms"
102
+ backend.flush
103
+ }
104
+
105
+ it "should add the name to the set" do
106
+ @db.sismember("timers", "api").should be_true
107
+ end
108
+
109
+ it "should have a rounded sample" do
110
+ samples = @db.lrange "timer_samples", 0, -1
111
+ samples.size.should == 1
112
+ @db.hlen(samples.first).should == 1
113
+ Marshal.load(@db.hget(samples.first, 'api')).should == {16 => 1}
114
+ end
115
+
116
+ it "should roll over on flush if there are too many samples" do
117
+ backend.samples_to_retain.times do |i|
118
+ @db.lpush "timer_samples", "timer:#{i}"
119
+ end
120
+ @db.llen("timer_samples").should == (backend.samples_to_retain + 1)
121
+ backend.flush
122
+ @db.llen("timer_samples").should == backend.samples_to_retain
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
3
+ require 'multi-statsd/backends/stdout'
4
+
5
+ module EventMachine
6
+ def self.add_periodic_timer(interval)
7
+ # Don't flush anything automatically
8
+ end
9
+ end
10
+
11
+ describe MultiStatsd::Backend::Stdout do
12
+ let(:backend) { MultiStatsd::Backend::Stdout.new('stdout') }
13
+
14
+ describe "flush behavior" do
15
+ it "should print to stdout" do
16
+ $stdout.should_receive(:puts)
17
+ backend.flush
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path('spec_helper.rb', File.dirname(__FILE__))
3
+ require 'multi-statsd/backends/stdout'
4
+
5
+ describe MultiStatsd do
6
+ it "should start and stop the reactor" do
7
+ MultiStatsd.start({'host' => 'localhost', 'port' => 33333, 'backends' => {'stdout' => {'backend' => 'Stdout'}}})
8
+ sleep 0.2
9
+ EM.reactor_running?.should be_true
10
+ MultiStatsd.stop
11
+ sleep 0.2
12
+ EM.reactor_running?.should be_false
13
+ end
14
+
15
+ it "should raise an error on an unknown backend" do
16
+ lambda {
17
+ MultiStatsd.start({'host' => 'localhost', 'port' => 33333, 'backends' => {'stdout' => {'backend' => 'Broken'}}})
18
+ sleep 1
19
+
20
+ }.should raise_error(MultiStatsd::Backend::Error)
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path('spec_helper.rb', File.dirname(__FILE__))
3
+ require 'multi-statsd/backends/stdout'
4
+
5
+ class MockBackend
6
+ def write(data); end
7
+ end
8
+
9
+ describe MultiStatsd::Server do
10
+ def send_message(message, server='localhost', port=33333)
11
+ UDPSocket.new.send(message, 0, server, port)
12
+ end
13
+
14
+ let(:message) { 'test:1|c' }
15
+
16
+ describe "recording data to backends" do
17
+ it "Should record data to a single backend" do
18
+ mock_backend = MockBackend.new
19
+ mock_backend.should_receive(:write).with(message)
20
+ EM.run do
21
+ EM::open_datagram_socket 'localhost', 33333, MultiStatsd::Server, mock_backend
22
+ send_message message
23
+ EM.next_tick { EM.stop }
24
+ end
25
+ end
26
+
27
+ it "should record data to multiple backends" do
28
+ backends = 3.times.map do
29
+ backend = MockBackend.new
30
+ backend.should_receive(:write).with(message)
31
+ backend
32
+ end
33
+
34
+ EM.run do
35
+ EM::open_datagram_socket 'localhost', 33333, MultiStatsd::Server, backends
36
+ send_message message
37
+ EM.next_tick { EM.stop }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter "/spec/"
4
+ end
5
+ require 'rspec'
6
+ require 'multi-statsd'
metadata ADDED
@@ -0,0 +1,205 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multi-statsd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kelley Reynolds
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: em-logger
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 1.0.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.0.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: simplecov
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: 2.6.0
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: 2.6.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: yard
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0.8'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rake
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: redis
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ description: Statsd Server with flexible aggregation and back-end support
143
+ email:
144
+ - kelley@bigcartel.com
145
+ executables:
146
+ - multi-statsd
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - .gitignore
151
+ - .travis.yml
152
+ - Gemfile
153
+ - LICENSE.txt
154
+ - README.md
155
+ - Rakefile
156
+ - bin/multi-statsd
157
+ - etc/multi-statsd.yml
158
+ - lib/multi-statsd.rb
159
+ - lib/multi-statsd/backends/base.rb
160
+ - lib/multi-statsd/backends/hottie.rb
161
+ - lib/multi-statsd/backends/stdout.rb
162
+ - lib/multi-statsd/server.rb
163
+ - lib/multi-statsd/version.rb
164
+ - multi-statsd.gemspec
165
+ - spec/backends/base_spec.rb
166
+ - spec/backends/hottie_spec.rb
167
+ - spec/backends/stdout_spec.rb
168
+ - spec/multi-statsd_spec.rb
169
+ - spec/server_spec.rb
170
+ - spec/spec_helper.rb
171
+ homepage: https://github.com/bigcartel/multi-statsd
172
+ licenses: []
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ none: false
179
+ requirements:
180
+ - - ! '>='
181
+ - !ruby/object:Gem::Version
182
+ version: 1.9.3
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ none: false
185
+ requirements:
186
+ - - ! '>='
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ segments:
190
+ - 0
191
+ hash: 1318037612798865336
192
+ requirements: []
193
+ rubyforge_project: multi-statsd
194
+ rubygems_version: 1.8.23
195
+ signing_key:
196
+ specification_version: 3
197
+ summary: Statsd Server with flexible aggregation and back-end support
198
+ test_files:
199
+ - spec/backends/base_spec.rb
200
+ - spec/backends/hottie_spec.rb
201
+ - spec/backends/stdout_spec.rb
202
+ - spec/multi-statsd_spec.rb
203
+ - spec/server_spec.rb
204
+ - spec/spec_helper.rb
205
+ has_rdoc: