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 +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.markdown +127 -0
- data/Rakefile +8 -0
- data/bin/simple_metrics_client +64 -0
- data/bin/simple_metrics_server +18 -0
- data/example/increment.rb +8 -0
- data/lib/simple_metrics.rb +97 -0
- data/lib/simple_metrics/bucket.rb +133 -0
- data/lib/simple_metrics/client.rb +83 -0
- data/lib/simple_metrics/mongo.rb +47 -0
- data/lib/simple_metrics/server.rb +66 -0
- data/lib/simple_metrics/stats.rb +99 -0
- data/lib/simple_metrics/version.rb +4 -0
- data/simple_metrics.gemspec +27 -0
- data/spec/bucket_spec.rb +181 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/stats_spec.rb +61 -0
- metadata +146 -0
    
        data/.gitignore
    ADDED
    
    
    
        data/.rvmrc
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            rvm use --create 1.9.3@simple_metrics
         | 
    
        data/Gemfile
    ADDED
    
    
    
        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,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,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,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
         | 
    
        data/spec/bucket_spec.rb
    ADDED
    
    | @@ -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
         | 
    
        data/spec/spec_helper.rb
    ADDED
    
    
    
        data/spec/stats_spec.rb
    ADDED
    
    | @@ -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
         |