metricize 0.4.4

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e9a9c558536848b9cabca975cdff4c4c985d41a4
4
+ data.tar.gz: 692614a119d59aca4dc08bfcfeb88a4aff12387e
5
+ SHA512:
6
+ metadata.gz: d9cf9019f8ad81f81203a665068536f8424c927f07c54fc3f83be073a0dc41eb491b1999e7ea2567164891ac493ddce5881c3cb9474f4b2d2f501d6dc35b514b
7
+ data.tar.gz: 5cf0995e784a3874906c05c1dc6b306148620654bd91334490c47e4ee305a328f3430345693aa3da942871fc2374c6f43eae0a2d65b4e7b8ac168c85d1df7624
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
+ .rbenv-version
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in metricize.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Matt McNeil
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,52 @@
1
+ # Metricize
2
+
3
+ Simple in-memory server to receive metrics, aggregate them, and send them to a stats service
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'metricize'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install metricize
18
+
19
+ ## Usage
20
+
21
+ # start server in its own Ruby process (eg using lib/tasks/metrics.rake)
22
+ Metricize::Server.new(username: 'name@example.com', password: 'api_key').start
23
+
24
+ # start appropriate client (eg from config/initializers/metrics.rb)
25
+ if Rails.env == 'production'
26
+ client_config = { prefix: "app_name.#{Rails.env}",
27
+ queue_host: 'localhost',
28
+ queue_name: "app_name.#{Rails.env}.metrics_queue",
29
+ logger: Rails.logger }
30
+
31
+ METRICS = Metricize::Client.new(client_config)
32
+
33
+ else
34
+ METRICS = Metricize::NullClient
35
+ end
36
+
37
+ # use client interface to send metrics from the app
38
+ METRICS.increment('content_post.make') # increment by default value of 1
39
+ METRICS.increment('bucket.make', by: 5) # increment counter by 5
40
+ METRICS.measure('worker_processes', 45) # send a snapshot of a current value (eg 45)
41
+ METRICS.time('facebook.request_content') do # record the execution time of a slow block
42
+ # make API call...
43
+ end
44
+ METRICS.measure('stat', 45, source: 'my_source') # break out stat by subgrouping
45
+
46
+ ## Contributing
47
+
48
+ 1. Fork it
49
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
50
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
51
+ 4. Push to the branch (`git push origin my-new-feature`)
52
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
7
+ task :test => :spec
data/lib/metricize.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "metricize/version"
2
+
3
+ require 'thread'
4
+ require 'rest-client'
5
+ require 'json'
6
+ require 'logger'
7
+ require 'redis'
8
+ require 'ascii_charts'
9
+
10
+ require "metricize/shared"
11
+ require "metricize/forwarder"
12
+ require "metricize/client"
13
+ require "metricize/stats"
14
+ require "metricize/null_client"
@@ -0,0 +1,62 @@
1
+ module Metricize
2
+ class Client
3
+ include SharedMethods
4
+
5
+ def initialize(options = {})
6
+ @prefix = options[:prefix]
7
+ establish_logger(options)
8
+ initialize_redis(options)
9
+ establish_redis_connection
10
+ end
11
+
12
+ def increment(name, options = {})
13
+ count = options.delete(:by) || 1
14
+ enqueue_count(name, count, options)
15
+ end
16
+
17
+ def measure(name, value, options = {})
18
+ enqueue_value(name, value, options)
19
+ end
20
+
21
+ def time(name, options = {})
22
+ raise ArgumentError, "must be invoked with a block to time" unless block_given?
23
+ start_time = Time.now
24
+ block_result = yield
25
+ measure(name + '.time', time_delta_ms(start_time))
26
+ return block_result
27
+ end
28
+
29
+ private
30
+
31
+ def enqueue_count(name, count, options)
32
+ push_to_queue(build_metric_name(name) + '.count', count, options)
33
+ end
34
+
35
+ def enqueue_value(name, value, options)
36
+ raise ArgumentError, "no numeric value provided in measure call" unless value.kind_of?(Numeric)
37
+ push_to_queue(build_metric_name(name), round(value, 4), options)
38
+ end
39
+
40
+ def push_to_queue(name, value, options)
41
+ data = prepare_metric(name, value, options).to_json
42
+ @redis.lpush(@queue_name, data)
43
+ msg = "#{name.gsub('.', '_')}=#{value}" # splunk chokes on dots in field names
44
+ msg << ", metric_source=#{options[:source].gsub('.', '_')}" if options[:source]
45
+ log_message msg, :info
46
+ end
47
+
48
+ def build_metric_name(name)
49
+ [ @prefix, sanitize(name) ].compact.join('.')
50
+ end
51
+
52
+ def sanitize(name)
53
+ name.to_s.strip.downcase.gsub(' ', '_').gsub(/[^a-z0-9._]/, '')
54
+ end
55
+
56
+ def prepare_metric(name, value, options)
57
+ options[:source] = sanitize(options[:source]) if options[:source]
58
+ options.merge(:name => name, :value => value)
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,140 @@
1
+ module Metricize
2
+ class Forwarder
3
+ include Metricize::SharedMethods
4
+
5
+ def initialize(options)
6
+ @password = options.fetch(:password)
7
+ @username = options.fetch(:username)
8
+ @remote_url = options[:remote_url] || 'metrics-api.librato.com/v1/metrics'
9
+ @remote_timeout = options[:remote_timeout] || 10
10
+ establish_logger(options)
11
+ initialize_redis(options)
12
+ end
13
+
14
+ def go!
15
+ establish_redis_connection
16
+ process_metric_queue
17
+ end
18
+
19
+ private
20
+
21
+ def process_metric_queue
22
+ queue = retrieve_queue_contents
23
+ return if queue.empty?
24
+ store_metrics(add_aggregate_info(queue))
25
+ clear_queue
26
+ rescue RuntimeError => e
27
+ log_message "Error: " + e.message, :error
28
+ end
29
+
30
+ def retrieve_queue_contents
31
+ log_message "checking... queue_length=#{queue_length = @redis.llen(@queue_name)}", :info
32
+ return [] unless queue_length > 0
33
+ queue = @redis.lrange(@queue_name, 0, -1)
34
+ queue.map {|metric| JSON.parse(metric, :symbolize_names => true) }
35
+ end
36
+
37
+ def clear_queue
38
+ log_message "clearing queue"
39
+ @redis.del @queue_name if @redis
40
+ end
41
+
42
+ def store_metrics(data)
43
+ log_message "remote_data_sent='#{data}'"
44
+ start_time = Time.now
45
+ RestClient.post("https://#{@username.sub('@','%40')}:#{@password}@#{@remote_url}",
46
+ data.to_json,
47
+ :timeout => @remote_timeout,
48
+ :content_type => 'application/json')
49
+ log_message "remote_data_sent_chars=#{data.to_s.length}, remote_request_duration_ms=#{time_delta_ms(start_time)}", :info
50
+ end
51
+
52
+ def add_aggregate_info(metrics)
53
+ counters, measurements = metrics.partition {|metric| metric.fetch(:name) =~ /.count$/ }
54
+ counters = consolidate_counts(counters)
55
+ measurements = add_value_stats(measurements)
56
+ measurements << add_stat_by_key(@queue_name + '.counters', counters.size)
57
+ { :gauges => counters + measurements, :measure_time => Time.now.to_i }
58
+ end
59
+
60
+ def consolidate_counts(counters)
61
+ aggregated_counts = {}
62
+ counters.each_with_index do |metric,i|
63
+ # collect aggregate stats for each name+source combination
64
+ key = [metric.fetch(:name), metric[:source]].join('|')
65
+ aggregated_counts[key] = aggregated_counts[key].to_i + metric[:value]
66
+ end
67
+ aggregated_counts.map do | key, count |
68
+ add_stat_by_key(key, count).merge(counter_attributes)
69
+ end
70
+ end
71
+
72
+ def counter_attributes
73
+ { :attributes => {:source_aggregate => true, :summarize_function => 'sum'} }
74
+ end
75
+
76
+ def add_value_stats(gauges)
77
+ value_groups = {}
78
+ gauges.each do | metric |
79
+ key = [metric.fetch(:name), metric[:source]].join('|')
80
+ value_groups[key] ||= []
81
+ value_groups[key] << metric[:value]
82
+ end
83
+ value_groups.each do |key, values|
84
+ print_histogram(key, values)
85
+ gauges << add_stat_by_key(key, values.size, '.count').merge(counter_attributes)
86
+ gauges << add_stat_by_key(key, values.max, ".max")
87
+ gauges << add_stat_by_key(key, values.min, ".min")
88
+ [0.25, 0.50, 0.75, 0.95].each do |p|
89
+ percentile = values.extend(Stats).calculate_percentile(p)
90
+ gauges << add_stat_by_key(key, percentile, ".#{(p*100).to_i}e")
91
+ end
92
+ end
93
+ gauges << add_stat_by_key(@queue_name + '.measurements', value_groups.size)
94
+ gauges
95
+ end
96
+
97
+ def print_histogram(name, values)
98
+ return if values.size < 5
99
+
100
+ num_bins = [25, values.size].min.to_f
101
+ bin_width = (values.max - values.min)/num_bins
102
+ bin_width = 1 if bin_width == 0
103
+
104
+ bins = (values.min...values.max).step(bin_width).to_a
105
+ freqs = bins.map {| bin | values.select{|x| x >= bin && x <= (bin+bin_width) }.count }
106
+
107
+ name = name.gsub('|','.').sub(/\.$/, '')
108
+
109
+ values.extend(Stats)
110
+ chart_data = bins.map!(&:floor).zip(freqs)
111
+ chart_options = { :bar => true,
112
+ :title => "\nHistogram for #{name} at #{Time.now}",
113
+ :hide_zero => true }
114
+ chart_output = AsciiCharts::Cartesian.new(chart_data, chart_options).draw +
115
+ "\n#{name}.count=#{values.count}\n" +
116
+ "#{name}.min=#{round(values.min, 2)}\n" +
117
+ "#{name}.max=#{round(values.max, 2)}\n" +
118
+ "#{name}.mean=#{round(values.mean, 2)}\n" +
119
+ "#{name}.stddev=#{round(values.standard_deviation, 2)}\n"
120
+ log_message(chart_output, :info)
121
+ rescue => e
122
+ log_message("#{e}: Could not print histogram for #{name} with these input values: #{values.inspect}", :error)
123
+ end
124
+
125
+ def add_stat_by_key(key, value, suffix = "")
126
+ metric = { :name => key.split('|')[0] + suffix,
127
+ :value => value }
128
+ metric.merge!(:source => key.split('|')[1]) if key.split('|')[1]
129
+ metric
130
+ end
131
+
132
+ def calculate_percentile(values, percentile)
133
+ return values.first if values.size == 1
134
+ values_sorted = values.sort
135
+ k = (percentile*(values_sorted.length-1)+1).floor - 1
136
+ values_sorted[k]
137
+ end
138
+
139
+ end
140
+ end
@@ -0,0 +1,14 @@
1
+ module Metricize
2
+
3
+ class NullClient
4
+ def self.increment(*args); end
5
+ def self.measure(*args); end
6
+ def self.time(*args); yield; end
7
+ def self.establish_redis_connection; end
8
+ end
9
+
10
+ class NullForwarder
11
+ def self.go!; end
12
+ end
13
+
14
+ end
@@ -0,0 +1,38 @@
1
+ module Metricize
2
+ module SharedMethods
3
+
4
+ def establish_redis_connection
5
+ log_message "metricize_version=#{VERSION} connecting to Redis at #{@queue_host}:#{@queue_port}", :info
6
+ @redis = Redis.connect(:host => @queue_host, :port => @queue_port)
7
+ @redis.ping
8
+ end
9
+
10
+ private
11
+
12
+ def initialize_redis(options)
13
+ @queue_host = options[:queue_host] || '127.0.0.1'
14
+ @queue_port = options[:queue_port] || 6379
15
+ @queue_name = options[:queue_name] || 'metricize_queue'
16
+ end
17
+
18
+ def establish_logger(options)
19
+ @logger = options[:logger] || Logger.new(STDOUT)
20
+ end
21
+
22
+ def log_message(message, level = :debug)
23
+ message = "[#{self.class} #{Process.pid}] " + message
24
+ @logger.send(level.to_sym, message)
25
+ end
26
+
27
+ def time_delta_ms(start_time)
28
+ (((Time.now - start_time) * 100000.0).round) / 100.0
29
+ end
30
+
31
+ def round(value, num_places)
32
+ factor = 10.0**num_places
33
+ ((value * factor).round) / factor
34
+ end
35
+
36
+ end
37
+ end
38
+
@@ -0,0 +1,30 @@
1
+ module Metricize
2
+ module Stats
3
+
4
+ def sum
5
+ return self.inject(0){|accum, i| accum + i }
6
+ end
7
+
8
+ def mean
9
+ return self.sum / self.length.to_f
10
+ end
11
+
12
+ def sample_variance
13
+ m = self.mean
14
+ sum = self.inject(0){|accum, i| accum + (i - m) ** 2 }
15
+ return sum / (self.length - 1).to_f
16
+ end
17
+
18
+ def standard_deviation
19
+ return Math.sqrt(self.sample_variance)
20
+ end
21
+
22
+ def calculate_percentile(percentile)
23
+ return self.first if self.size == 1
24
+ values_sorted = self.sort
25
+ k = (percentile*(values_sorted.length-1)+1).floor - 1
26
+ values_sorted[k]
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module Metricize
2
+ VERSION = "0.4.4"
3
+ end
data/metricize.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'metricize/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "metricize"
8
+ spec.version = Metricize::VERSION
9
+ spec.authors = ["Matt McNeil"]
10
+ spec.email = ["mmcneil@liveworld.com"]
11
+ spec.description = %q{Simple client/forwarder system to aggregate metrics and periodically send them to a stats service}
12
+ spec.summary = %q{Collect, aggregate, and send metrics to a stats service}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "timecop"
25
+ spec.add_development_dependency "fakeredis"
26
+ spec.add_development_dependency "simplecov"
27
+
28
+ spec.add_runtime_dependency "rest-client"
29
+ spec.add_runtime_dependency "json"
30
+ spec.add_runtime_dependency "redis"
31
+ spec.add_runtime_dependency "ascii_charts"
32
+
33
+ end
@@ -0,0 +1,245 @@
1
+ require "spec_helper"
2
+
3
+ describe Metricize do
4
+ let(:logger) { double.as_null_object }
5
+ let(:forwarder) { Metricize::Forwarder.new( :password => 'api_key',
6
+ :username => 'name@example.com',
7
+ :logger => logger) }
8
+
9
+ let(:client) { Metricize::Client.new( :prefix => 'prefix', :logger => logger ) }
10
+
11
+ before do
12
+ Timecop.freeze(Time.at(1234))
13
+ RestClient.stub(:post)
14
+ end
15
+
16
+ after do
17
+ Timecop.return
18
+ forwarder.send(:clear_queue)
19
+ end
20
+
21
+ it "provides null object implementations to allow for easily disabling metrics functionality" do
22
+ expect(Metricize::NullClient).to respond_to(:new, :increment, :measure, :time)
23
+ expect(Metricize::NullForwarder).to respond_to(:new, :go!)
24
+ end
25
+
26
+ it "properly uses the remote API to send gauge stats" do
27
+ client.increment('stat.name', :source => 'my_source')
28
+ RestClient.should_receive(:post).with do | api_url, post_data, request_params |
29
+ expect(api_url).to eq("https://name%40example.com:api_key@metrics-api.librato.com/v1/metrics")
30
+ first_gauge = JSON.parse(post_data)['gauges'].first
31
+ expect(first_gauge['name']).to eq "prefix.stat.name.count"
32
+ expect(first_gauge['source']).to eq "my_source"
33
+ expect(first_gauge['attributes']).to eq("source_aggregate" => true, "summarize_function" => "sum")
34
+ expect(request_params).to eq( :timeout => 10, :content_type => "application/json" )
35
+ end
36
+ forwarder.go!
37
+ end
38
+
39
+ it "sends stats when requested with go!" do
40
+ client.measure('value_stat', 777)
41
+ RestClient.should_receive(:post)
42
+ forwarder.go!
43
+ end
44
+
45
+ it "does not send stats if none have been recorded" do
46
+ RestClient.should_not_receive(:post)
47
+ forwarder.go!
48
+ end
49
+
50
+ it "clears queue and does not send again after a successful request" do
51
+ client.increment('stat.name')
52
+ forwarder.go!
53
+ RestClient.should_not_receive(:post)
54
+ forwarder.go!
55
+ end
56
+
57
+ it "removes special characters and spaces and converts the metric names and sources to dotted decimal snake_case" do
58
+ client.increment(' My UNRULY stat!@#$%^&*\(\) ')
59
+ RestClient.should_receive(:post).with(anything, /my_unruly_stat/, anything)
60
+ forwarder.go!
61
+ client.increment('test', :source => ' My UNRULY source!@#$%^&*\(\) ')
62
+ RestClient.should_receive(:post).with(anything, /my_unruly_source/, anything)
63
+ forwarder.go!
64
+ end
65
+
66
+ it "converts passed in objects to string before using them as metric or source names" do
67
+ client.increment(Numeric, :source => Integer)
68
+ RestClient.should_receive(:post).with do | _, post_data |
69
+ expect(post_data).to match( /prefix.numeric.count/ )
70
+ expect(post_data).to match( /source":"integer"/ )
71
+ end
72
+ forwarder.go!
73
+ end
74
+
75
+ it "sends all stats in a batch with the same timestamp" do
76
+ client.measure('value1', 5)
77
+ RestClient.should_receive(:post).with(anything, /measure_time":1234\D/, anything)
78
+ forwarder.go!
79
+ end
80
+
81
+ it "adds subgrouping information if present" do
82
+ client.increment('counter1', :source => 'my_source')
83
+ RestClient.should_receive(:post).with(anything, /"source":"my_source"/, anything)
84
+ forwarder.go!
85
+ end
86
+
87
+ it "consolidates repeated counts into an aggregate total before sending" do
88
+ client.increment('counter1')
89
+ client.increment('counter1', :by => 5)
90
+ RestClient.should_receive(:post).with do | _, post_data |
91
+ expect(post_data).to match( /"value":6/ )
92
+ expect(post_data).to match( /"name":"prefix.counter1.count"/ )
93
+ expect(post_data).to match( /source_aggregate":true/ )
94
+ expect(post_data).to match( /summarize_function":"sum"/ )
95
+ end
96
+ forwarder.go!
97
+ end
98
+
99
+ it "aggregates requests from multiple clients" do
100
+ client.increment('counter1')
101
+ client2 = Metricize::Client.new( :prefix => 'prefix', :logger => logger )
102
+ client2.increment('counter1', :by => 5)
103
+ RestClient.should_receive(:post).with do | _, post_data |
104
+ expect(post_data).to match( /"value":6/ )
105
+ expect(post_data).to match( /"name":"prefix.counter1.count"/ )
106
+ end
107
+ forwarder.go!
108
+ end
109
+
110
+ it "sends value stats when asked to measure something" do
111
+ client.measure('value1', 10)
112
+ client.measure('value2', 20)
113
+ RestClient.should_receive(:post).with do | _, post_data |
114
+ gauges = JSON.parse(post_data)['gauges']
115
+ expect(gauges[1]['name']).to eq "prefix.value1"
116
+ expect(gauges[1]['value']).to eq 10.0
117
+ expect(gauges[0]['name']).to eq "prefix.value2"
118
+ expect(gauges[0]['value']).to eq 20.0
119
+ end
120
+ forwarder.go!
121
+ end
122
+
123
+ it "raises an error when measure is called without a numeric value" do
124
+ expect { client.measure('boom', {}) }.to raise_error(ArgumentError, /no numeric value provided in measure call/)
125
+ expect { client.measure('boom', 'NaN') }.to raise_error(ArgumentError, /no numeric value provided in measure call/)
126
+ end
127
+
128
+ it "rounds value stats to 4 decimals" do
129
+ client.measure('value1', 1.0/7.0)
130
+ RestClient.should_receive(:post).with(anything, /value":0.1429/, anything)
131
+ forwarder.go!
132
+ end
133
+
134
+ describe "adding aggregate stats based on all instances of each value stat in this time interval" do
135
+
136
+ it "splits out aggregate stats for each subgrouping for values with multiple sources" do
137
+ [4,5,6].each { |value| client.measure('value1', value, :source => 'source1') }
138
+ [1,2,3].each { |value| client.measure('value1', value, :source => 'source2') }
139
+ RestClient.should_receive(:post).with do | url, post_data |
140
+ gauges = JSON.parse(post_data)['gauges']
141
+ expect(gauges).to include("name"=>"prefix.value1.50e", "source"=> "source1", "value"=>5.0)
142
+ expect(gauges).to include("name"=>"prefix.value1.50e", "source"=> "source2", "value"=>2.0)
143
+ end
144
+ forwarder.go!
145
+ end
146
+
147
+ it "asks for server aggregation on the count of value stats" do
148
+ client.measure('value_stat1', 7)
149
+ RestClient.should_receive(:post).with do | url, post_data |
150
+ gauges = JSON.parse(post_data)['gauges']
151
+ expect(gauges).to include("name"=>"prefix.value_stat1.count", "value"=>1, "attributes"=>{"source_aggregate"=>true, "summarize_function"=>"sum"})
152
+ end
153
+ forwarder.go!
154
+ end
155
+
156
+ it "adds min, max, and count" do
157
+ [4,5,6].each { |value| client.measure('value1', value) }
158
+ RestClient.should_receive(:post).with do | url, post_data |
159
+ gauges = JSON.parse(post_data)['gauges']
160
+ expect(gauges).to include("name"=>"prefix.value1.count", "value"=>3, "attributes"=>{"source_aggregate"=>true, "summarize_function"=>"sum"})
161
+ expect(gauges).to include("name"=>"prefix.value1.max", "value"=>6)
162
+ expect(gauges).to include("name"=>"prefix.value1.min", "value"=>4)
163
+ end
164
+ forwarder.go!
165
+ end
166
+
167
+ it "adds metadata about the entire batch of stats" do
168
+ (1..4).each { |index| client.measure("value_stat#{index}", 0) }
169
+ (1..7).each { |index| client.increment("counter_stat#{index}") }
170
+ RestClient.should_receive(:post).with do | url, post_data |
171
+ gauges = JSON.parse(post_data)['gauges']
172
+ expect(gauges).to include("name"=>"metricize_queue.measurements", "value"=>4)
173
+ expect(gauges).to include("name"=>"metricize_queue.counters", "value"=>7)
174
+ end
175
+ forwarder.go!
176
+ end
177
+
178
+ it "calculates standard deviation and mean" do
179
+ values = [1,2,3,4,5,6].extend(Metricize::Stats)
180
+ expect(values.mean).to eq(3.5)
181
+ expect(values.standard_deviation).to be_within(0.01).of(1.87083)
182
+ end
183
+
184
+ it "adds percentile stats for each value stat" do
185
+ (1..20).each { |value| client.measure('value_stat1', value) }
186
+ client.measure('value_stat2', 7)
187
+ RestClient.should_receive(:post).with do | _, post_data |
188
+ gauges = JSON.parse(post_data)['gauges']
189
+ expect(gauges).to include("name"=>"prefix.value_stat1.25e", "value"=>5.0)
190
+ expect(gauges).to include("name"=>"prefix.value_stat1.50e", "value"=>10.0)
191
+ expect(gauges).to include("name"=>"prefix.value_stat1.75e", "value"=>15.0)
192
+ expect(gauges).to include("name"=>"prefix.value_stat1.95e", "value"=>19.0)
193
+ expect(gauges).to include("name"=>"prefix.value_stat2.95e", "value"=>7.0)
194
+ end
195
+ forwarder.go!
196
+ end
197
+
198
+ it "logs a histogram for value stats with more than 5 measurements" do
199
+ 2.times { logger.should_receive(:info) }
200
+ [10,10,15,15,15,19].each { |value| client.measure('value_stat1', value) }
201
+ #3| *
202
+ #2| * *
203
+ #1| * * *
204
+ #0+------------------
205
+ # 10 11 13 14 16 17
206
+ logger.should_receive(:info).with(/10 11 13 14 16 17/m)
207
+ forwarder.go!
208
+ end
209
+
210
+ it "doesn't log a histogram for value stats with less than 5 measurements" do
211
+ [10,10,15].each { |value| client.measure('value_stat1', value) }
212
+ 2.times { logger.should_receive(:info) }
213
+ logger.should_not_receive(:info).with(/Histogram/)
214
+ forwarder.go!
215
+ end
216
+
217
+ it "handles cases where all values are the same" do
218
+ [10,10,10,10,10,10].each { |value| client.measure('value_stat1', value) }
219
+ logger.should_not_receive(:error)
220
+ forwarder.go!
221
+ end
222
+
223
+ end
224
+
225
+ it "times and reports the execution of a block of code in milliseconds" do
226
+ client.time('my_slow_code') do
227
+ Timecop.travel(5) # simulate 5000 milliseconds of runtime
228
+ end
229
+ RestClient.should_receive(:post).with do | _, post_data |
230
+ first_gauge = JSON.parse(post_data)['gauges'].first
231
+ expect(first_gauge['name']).to eq('prefix.my_slow_code.time')
232
+ expect(first_gauge['value']).to be_within(0.2).of(5000)
233
+ end
234
+ forwarder.go!
235
+ end
236
+
237
+ it "retries sending if it encounters an error" do
238
+ client.increment('counter1')
239
+ RestClient.stub(:post).and_raise(RestClient::Exception)
240
+ forwarder.go!
241
+ RestClient.should_receive(:post)
242
+ forwarder.go!
243
+ end
244
+
245
+ end
@@ -0,0 +1,6 @@
1
+ require 'bundler/setup'
2
+ require 'simplecov'
3
+ SimpleCov.start
4
+ require 'metricize'
5
+ require 'timecop'
6
+ require 'fakeredis'
metadata ADDED
@@ -0,0 +1,202 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: metricize
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.4
5
+ platform: ruby
6
+ authors:
7
+ - Matt McNeil
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-07-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: timecop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: fakeredis
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rest-client
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: json
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: redis
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: ascii_charts
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Simple client/forwarder system to aggregate metrics and periodically
154
+ send them to a stats service
155
+ email:
156
+ - mmcneil@liveworld.com
157
+ executables: []
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - .gitignore
162
+ - Gemfile
163
+ - LICENSE.txt
164
+ - README.md
165
+ - Rakefile
166
+ - lib/metricize.rb
167
+ - lib/metricize/client.rb
168
+ - lib/metricize/forwarder.rb
169
+ - lib/metricize/null_client.rb
170
+ - lib/metricize/shared.rb
171
+ - lib/metricize/stats.rb
172
+ - lib/metricize/version.rb
173
+ - metricize.gemspec
174
+ - spec/lib/metricize_spec.rb
175
+ - spec/spec_helper.rb
176
+ homepage: ''
177
+ licenses:
178
+ - MIT
179
+ metadata: {}
180
+ post_install_message:
181
+ rdoc_options: []
182
+ require_paths:
183
+ - lib
184
+ required_ruby_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - '>='
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - '>='
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ requirements: []
195
+ rubyforge_project:
196
+ rubygems_version: 2.0.3
197
+ signing_key:
198
+ specification_version: 4
199
+ summary: Collect, aggregate, and send metrics to a stats service
200
+ test_files:
201
+ - spec/lib/metricize_spec.rb
202
+ - spec/spec_helper.rb