simple_metrics 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,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use --create 1.9.3@simple_metrics
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in simple-metrics-server.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,127 @@
1
+ SimpleMetrics
2
+ =============
3
+
4
+ SimpleMetrics makes it easy to collect and aggregate data (specifically counters, timers and events).
5
+
6
+ It is heavily inspired by Statsd (https://github.com/etsy/statsd) from Etsy. Read the "Measure Anything, measure Everything" blog post (http://codeascraft.etsy.com/2011/02/15/measure-anything-measure-everything/) which did it for me.
7
+
8
+ Technically speaking it provides a simple UDP interface to send data to an Eventmachine based UDP Server. The data is stored in MongoDB using a Round Robin Database (RRD) scheme.
9
+
10
+ SimpleMetrics is written in Ruby and packaged as a gem.
11
+
12
+ The current version is considered ALPHA.
13
+
14
+ SimpleMetrics Client
15
+ --------------------
16
+
17
+ Commandline client:
18
+
19
+ Send a count of 5 for data point "module.test1":
20
+
21
+ simple_metrics_client module.test1 -counter 5
22
+
23
+ Send a timing of 100ms:
24
+
25
+ simple_metrics_client module.test1 -timing 100
26
+
27
+ doing the same, but since we expect a lot of calls we sample the data (10%):
28
+
29
+ simple_metrics_client module.test1 -timing 100 --sample_rate 0.1
30
+
31
+ more info:
32
+
33
+ simple_metrics_client --help
34
+
35
+ Ruby client API
36
+ ---------------
37
+
38
+ Initialize client:
39
+
40
+ client = SimpleMetrics::Client.new("localhost")
41
+
42
+ sends "com.example.test1:1|c" via UDP:
43
+
44
+ client.increment("com.example.test1")
45
+
46
+ sends "com.example.test1:-1|c":
47
+
48
+ client.decrement("com.example.test1")
49
+
50
+ sends "com.example.test1:5|c" (a counter with a relative value of 5):
51
+
52
+ client.count("com.example.test1", 5)
53
+
54
+ sends "com.example.test1:5|c|@0.1" with a sample rate of 10%:
55
+
56
+ client.count("com.example.test1", 5, 0.1)
57
+
58
+ sends "com.example.test1:5|g" (meaning gauge, an absolute value of 5):
59
+
60
+ client.count("com.example.test1", 5)
61
+
62
+ sends "com.example.test1:100|ms":
63
+
64
+ client.timing("com.example.test1")
65
+
66
+ More examples in the examples/ directory.
67
+
68
+ SimpleMetrics Server
69
+ --------------------
70
+
71
+ We provide a simple commandline wrapper using daemons gem (http://daemons.rubyforge.org/).
72
+
73
+ Start Server as background daemond:
74
+
75
+ simple_metrics_server start
76
+
77
+ Start in foreground:
78
+
79
+ simple_metrics_server start -t
80
+
81
+ Show Help:
82
+
83
+ simple_metrics_server --help
84
+
85
+ Round Robin Database Principles in MongoDB
86
+ ------------------------------------------
87
+
88
+ We use 4 collections in MongoDB each with more coarse timestamp buckets:
89
+ * 10 sec
90
+ * 1 min
91
+ * 10 min
92
+ * 1 day
93
+
94
+ The 10s and 1m collections are capped collections and have a fixed size. The other will store the data as long as we have sufficient disc space.
95
+
96
+ How can we map these times to graphs?
97
+
98
+ * 10 sec, near real-time graph (ttl: several hours)
99
+ * 1 min, last hour (ttl: several days)
100
+ * 10 min, whole day view (ttl: forever)
101
+ * 1 day , week view (ttl: forever)
102
+
103
+ License
104
+ -------
105
+
106
+ (The MIT License)
107
+
108
+ Copyright (c) 2012 Frederik Dietz <fdietz@gmail.com>
109
+
110
+ Permission is hereby granted, free of charge, to any person obtaining
111
+ a copy of this software and associated documentation files (the
112
+ 'Software'), to deal in the Software without restriction, including
113
+ without limitation the rights to use, copy, modify, merge, publish,
114
+ distribute, sublicense, and/or sell copies of the Software, and to
115
+ permit persons to whom the Software is furnished to do so, subject to
116
+ the following conditions:
117
+
118
+ The above copyright notice and this permission notice shall be
119
+ included in all copies or substantial portions of the Software.
120
+
121
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
122
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
123
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
124
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
125
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
126
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
127
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new
6
+ task :default => :spec
7
+ task :test => :spec
8
+
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+
6
+ require 'optparse'
7
+ require "simple_metrics"
8
+
9
+ options = {
10
+ :host => 'localhost',
11
+ :port => 8125,
12
+ :sample_rate => 1
13
+ }
14
+
15
+ parser ||= OptionParser.new do |opts|
16
+ opts.banner = "Usage Example: simple_metrics_send com.test.mymetric -c5"
17
+
18
+ opts.separator ""
19
+ opts.separator "Client options:"
20
+
21
+ opts.on("-c", "--counter VALUE", "Counter, a relative value") do |value|
22
+ options[:type] = 'c'
23
+ options[:stat] = value.to_i
24
+ end
25
+ opts.on("-g", "--gauge VALUE", "Gauge, an absolute value ") do |value|
26
+ options[:type] = 'g'
27
+ options[:stat] = value.to_i
28
+ end
29
+ opts.on("-t", "--timing VALUE", "A timing in ms") do |value|
30
+ options[:type] = 'ms'
31
+ options[:stat] = value.to_i
32
+ end
33
+ opts.on("-s", "--sample_rate VALUE", "An optional sample rate between 0 and 1 (example: 0.2)") do |value|
34
+ options[:sample_rate] = value.to_f || 1
35
+ end
36
+
37
+ opts.separator ""
38
+
39
+ opts.on("-a", "--address HOST", "bind to HOST address (default: #{options[:host]})") do |host|
40
+ options[:host] = host
41
+ end
42
+
43
+ opts.on("-p", "--port PORT", "use PORT (default: #{options[:port]})") do |port|
44
+ options[:port] = port.to_i
45
+ end
46
+
47
+ opts.separator ""
48
+ opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
49
+ opts.on_tail('-v', '--version', "Show version") { puts SimpleMetrics::VERSION; exit }
50
+
51
+ end.parse!(ARGV)
52
+
53
+ command = ARGV.shift
54
+ arguments = ARGV
55
+ client = SimpleMetrics::Client.new(options[:host])
56
+
57
+ case options[:type]
58
+ when'c'
59
+ client.count(command, options[:stat], options[:sample_rate])
60
+ when 'g'
61
+ client.gauge(command, options[:stat], options[:sample_rate])
62
+ when 'ms'
63
+ client.timing(command, options[:stat], options[:sample_rate])
64
+ end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+ require "simple_metrics"
6
+ require "daemons"
7
+
8
+ options = {
9
+ :backtrace => true,
10
+ :log_output => true,
11
+ :dir_mode => :script
12
+ }
13
+
14
+ Daemons.run_proc("simple_metrics", options) do
15
+ SimpleMetrics::Server.new.start
16
+ end
17
+
18
+
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+ require "rubygems"
3
+ require "bundler/setup"
4
+ require "simple_metrics"
5
+
6
+ client = SimpleMetrics::Client.new("localhost")
7
+ # com.example.test1:1|c
8
+ client.increment("com.example.test1")
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+ require "logger"
3
+
4
+ require "simple_metrics/version"
5
+ require "simple_metrics/client"
6
+ require "simple_metrics/server"
7
+ require "simple_metrics/stats"
8
+ require "simple_metrics/bucket"
9
+ require "simple_metrics/mongo"
10
+
11
+ module SimpleMetrics
12
+ extend self
13
+
14
+ def logger
15
+ @@logger ||= Logger.new(STDOUT)
16
+ end
17
+
18
+ def logger=(logger)
19
+ @@logger = logger
20
+ end
21
+
22
+ CONFIG_DEFAULTS = {
23
+ :host => 'localhost',
24
+ :port => 8125,
25
+ :flush_interval => 10
26
+ }.freeze
27
+
28
+ def config
29
+ @@config ||= CONFIG_DEFAULTS
30
+ end
31
+
32
+ def config=(options)
33
+ @@config = CONFIG_DEFAULTS.merge(options)
34
+ end
35
+
36
+ BUCKETS_DEFAULTS = [
37
+ {
38
+ :name => 'stats_per_10s',
39
+ :seconds => 10,
40
+ :capped => true,
41
+ :size => 100_100_100
42
+ },
43
+ {
44
+ :name => 'stats_per_1min',
45
+ :seconds => 60,
46
+ :capped => true,
47
+ :size => 1_100_100_100
48
+ },
49
+ {
50
+ :name => 'stats_per_10min',
51
+ :seconds => 600,
52
+ :size => 0 ,
53
+ :capped => false
54
+ },
55
+ {
56
+ :name => 'stats_per_day',
57
+ :seconds => 86400,
58
+ :size => 0,
59
+ :capped => false
60
+ }
61
+ ].freeze
62
+
63
+ def buckets_config
64
+ @@buckets ||= BUCKETS_DEFAULTS
65
+ end
66
+
67
+ def buckets_config=(buckets)
68
+ @@buckets = buckets
69
+ end
70
+
71
+ MONGODB_DEFAULTS = {
72
+ :pool_size => 5,
73
+ :timeout => 5,
74
+ :strict => true
75
+ }.freeze
76
+
77
+ DB_CONFIG_DEFAULTS = {
78
+ :host => 'localhost',
79
+ :prefix => 'development'
80
+ }.freeze
81
+
82
+ def db_config=(options)
83
+ @@db_config = {
84
+ :host => options.delete(:host),
85
+ :db_name => "simple_metrics_#{options.delete(:prefix)}",
86
+ :options => MONGODB_DEFAULTS.merge(options)
87
+ }
88
+ end
89
+
90
+ def db_config
91
+ @@db_config ||= DB_CONFIG_DEFAULTS.merge(
92
+ :db_name => "simple_metrics_#{DB_CONFIG_DEFAULTS[:prefix]}",
93
+ :options => MONGODB_DEFAULTS
94
+ )
95
+ end
96
+
97
+ end
@@ -0,0 +1,133 @@
1
+ # encoding: utf-8
2
+ module SimpleMetrics
3
+ class Bucket
4
+
5
+ class << self
6
+
7
+ def all
8
+ @@all ||= SimpleMetrics.buckets_config.map { |r| Bucket.new(r) }
9
+ end
10
+
11
+ def first
12
+ all.first
13
+ end
14
+ alias :finest :first
15
+
16
+ def [](index)
17
+ all[index]
18
+ end
19
+
20
+ def coarse_buckets
21
+ Bucket.all.sort_by! { |r| r.seconds }[1..-1]
22
+ end
23
+
24
+ def flush_stats(stats)
25
+ return if stats.empty?
26
+ SimpleMetrics.logger.info "#{Time.now} Flushing #{stats.count} counters to MongoDB"
27
+
28
+ ts = Time.now.utc.to_i
29
+ bucket = Bucket.first
30
+ stats.each { |data| bucket.save(data, ts) }
31
+
32
+ self.aggregate_all(ts)
33
+ end
34
+
35
+ def aggregate_all(ts)
36
+ ts_bucket = self.first.ts_bucket(ts)
37
+
38
+ coarse_buckets.each do |bucket|
39
+ current_ts = bucket.ts_bucket(ts_bucket)
40
+ previous_ts = bucket.previous_ts_bucket(ts_bucket)
41
+ SimpleMetrics.logger.debug "Aggregating #{bucket.name} #{previous_ts}....#{current_ts} (#{humanized_timestamp(previous_ts)}..#{humanized_timestamp(current_ts)})"
42
+
43
+ unless bucket.stats_exist_in_previous_ts?(previous_ts)
44
+ stats_coll = self.first.find_all_in_ts_range(previous_ts, current_ts)
45
+ stats_coll.group_by { |stats| stats.name }.each_pair do |name,stats_array|
46
+ stats = Stats.aggregate(stats_array)
47
+ bucket.save(stats, previous_ts)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def humanized_timestamp(ts)
56
+ Time.at(ts).utc
57
+ end
58
+ end
59
+
60
+ attr_reader :name, :capped
61
+
62
+ def initialize(attributes)
63
+ @name = attributes[:name]
64
+ @seconds = attributes[:seconds]
65
+ @capped = attributes[:capped]
66
+ @size = attributes[:size]
67
+ end
68
+
69
+ def seconds
70
+ @seconds.to_i
71
+ end
72
+
73
+ def size
74
+ @size.to_i
75
+ end
76
+
77
+ def ts_bucket(ts)
78
+ ts / seconds * seconds
79
+ end
80
+
81
+ def next_ts_bucket(ts)
82
+ ts_bucket(ts) + seconds
83
+ end
84
+
85
+ def previous_ts_bucket(ts)
86
+ ts_bucket(ts) - seconds
87
+ end
88
+
89
+ def find(id)
90
+ mongo_result = mongo_coll.find_one({ :_id => id })
91
+ Stats.create_from_db(mongo_result)
92
+ end
93
+
94
+ def find_all_by_name(name)
95
+ mongo_result = mongo_coll.find({ :name => name })
96
+ mongo_result.inject([]) { |result, a| result << Stats.create_from_db(a) }
97
+ end
98
+
99
+ def find_all_in_ts(ts)
100
+ mongo_result = mongo_coll.find({ :ts => ts_bucket(ts) })
101
+ mongo_result.inject([]) { |result, a| result << Stats.create_from_db(a) }
102
+ end
103
+
104
+ def find_all_in_ts_by_name(ts, name)
105
+ mongo_result = mongo_coll.find({ :ts => ts_bucket(ts), :name => name })
106
+ mongo_result.inject([]) { |result, a| result << Stats.create_from_db(a) }
107
+ end
108
+
109
+ def find_all_in_ts_range(previous_ts, current_ts)
110
+ mongo_result = mongo_coll.find({ :ts => { "$gte" => previous_ts, "$lt" => current_ts }}).to_a
111
+ mongo_result.inject([]) { |result, a| result << Stats.create_from_db(a) }
112
+ end
113
+
114
+ def stats_exist_in_previous_ts?(ts)
115
+ mongo_coll.find({ :ts => ts }).count > 0
116
+ end
117
+
118
+ def save(stats, ts)
119
+ stats.ts = ts_bucket(ts)
120
+ result = mongo_coll.insert(stats.attributes)
121
+ SimpleMetrics.logger.debug "SERVER: MongoDB - insert in #{name}: #{stats.inspect}, result: #{result}"
122
+ end
123
+
124
+ def mongo_coll
125
+ Mongo.collection(name)
126
+ end
127
+
128
+ def capped?
129
+ @capped == true
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,83 @@
1
+ # encoding: utf-8
2
+ require "socket"
3
+
4
+ module SimpleMetrics
5
+
6
+ class Client
7
+ VERSION = "0.0.1"
8
+
9
+ def initialize(host, port = 8125)
10
+ @host, @port = host, port
11
+ end
12
+
13
+ # send relative value
14
+ def increment(stat, sample_rate = 1)
15
+ count(stat, 1, sample_rate)
16
+ end
17
+
18
+ # send relative value
19
+ def decrement(stat, sample_rate = 1)
20
+ count(stat, -1, sample_rate)
21
+ end
22
+
23
+ # send relative value
24
+ def count(stat, count, sample_rate = 1)
25
+ send_data( stat, count, 'c', sample_rate)
26
+ end
27
+
28
+ # send absolute value
29
+ # TODO: check if this is actually supported by Statsd server
30
+ def gauge(stat, value)
31
+ send_data(stat, value, 'g')
32
+ end
33
+
34
+ # Sends a timing (in ms) (glork)
35
+ def timing(stat, ms, sample_rate = 1)
36
+ send_data(stat, ms, 'ms', sample_rate)
37
+ end
38
+
39
+ # Sends a timing (in ms) block based
40
+ def time(stat, sample_rate = 1, &block)
41
+ start = Time.now
42
+ result = block.call
43
+ timing(stat, ((Time.now - start) * 1000).round, sample_rate)
44
+ result
45
+ end
46
+
47
+ private
48
+
49
+ def sampled(sample_rate, &block)
50
+ if sample_rate < 1
51
+ block.call if rand <= sample_rate
52
+ else
53
+ block.call
54
+ end
55
+ end
56
+
57
+ def send_data(stat, delta, type, sample_rate = 1)
58
+ sampled(sample_rate) do
59
+ data = "#{stat}:#{delta}|#{type}" # TODO: check stat is valid
60
+ data << "|@#{sample_rate}" if sample_rate < 1
61
+ send_to_socket(data)
62
+ end
63
+ end
64
+
65
+ def send_to_socket(data)
66
+ logger.debug "SimpleMetrics Client send: #{data}"
67
+ socket.send(data, 0, @host, @port)
68
+ rescue Exception => e
69
+ puts e.backtrace
70
+ logger.error "SimpleMetrics Client error: #{e}"
71
+ end
72
+
73
+ def socket
74
+ @socket ||= UDPSocket.new
75
+ end
76
+
77
+ def logger
78
+ @logger ||= SimpleMetrics.logger
79
+ end
80
+
81
+ end
82
+
83
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+ require "mongo"
3
+
4
+ module SimpleMetrics
5
+ module Mongo
6
+ extend self
7
+
8
+ def ensure_collections_exist
9
+ SimpleMetrics.logger.debug "SERVER: MongoDB - found following collections: #{db.collection_names.inspect}"
10
+ Bucket.all.each do |bucket|
11
+ unless db.collection_names.include?(bucket.name)
12
+ db.create_collection(bucket.name, :capped => bucket.capped, :size => bucket.size)
13
+ SimpleMetrics.logger.debug "SERVER: MongoDB - created collection #{bucket.name}, capped: #{bucket.capped}, size: #{bucket.size}"
14
+ end
15
+ db.collection(bucket.name).ensure_index([['ts', ::Mongo::ASCENDING]])
16
+ SimpleMetrics.logger.debug "SERVER: MongoDB - ensure index on column ts for collection #{bucket.name}"
17
+ end
18
+ end
19
+
20
+ def truncate_collections
21
+ Bucket.all.each do |bucket|
22
+ if db.collection_names.include?(bucket.name)
23
+ if bucket.capped?
24
+ collection(bucket.name).drop # capped collections can't remove elements, drop it instead
25
+ else
26
+ collection(bucket.name).remove
27
+ end
28
+ SimpleMetrics.logger.debug "SERVER: MongoDB - truncated collection #{bucket.name}"
29
+ end
30
+ end
31
+ end
32
+
33
+ @@collection = {}
34
+ def collection(name)
35
+ @@collection[name] ||= db.collection(name)
36
+ end
37
+
38
+ def connection
39
+ @@connection ||= ::Mongo::Connection.new(SimpleMetrics.db_config[:host])
40
+ end
41
+
42
+ def db
43
+ @@db ||= connection.db(SimpleMetrics.db_config[:db_name], SimpleMetrics.db_config[:options])
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: utf-8
2
+ require "eventmachine"
3
+
4
+ module SimpleMetrics
5
+
6
+ module ClientHandler
7
+
8
+ @@stats = []
9
+
10
+ class << self
11
+ def get_and_clear_stats
12
+ stats = @@stats.dup
13
+ @@stats = []
14
+ stats
15
+ end
16
+ end
17
+
18
+ def stats
19
+ @@stats
20
+ end
21
+
22
+ def post_init
23
+ SimpleMetrics.logger.info "ClientHandler entering post_init"
24
+ end
25
+
26
+ def receive_data(data)
27
+ SimpleMetrics.logger.debug "received_data: #{data.inspect}"
28
+
29
+ @@stats ||= []
30
+ @@stats << Stats.parse(data)
31
+ rescue Stats::ParserError => e
32
+ SimpleMetrics.logger.debug "Invalid Data skipped: #{data}"
33
+ end
34
+ end
35
+
36
+ class Server
37
+
38
+ attr_reader :db, :connection
39
+
40
+ def start
41
+ SimpleMetrics.logger.info "SERVER: starting up on #{SimpleMetrics.config[:host]}:#{SimpleMetrics.config[:port]}..."
42
+
43
+ Mongo.ensure_collections_exist
44
+
45
+ EM.run do
46
+ EM.open_datagram_socket(SimpleMetrics.config[:host], SimpleMetrics.config[:port], SimpleMetrics::ClientHandler) do |con|
47
+ EventMachine::add_periodic_timer(SimpleMetrics.config[:flush_interval]) do
48
+ SimpleMetrics.logger.debug "SERVER: period timer triggered after #{SimpleMetrics.config[:flush_interval]} seconds"
49
+
50
+ EM.defer { Bucket.flush_stats(ClientHandler.get_and_clear_stats) }
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def stop
57
+ SimpleMetrics.logger.info "EventMachine stop"
58
+ EM.stop
59
+ end
60
+
61
+ def to_s
62
+ "#{SimpleMetrics.config[:host]}:#{SimpleMetrics.config[:port]}"
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,99 @@
1
+ # encoding: utf-8
2
+ module SimpleMetrics
3
+
4
+ class Stats
5
+
6
+ class NonMatchingTypesError < Exception; end
7
+ class ParserError < Exception; end
8
+
9
+ # examples:
10
+ # com.example.test1:1|c
11
+ # com.example.test2:-1|c
12
+ # com.example.test2:50|g
13
+ # com.example.test3:5|c|@0.1
14
+ # com.example.test4:44|ms
15
+ REGEXP = /^([\d\w_.]*):(-?[\d]*)\|(c|g|ms){1}(\|@([.\d]+))?$/i
16
+
17
+ class << self
18
+
19
+ def parse(str)
20
+ if str =~ REGEXP
21
+ name, value, type, sample_rate = $1, $2, $3, $5
22
+ if type == "ms"
23
+ # TODO: implement sample_rate handling
24
+ create_timing(:name => name, :value => value)
25
+ elsif type == "g"
26
+ create_gauge(:name => name, :value => (value.to_i || 1) * (1.0 / (sample_rate || 1).to_f) )
27
+ elsif type == "c"
28
+ create_counter(:name => name, :value => (value.to_i || 1) * (1.0 / (sample_rate || 1).to_f) )
29
+ end
30
+ else
31
+ raise ParserError, "Parser Error - Invalid Stat: #{str}"
32
+ end
33
+ end
34
+
35
+ def create_counter(attributes)
36
+ Stats.new(attributes.merge(:type => 'c'))
37
+ end
38
+
39
+ def create_gauge(attributes)
40
+ Stats.new(attributes.merge(:type => 'g'))
41
+ end
42
+
43
+ def create_timing(attributes)
44
+ Stats.new(attributes.merge(:type => 'ms'))
45
+ end
46
+
47
+ def aggregate(stats_array)
48
+ raise NonMatchingTypesError unless stats_array.group_by { |stats| stats.type }.size == 1
49
+
50
+ result_stat = stats_array.first.dup
51
+ result_stat.value = stats_array.map { |stats| stats.value }.inject(0) { |result, value| result += value }
52
+ result_stat
53
+ end
54
+
55
+ def create_from_db(attributes)
56
+ Stats.new(:name => attributes["name"], :value => attributes["value"], :ts => attributes["ts"], :type => attributes["type"])
57
+ end
58
+ end
59
+
60
+ attr_accessor :name, :ts, :type, :value
61
+
62
+ def initialize(attributes)
63
+ @name = attributes[:name]
64
+ @value = attributes[:value]
65
+ @ts = attributes[:ts]
66
+ @type = attributes[:type]
67
+ end
68
+
69
+ def counter?
70
+ type == 'c'
71
+ end
72
+
73
+ def gauge?
74
+ type == 'g'
75
+ end
76
+
77
+ def timing?
78
+ type == 'ms'
79
+ end
80
+
81
+ def timestamp
82
+ ts
83
+ end
84
+
85
+ def value
86
+ @value.to_i
87
+ end
88
+
89
+ def attributes
90
+ {
91
+ :name => name,
92
+ :value => value,
93
+ :ts => ts,
94
+ :type => type
95
+ }
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+ module SimpleMetrics
3
+ VERSION = "0.0.1"
4
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "simple_metrics/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "simple_metrics"
7
+ s.version = SimpleMetrics::VERSION
8
+ s.authors = ["Frederik Dietz"]
9
+ s.email = ["fdietz@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{SimpleMetrics}
12
+ s.description = %q{SimpleMetrics}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency "rake"
20
+ s.add_development_dependency "rspec"
21
+ s.add_development_dependency "rr"
22
+
23
+ s.add_dependency "eventmachine"
24
+ s.add_dependency "daemons"
25
+ s.add_dependency "mongo"
26
+ s.add_dependency "bson_ext"
27
+ end
@@ -0,0 +1,181 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ module SimpleMetrics
5
+
6
+ describe Bucket do
7
+
8
+ let(:bucket) do
9
+ Bucket.first
10
+ end
11
+
12
+ let(:sec) do
13
+ bucket.seconds
14
+ end
15
+
16
+ let(:ts) do
17
+ Time.now.utc.to_i
18
+ end
19
+
20
+ describe "#ts_bucket" do
21
+ it "calculates timestamp for current bucket" do
22
+ bucket.ts_bucket(ts).should == ts/sec*sec
23
+ end
24
+ end
25
+
26
+ describe "#next_ts_bucket" do
27
+ it "calculates timestamp for next bucket" do
28
+ bucket.next_ts_bucket(ts).should == ts/sec*sec+sec
29
+ end
30
+ end
31
+
32
+ describe "#previous_ts_bucket" do
33
+ it "calculates timestamp for previous bucket" do
34
+ bucket.previous_ts_bucket(ts).should == ts/sec*sec-sec
35
+ end
36
+ end
37
+
38
+ describe "#save" do
39
+ before do
40
+ Mongo.truncate_collections
41
+ Mongo.ensure_collections_exist
42
+ bucket.save(stats, ts)
43
+ end
44
+
45
+ let(:stats) do
46
+ Stats.create_counter(:name => "key1", :value => 5)
47
+ end
48
+
49
+ it "saves given data in bucket" do
50
+ results = bucket.find_all_by_name("key1")
51
+ results.should have(1).item
52
+ result = results.first
53
+ result.name.should == stats.name
54
+ result.value.should == stats.value
55
+ result.type.should == stats.type
56
+ end
57
+
58
+ it "saves data in correct timestamp" do
59
+ result = bucket.find_all_by_name("key1").first
60
+ result.ts.should == ts/sec*sec
61
+ end
62
+
63
+ end # describe "#save" do
64
+
65
+ describe "finder methods" do
66
+
67
+ before do
68
+ Mongo.truncate_collections
69
+ Mongo.ensure_collections_exist
70
+ end
71
+
72
+ describe "#find_all_by_name" do
73
+ it "returns all stats for given name" do
74
+ stats_same1 = Stats.create_counter(:name => "key1", :value => 5)
75
+ stats_same2 = Stats.create_counter(:name => "key1", :value => 3)
76
+ stats_different = Stats.create_counter(:name => "key2", :value => 3)
77
+
78
+ bucket.save(stats_same1, ts)
79
+ bucket.save(stats_same2, ts)
80
+ bucket.save(stats_different, ts)
81
+
82
+ results = bucket.find_all_by_name("key1")
83
+ results.should have(2).items
84
+ results.first.name.should == stats_same1.name
85
+ end
86
+ end
87
+
88
+ describe "#find_all_in_ts" do
89
+ it "returns all stats in given timestamp" do
90
+ stats1 = Stats.create_counter(:name => "key1", :value => 5)
91
+ stats2 = Stats.create_counter(:name => "key2", :value => 3)
92
+
93
+ bucket.save(stats1, ts)
94
+ bucket.save(stats2, bucket.next_ts_bucket(ts))
95
+
96
+ result1 = bucket.find_all_in_ts(ts).first
97
+ result1.name.should == stats1.name
98
+ result1.value.should == stats1.value
99
+
100
+ result2 = bucket.find_all_in_ts(bucket.next_ts_bucket(ts)).first
101
+ result2.name.should == stats2.name
102
+ result2.value.should == stats2.value
103
+ end
104
+ end
105
+
106
+ describe "#find_all_in_ts_by_name" do
107
+ it "returns all stats for given name and timestamp" do
108
+ stats1a = Stats.create_counter(:name => "key1", :value => 5)
109
+ stats1b = Stats.create_counter(:name => "key1", :value => 7)
110
+ stats2 = Stats.create_counter(:name => "key2", :value => 7)
111
+ stats1_different_ts = Stats.create_counter(:name => "key1", :value => 3)
112
+
113
+ bucket.save(stats1a, ts)
114
+ bucket.save(stats1b, ts)
115
+ bucket.save(stats2, ts)
116
+ bucket.save(stats1_different_ts, bucket.next_ts_bucket(ts))
117
+
118
+ results = bucket.find_all_in_ts_by_name(ts, "key1")
119
+ results.should have(2).items
120
+ results.first.name.should == "key1"
121
+ results.last.name.should == "key1"
122
+ end
123
+ end
124
+
125
+ end # describe "finder methods"
126
+
127
+ describe "#aggregate_all" do
128
+ before do
129
+ Mongo.truncate_collections
130
+ Mongo.ensure_collections_exist
131
+ end
132
+
133
+ it "aggregates all stats" do
134
+ stats1a = Stats.create_counter(:name => "key1", :value => 5)
135
+ stats1b = Stats.create_counter(:name => "key1", :value => 7)
136
+ stats2 = Stats.create_counter(:name => "key2", :value => 3)
137
+
138
+ bucket2 = Bucket[1]
139
+ ts_at_insert = bucket2.previous_ts_bucket(ts)
140
+ bucket.save(stats1a, ts_at_insert)
141
+ bucket.save(stats1b, ts_at_insert)
142
+ bucket.save(stats2, ts_at_insert)
143
+
144
+ Bucket.aggregate_all(ts)
145
+
146
+ results = bucket2.find_all_in_ts(ts_at_insert)
147
+ results.should have(2).items
148
+
149
+ key1_result = results.find {|stat| stat.name == "key1"}
150
+ key1_result.value.should == 12
151
+ key1_result.should be_counter
152
+
153
+ key2_result = results.find {|stat| stat.name == "key2"}
154
+ key2_result.value.should == 3
155
+ key2_result.should be_counter
156
+ end
157
+ end # describe "#aggregate_all"
158
+
159
+ describe "#flush_stats" do
160
+ before do
161
+ stats1 = Stats.create_counter(:name => "key1", :value => 5)
162
+ stats2 = Stats.create_counter(:name => "key1", :value => 7)
163
+ stats3 = Stats.create_counter(:name => "key2", :value => 3)
164
+ @stats = [stats1, stats2, stats3]
165
+ end
166
+
167
+ it "saves all stats in finest/first bucket" do
168
+ Bucket.flush_stats(@stats)
169
+
170
+ results = bucket.find_all_in_ts(ts)
171
+ results.should have(3).items
172
+ end
173
+
174
+ it "calls aggregate_all afterwards" do
175
+ mock(Bucket).aggregate_all(ts)
176
+ Bucket.flush_stats(@stats)
177
+ end
178
+ end # describe "#flush_stats"
179
+
180
+ end
181
+ end
@@ -0,0 +1,10 @@
1
+ require "simple_metrics"
2
+
3
+ RSpec.configure do |config|
4
+ config.mock_with :rr
5
+ end
6
+
7
+ SimpleMetrics.logger = Logger.new('/dev/null')
8
+ SimpleMetrics.db_config = { :host => 'localhost', :prefix => 'test' }
9
+
10
+
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ module SimpleMetrics
5
+
6
+ describe Bucket do
7
+
8
+ describe "#parse" do
9
+
10
+ it "parses increment counter" do
11
+ stats = Stats.parse("com.example.test1:1|c")
12
+ stats.name.should == "com.example.test1"
13
+ stats.value.should == 1
14
+ stats.should be_counter
15
+ end
16
+
17
+ it "parses decrement counter" do
18
+ stats = Stats.parse("com.example.test1:-1|c")
19
+ stats.name.should == "com.example.test1"
20
+ stats.value.should == -1
21
+ stats.should be_counter
22
+ end
23
+
24
+ it "parses counter with sample rate" do
25
+ stats = Stats.parse("com.example.test2:5|c|@0.1")
26
+ stats.name.should == "com.example.test2"
27
+ stats.value.should == 50
28
+ stats.should be_counter
29
+ end
30
+
31
+ it "parses increment gauge" do
32
+ stats = Stats.parse("com.example.test3:5|g")
33
+ stats.name.should == "com.example.test3"
34
+ stats.value.should == 5
35
+ stats.should be_gauge
36
+ end
37
+
38
+ it "parses increment gauge with sample rate" do
39
+ stats = Stats.parse("com.example.test3:5|g|@0.1")
40
+ stats.name.should == "com.example.test3"
41
+ stats.value.should == 50
42
+ stats.should be_gauge
43
+ end
44
+
45
+ it "parses increment timing" do
46
+ stats = Stats.parse("com.example.test4:44|ms")
47
+ stats.name.should == "com.example.test4"
48
+ stats.value.should == 44
49
+ stats.should be_timing
50
+ end
51
+
52
+ it "parses increment timing with sample rate" do
53
+ end
54
+ end
55
+
56
+
57
+ describe "create_counter" do
58
+ end
59
+ end
60
+
61
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_metrics
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Frederik Dietz
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70244480135940 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70244480135940
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70244480135460 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70244480135460
36
+ - !ruby/object:Gem::Dependency
37
+ name: rr
38
+ requirement: &70244480135020 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70244480135020
47
+ - !ruby/object:Gem::Dependency
48
+ name: eventmachine
49
+ requirement: &70244480134600 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70244480134600
58
+ - !ruby/object:Gem::Dependency
59
+ name: daemons
60
+ requirement: &70244480134180 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70244480134180
69
+ - !ruby/object:Gem::Dependency
70
+ name: mongo
71
+ requirement: &70244480133740 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: *70244480133740
80
+ - !ruby/object:Gem::Dependency
81
+ name: bson_ext
82
+ requirement: &70244480133300 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: *70244480133300
91
+ description: SimpleMetrics
92
+ email:
93
+ - fdietz@gmail.com
94
+ executables:
95
+ - simple_metrics_client
96
+ - simple_metrics_server
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - .gitignore
101
+ - .rvmrc
102
+ - Gemfile
103
+ - README.markdown
104
+ - Rakefile
105
+ - bin/simple_metrics_client
106
+ - bin/simple_metrics_server
107
+ - example/increment.rb
108
+ - lib/simple_metrics.rb
109
+ - lib/simple_metrics/bucket.rb
110
+ - lib/simple_metrics/client.rb
111
+ - lib/simple_metrics/mongo.rb
112
+ - lib/simple_metrics/server.rb
113
+ - lib/simple_metrics/stats.rb
114
+ - lib/simple_metrics/version.rb
115
+ - simple_metrics.gemspec
116
+ - spec/bucket_spec.rb
117
+ - spec/spec_helper.rb
118
+ - spec/stats_spec.rb
119
+ homepage: ''
120
+ licenses: []
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 1.8.15
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: SimpleMetrics
143
+ test_files:
144
+ - spec/bucket_spec.rb
145
+ - spec/spec_helper.rb
146
+ - spec/stats_spec.rb