multi-statsd 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,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: