├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── benchmarks ├── block_vs_interpolation.rb ├── increment_counts.rb ├── inline_vs_return.rb ├── measures_timings.rb ├── numeric_detection_float.rb ├── numeric_detection_int.rb └── worker_check_vs_forking_check.rb ├── certs └── librato-public.pem ├── lib ├── librato-rack.rb └── librato │ ├── collector.rb │ ├── collector │ ├── aggregator.rb │ ├── counter_cache.rb │ ├── exceptions.rb │ └── group.rb │ ├── rack.rb │ └── rack │ ├── configuration.rb │ ├── configuration │ └── suites.rb │ ├── errors.rb │ ├── logger.rb │ ├── tracker.rb │ ├── validating_queue.rb │ ├── version.rb │ └── worker.rb ├── librato-rack.gemspec └── test ├── apps ├── basic.ru ├── custom.ru ├── custom_suites.ru ├── no_stats.ru └── queue_wait.ru ├── integration ├── custom_test.rb ├── no_stats_test.rb ├── no_suites_test.rb ├── queue_wait_test.rb ├── request_test.rb └── suites_test.rb ├── remote └── tracker_test.rb ├── support └── environment_helpers.rb ├── test_helper.rb └── unit ├── collector ├── aggregator_test.rb ├── counter_cache_test.rb └── group_test.rb ├── collector_test.rb └── rack ├── configuration └── suites_test.rb ├── configuration_test.rb ├── logger_test.rb ├── tracker_test.rb ├── validating_queue_test.rb └── worker_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .ruby-version 2 | 3 | .bundle 4 | .DS_Store 5 | Gemfile.lock 6 | *.gem 7 | 8 | # docs 9 | rdoc 10 | doc 11 | .yardoc 12 | 13 | env/* 14 | 15 | # dummy app 16 | test/dummy/db/*.sqlite3 17 | test/dummy/log/*.log 18 | test/dummy/tmp/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - jruby-19mode 3 | - 1.9.3 4 | - 2.1.10 5 | - 2.2.10 6 | - 2.3.8 7 | - 2.4.5 8 | - 2.5.3 9 | - ruby-head 10 | 11 | before_install: 12 | - gem update bundler 13 | 14 | matrix: 15 | allow_failures: 16 | - rvm: ruby-head 17 | - rvm: rbx 18 | 19 | notifications: 20 | email: 21 | on_success: change # only send if status changes 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Version 2.0.3 2 | * Fix bug to prevent `rack.processes` reporting when `rack` suite is disabled (#75, Sam Moore) 3 | 4 | ### Version 2.0.2 5 | * Update `ValidatingQueue` tag value restrictions (#72) 6 | 7 | ### Version 2.0.1 8 | * Ensure `rack.request.queue.time` metric is not negative (#68, Austin Schneider) 9 | 10 | ### Version 2.0.0 11 | * Add support for tagged measurements (#54). **NOTE**: This version introduces breaking changes for legacy sources. Please contact support@librato.com to learn more. 12 | 13 | ### Version 1.1.0 14 | * Fix deprecation warnings in ruby 2.4 (#57, Ben Radler) 15 | 16 | ### Version 1.0.1 17 | * Fix missing p95 for rack.request.time 18 | 19 | ### Version 1.0.0 20 | * Add support for configurable metric suites 21 | * Drop support for long-deprecated config via LIBRATO_METRICS_* env vars 22 | * Drop support for old-style config passing during initialization 23 | * Deprecate `disable_rack_metrics` config option, use `suites='none'` instead 24 | 25 | ### Version 0.6.0 26 | * Add support for proxy configuration 27 | 28 | ### Version 0.5.0 29 | * Add support for percentiles when timing 30 | * Report p95 for rack.request.time and rack.request.queue.time 31 | 32 | ### Version 0.4.5 33 | * Add #start! to tracker 34 | 35 | ### Version 0.4.4 36 | * Relax version constraint for librato-metrics 37 | 38 | ### Version 0.4.3 39 | * Update queue wait support to tolerate float-style timestamps 40 | 41 | ### Version 0.4.2 42 | * Move gem sign code to rake task, fixes bug bundling in some environments 43 | 44 | ### Version 0.4.1 45 | * Support a pre-configured tracker object 46 | * Make log-prefix configurable 47 | * Break pid-locking out of startup checks 48 | 49 | ### Version 0.4.0 50 | * Add HTTP method (GET, POST) metrics 51 | * Add log buffering support 52 | * Ensure all options passed to a grouped increment are respected 53 | * LIBRATO_AUTORUN can be used to prevent startup 54 | * Add ability to interrupt reporter process 55 | * Start reporting deprecations for old config methods 56 | * Add docs for best practices for background workers 57 | * Other documentation improvements 58 | 59 | ### Version 0.3.0 60 | * Add experimental support for EventMachine and EMSynchrony (Balwant K) 61 | * Start testing suite against jruby/rbx 62 | * Gem is now signed 63 | 64 | ### Version 0.2.1 65 | * Fix exception if logging metrics before middleware init (Eric Holmes) 66 | 67 | ### Version 0.2.0 68 | * Add disable_rack_metrics config option 69 | * Remove metrics based on deprecated heroku HTTP headers 70 | * Ensure compatibility with ruby 2.0 71 | 72 | ### Version 0.1.0 73 | * Initial version 74 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | # test application 5 | gem 'rack-test' 6 | gem 'sinatra' 7 | 8 | # mocks 9 | gem 'mocha', :require => false 10 | 11 | # debugging 12 | gem 'pry' 13 | 14 | # benchmarking 15 | gem 'benchmark_suite' 16 | 17 | # resolve load order issue 18 | gem 'rake' 19 | 20 | # used for variable timer modes 21 | gem 'eventmachine' 22 | gem 'em-synchrony' 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012. Librato, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Librato, Inc. nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL LIBRATO, INC. BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | librato-rack 2 | ======= 3 | 4 | [![Gem Version](https://badge.fury.io/rb/librato-rack.png)](http://badge.fury.io/rb/librato-rack) [![Build Status](https://secure.travis-ci.org/librato/librato-rack.png?branch=master)](http://travis-ci.org/librato/librato-rack) [![Code Climate](https://codeclimate.com/github/librato/librato-rack.png)](https://codeclimate.com/github/librato/librato-rack) 5 | 6 | --- 7 | 8 | ## Important note on breaking change 9 | **NOTE:** Starting with version 2.0.0 librato-rack requires a Librato account that [supports tagged metrics](https://www.librato.com/docs/kb/faq/account_questions/tags_or_sources/). 10 | 11 | If your Librato account doesn't yet support tagged metrics or you are using [a heroku addon](https://devcenter.heroku.com/articles/librato#using-with-ruby), please use the [1.x.x version](https://rubygems.org/gems/librato-rack/versions/1.1.0). 12 | 13 | --- 14 | 15 | `librato-rack` provides rack middleware which will report key statistics for your rack applications to [Librato Metrics](https://metrics.librato.com/). It will also allow you to easily track your own custom metrics. Metrics are delivered asynchronously behind the scenes so they won't affect performance of your requests. 16 | 17 | Currently Ruby 1.9.2+ is required. 18 | 19 | ## Upgrading 20 | 21 | Upgrading from version 1.x to 2.x introduces breaking changes for legacy sources. Please contact [support@librato.com](mailto:support@librato.com) to migrate an existing Librato account. 22 | 23 | ## Quick Start 24 | 25 | Install `librato-rack` as middleware in your application: 26 | 27 | use Librato::Rack 28 | 29 | Configuring and relaunching your application will start the reporting of performance and request metrics. You can also track custom metrics by adding simple one-liners to your code: 30 | 31 | # keep counts of key events 32 | Librato.increment 'user.signup' 33 | 34 | # benchmark sections of code to verify production performance 35 | Librato.timing 'my.complicated.work' do 36 | # do work 37 | end 38 | 39 | # track averages across requests 40 | Librato.measure 'user.social_graph.nodes', user.social_graph.size 41 | 42 | ## Installation & Configuration 43 | 44 | Install the gem: 45 | 46 | $ gem install librato-rack 47 | 48 | Or add to your Gemfile if using bundler: 49 | 50 | gem "librato-rack" 51 | 52 | In your rackup file or equivalent, require and add the middleware: 53 | 54 | require 'librato-rack' 55 | use Librato::Rack 56 | 57 | In order to get the most accurate measurements, it is recommended that the `librato-rack` middleware be the first middleware in your stack. This will ensure that timing measurements like the `rack.request.time` metric will include all of the time spent in the Rack middleware stack. 58 | 59 | If you don't have a Metrics account already, [sign up](https://metrics.librato.com/). In order to send measurements to Metrics you need to provide your account credentials to `librato-rack`. You can provide these one of two ways: 60 | 61 | ##### Use environment variables 62 | 63 | By default you can use `LIBRATO_USER` and `LIBRATO_TOKEN` to pass your account data to the middleware. While these are the only required variables, there are a few more optional environment variables you may find useful. 64 | 65 | * `LIBRATO_TAGS` - the default tags to use for submitted metrics. Format is comma-separated key=value pairs, e.g. `region=us-east,az=b`. If not set, `host` of the executing machine is detected and set as default tag 66 | * `LIBRATO_SUITES` - manage which metrics librato-rack will report. See more in [metrics suites](#metric-suites). 67 | * `LIBRATO_PREFIX` - a prefix which will be prepended to all metric names 68 | * `LIBRATO_LOG_LEVEL` - see logging section for more 69 | * `LIBRATO_PROXY` - HTTP proxy to use when connecting to the Librato API (can also use the `https_proxy` or `http_proxy` environment variable commonly supported by Linux command line utilities) 70 | * `LIBRATO_AUTORUN` - set to `'0'` to prevent the reporter from starting, useful if you don't want `librato-rack` to start under certain circumstances 71 | * `LIBRATO_EVENT_MODE` - use with evented apps, see "Use with EventMachine" below 72 | 73 | ##### Use a configuration object 74 | 75 | If you want to do more complex configuration, use your own environment variables, or control your configuration in code, you can use a configuration object: 76 | 77 | config = Librato::Rack::Configuration.new 78 | config.user = 'myuser@mysite.com' 79 | config.token = 'mytoken' 80 | # …more configuration 81 | 82 | use Librato::Rack, :config => config 83 | 84 | See the [configuration class](https://github.com/librato/librato-rack/blob/master/lib/librato/rack/configuration.rb) for all available options. 85 | 86 | ##### Running on Heroku 87 | 88 | If you are using the [Librato Metrics Heroku addon](https://addons.heroku.com/librato), your `LIBRATO_USER` and `LIBRATO_TOKEN` environment variables will already be set in your Heroku environment. If you are running without the addon you will need to provide them yourself. 89 | 90 | NOTE: if Heroku idles your application no measurements will be sent until it receives another request and is restarted. If you see intermittent gaps in your measurements during periods of low traffic this is the most likely cause. 91 | 92 | ## Default Tags 93 | 94 | Librato Metrics supports tagged measurements that are associated with a metric, one or more tag pairs, and a point in time. For more information on tagged measurements, visit our [API documentation](https://www.librato.com/docs/api/#measurements). 95 | 96 | ##### Detected Tags 97 | 98 | By default, `host` is detected and applied as a default tag for submitted measurements. Optionally, you can override the detected values, e.g. `LIBRATO_TAGS=host=myapp-prod-1` 99 | 100 | ##### Custom Tags 101 | 102 | In addition to the default tags, you can also provide custom tags: 103 | 104 | ```ruby 105 | config = Librato::Rack::Configuration.new 106 | config.user = 'myuser@mysite.com' 107 | config.token = 'mytoken' 108 | config.tags = { service: 'myapp', environment: 'production', host: 'myapp-prod-1' } 109 | 110 | use Librato::Rack, :config => config 111 | ``` 112 | 113 | ##### Metric Suites 114 | 115 | The metrics recorded by `librato-rack` are organized into named metric suites that can be selectively enabled/disabled: 116 | 117 | * `rack`: The `rack.request.total`, `rack.request.time`, `rack.request.slow`, `rack.request.queue.time`, and `rack.processes` metrics 118 | * `rack_status`: `rack.request.status` metric with `status` tag name and HTTP status code tag value, e.g. `status=200` 119 | * `rack_method`: `rack.request.method` metric with `method` tag name and HTTP method tag value, e.g. `method=POST` 120 | 121 | All three of the metric suites listed above are enabled by default. 122 | 123 | The metric suites can be configured via either the `LIBRATO_SUITES` environment variable or the `suites` attributes on the `Librato::Rack::Configuration` object. 124 | 125 | LIBRATO_SUITES="rack,rack_method" # use ONLY the rack and rack_method suites 126 | LIBRATO_SUITES="+foo,+bar" # + prefix indicates that you want the default suites plus foo and bar 127 | LIBRATO_SUITES="-rack_status" # - prefix indicates that you want the default suites removing rack_status 128 | LIBRATO_SUITES="+foo,-rack_status" # Use all default suites except for rack_status while also adding foo 129 | LIBRATO_SUITES="all" # Enable all suites 130 | LIBRATO_SUITES="none" # Disable all suites 131 | LIBRATO_SUITES="" # Use only the default suites (same as if env var is absent) 132 | 133 | Note that you should EITHER specify an explict list of suites to enable OR add/subtract individual suites from the default list (using the +/- prefixes). If you try to mix these two forms a `Librato::Rack::InvalidSuiteConfiguration` error will be raised. 134 | 135 | ##### Use with EventMachine and EM Synchrony 136 | 137 | `librato-rack` has experimental support for EventMachine and EM Synchrony apps. 138 | 139 | When using in an evented context set LIBRATO_EVENT_MODE to `'eventmachine'` if using [EventMachine](https://github.com/eventmachine/eventmachine) or `'synchrony'` if using [EM Synchrony](https://github.com/igrigorik/em-synchrony) and/or [Rack::FiberPool](https://github.com/alebsack/rack-fiber_pool). We're interested in maturing this support, so please let us know if you have any issues. 140 | 141 | ## Custom Measurements 142 | 143 | Tracking anything that interests you is easy with Metrics. There are four primary helpers available: 144 | 145 | #### increment 146 | 147 | Use for tracking a running total of something _across_ requests, examples: 148 | 149 | ```ruby 150 | # increment the 'sales_completed' metric by one 151 | Librato.increment 'sales.completed' 152 | # => {:host=>"myapp-prod-1"} 153 | 154 | # increment by five 155 | Librato.increment 'items.purchased', by: 5 156 | # => {:host=>"myapp-prod-1"} 157 | 158 | # increment with custom per-measurement tags 159 | Librato.increment 'user.purchases', tags: { user_id: user.id, currency: 'USD' } 160 | # => {:user_id=>43, :currency=>"USD"} 161 | 162 | # increment with custom per-measurement tags and inherited default tags 163 | Librato.increment 'user.purchases', tags: { user_id: user.id, currency: 'USD' }, inherit_tags: true 164 | # => {:host=>"myapp-prod-1", :user_id=>43, :currency=>"USD"} 165 | ``` 166 | 167 | Other things you might track this way: user signups, requests of a certain type or to a certain route, total jobs queued or processed, emails sent or received 168 | 169 | ###### Sporadic Increment Reporting 170 | 171 | Note that `increment` is primarily used for tracking the rate of occurrence of some event. Given this increment metrics are _continuous by default_: after being called on a metric once they will report on every interval, reporting zeros for any interval when increment was not called on the metric. 172 | 173 | Especially with custom per-measurement tags you may want the opposite behavior - reporting a measurement only during intervals where `increment` was called on the metric: 174 | 175 | ```ruby 176 | # report a value for 'user.uploaded_file' only during non-zero intervals 177 | Librato.increment 'user.uploaded_file', tags: { user_id: user.id, bucket: bucket.name }, sporadic: true 178 | ``` 179 | 180 | #### measure 181 | 182 | Use when you want to track an average value _per_-request. Examples: 183 | 184 | Librato.measure 'user.social_graph.nodes', 212 185 | 186 | # report from custom per-measurement tags 187 | Librato.measure 'jobs.queued', 3, tags: { priority: 'high', worker: 'worker.12' } 188 | 189 | #### timing 190 | 191 | Like `Librato.measure` this is per-request, but specialized for timing information: 192 | 193 | Librato.timing 'twitter.lookup.time', 21.2 194 | 195 | The block form auto-submits the time it took for its contents to execute as the measurement value: 196 | 197 | Librato.timing 'twitter.lookup.time' do 198 | @twitter = Twitter.lookup(user) 199 | end 200 | 201 | #### percentiles 202 | 203 | By defaults timings will send the average, sum, max and min for every minute. If you want to send percentiles as well you can specify them inline while instrumenting: 204 | 205 | ```ruby 206 | # track a single percentile 207 | Librato.timing 'api.request.time', time, percentile: 95 208 | 209 | # track multiple percentiles 210 | Librato.timing 'api.request.time', time, percentile: [95, 99] 211 | ``` 212 | 213 | You can also use percentiles with the block form of timings: 214 | 215 | ```ruby 216 | Librato.timing 'my.important.event', percentile: 95 do 217 | # do work 218 | end 219 | ``` 220 | 221 | #### group 222 | 223 | There is also a grouping helper, to make managing nested metrics easier. So this: 224 | 225 | Librato.measure 'memcached.gets', 20 226 | Librato.measure 'memcached.sets', 2 227 | Librato.measure 'memcached.hits', 18 228 | 229 | Can also be written as: 230 | 231 | Librato.group 'memcached' do |g| 232 | g.measure 'gets', 20 233 | g.measure 'sets', 2 234 | g.measure 'hits', 18 235 | end 236 | 237 | Symbols can be used interchangeably with strings for metric names. 238 | 239 | ## Use with Background Workers / Cron Jobs 240 | 241 | `librato-rack` is designed to run within a long-running process and report periodically. Intermittently running rake tasks and most background job tools (delayed job, resque, queue_classic) don't run long enough for this to work. 242 | 243 | Never fear, [we have some guidelines](https://github.com/librato/librato-rails/wiki/Monitoring-Background-Workers) for how to instrument your workers properly. 244 | 245 | If you are using `librato-rack` with sidekiq, [see these notes about setup](https://github.com/librato/librato-rails/wiki/Monitoring-Background-Workers#monitoring-long-running-threaded-workers-sidekiq-etc). 246 | 247 | ## Cross-Process Aggregation 248 | 249 | `librato-rack` submits measurements back to the Librato platform on a _per-process_ basis. By default these measurements are then combined into a single measurement per default tags (detects `host`) before persisting the data. 250 | 251 | For example if you have 4 hosts with 8 unicorn instances each (i.e. 32 processes total), on the Metrics site you'll find 4 data streams (1 per host) instead of 32. 252 | Current pricing applies after aggregation, so in this case you will be charged for 4 streams instead of 32. 253 | 254 | ## Troubleshooting 255 | 256 | Note that it may take 2-3 minutes for the first results to show up in your Metrics account after you have started your servers with `librato-rack` enabled and the first request has been received. 257 | 258 | For more information about startup and submissions to the Metrics service you can set your `log_level` to `debug`. If you are having an issue with a specific metric, using `trace` will add the exact measurements being sent to your logs along with other details about `librato-rack` execution. Neither of these modes are recommended long-term in production as they will add significant volume to your log file and may slow operation somewhat. 259 | 260 | Submission times are total time but submission I/O is non-blocking - your process will continue to handle requests during submissions. 261 | 262 | If you are debugging setup locally you can set `flush_interval` to something shorter (say 10s) to force submission more frequently. Don't change your `flush_interval` in production as it will not result in measurements showing up more quickly, but may affect performance. 263 | 264 | ## Contribution 265 | 266 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. 267 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. 268 | * Fork the project and submit a pull request from a feature or bugfix branch. 269 | * Please include tests. This is important so we don't break your changes unintentionally in a future version. 270 | * Please don't modify the gemspec, Rakefile, version, or changelog. If you do change these files, please isolate a separate commit so we can cherry-pick around it. 271 | 272 | ## Copyright 273 | 274 | Copyright (c) 2013-2017 [Librato Inc.](http://librato.com) See LICENSE for details. 275 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | # Packaging tasks 9 | Bundler::GemHelper.install_tasks 10 | 11 | # Gem signing 12 | task 'before_build' do 13 | signing_key = File.expand_path("~/.gem/librato-private_key.pem") 14 | if signing_key 15 | puts "Key found: signing gem..." 16 | ENV['GEM_SIGNING_KEY'] = signing_key 17 | else 18 | puts "WARN: signing key not found, gem not signed" 19 | end 20 | end 21 | task :build => :before_build 22 | 23 | # Console 24 | desc "Open an console session preloaded with this library" 25 | task :console do 26 | sh "pry -r ./lib/librato-rack.rb" 27 | end 28 | 29 | # Testing 30 | require 'rake/testtask' 31 | Rake::TestTask.new(:test) do |t| 32 | t.libs << 'lib' 33 | t.libs << 'test' 34 | t.pattern = 'test/**/*_test.rb' 35 | t.verbose = false 36 | end 37 | 38 | task :default => :test 39 | -------------------------------------------------------------------------------- /benchmarks/block_vs_interpolation.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'benchmark/ips' 3 | 4 | def log_block(&block); nil; end 5 | def log(string); nil; end 6 | 7 | first, second, third = %w{foo bar baz} 8 | 9 | Benchmark.ips do |x| 10 | x.report('interpolation no var') do 11 | log "so simple" 12 | end 13 | 14 | x.report('interpolation 1 var') do 15 | log "my #{first} var" 16 | end 17 | 18 | x.report('interpolation 2 var') do 19 | log "my #{first} var is #{second}" 20 | end 21 | 22 | x.report('interpolation 3 var') do 23 | log "my #{first} var is #{second} and #{third}" 24 | end 25 | 26 | x.report('block no var') do 27 | log_block { "so simple" } 28 | end 29 | 30 | x.report('block 1 var') do 31 | log_block { "my #{first} var" } 32 | end 33 | 34 | x.report('block 2 var') do 35 | log_block { "my #{first} var is #{second}" } 36 | end 37 | 38 | x.report('block 3 var') do 39 | log_block { "my #{first} var is #{second} and #{third}" } 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /benchmarks/increment_counts.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'benchmark/ips' 3 | require 'librato/rails' 4 | 5 | Benchmark.ips do |x| 6 | x.report('simple increment') do 7 | Librato::Rails.increment :foo 8 | end 9 | 10 | x.report('multiple increments') do 11 | z = rand(1000) 12 | Librato::Rails.increment z.to_s 13 | end 14 | end -------------------------------------------------------------------------------- /benchmarks/inline_vs_return.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'benchmark/ips' 3 | 4 | class Demo 5 | def initialize 6 | @skip = true 7 | end 8 | 9 | def do_foo 10 | return if @skip 11 | sleep 1 12 | end 13 | 14 | def always_call 15 | do_foo 16 | end 17 | 18 | def call_if 19 | do_foo if !@skip 20 | end 21 | 22 | def call_unless 23 | do_foo unless @skip 24 | end 25 | end 26 | 27 | demo = Demo.new 28 | 29 | Benchmark.ips do |x| 30 | x.report('always_call') do 31 | demo.always_call 32 | end 33 | 34 | x.report('call_if') do 35 | demo.call_if 36 | end 37 | 38 | x.report('call_unless') do 39 | demo.call_unless 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /benchmarks/measures_timings.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'benchmark/ips' 3 | require 'librato/rails' 4 | 5 | Benchmark.ips do |x| 6 | x.report('simple measure') do 7 | Librato::Rails.measure :foo, 23.2 8 | end 9 | 10 | x.report('simple random') do 11 | Librato::Rails.measure :foo, rand(1000) 12 | end 13 | 14 | x.report('multiple measures') do 15 | z = rand(1000) 16 | Librato::Rails.measure z.to_s, 100.3 17 | end 18 | 19 | x.report('multiple random') do 20 | z = rand(1000) 21 | Librato::Rails.measure z.to_s, rand(1000) 22 | end 23 | 24 | x.report('simple timing') do 25 | Librato::Rails.timing :bar do 26 | 10.2 27 | end 28 | end 29 | 30 | x.report('multiple timing') do 31 | z = rand(1000) 32 | Librato::Rails.timing z.to_s do 33 | 200.1 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /benchmarks/numeric_detection_float.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'benchmark/ips' 3 | 4 | float = '12.2312' 5 | bad_float = '22.to.2' 6 | 7 | Benchmark.ips do |x| 8 | x.report('cast') do 9 | Float(float) rescue false 10 | end 11 | 12 | x.report('cast fail') do 13 | Float(bad_float) rescue false 14 | end 15 | 16 | x.report('to_s') do 17 | float.to_f.to_s == float 18 | end 19 | 20 | x.report('to_s fail') do 21 | bad_float.to_f.to_s == bad_float 22 | end 23 | 24 | x.report('regexp') do 25 | float =~ /^[-+]?[0-9]*\.?[0-9]+$/ 26 | end 27 | 28 | x.report('regexp fail') do 29 | bad_float =~ /^[-+]?[0-9]*\.?[0-9]+$/ 30 | end 31 | end 32 | 33 | # 1.9.3-p448 34 | # 35 | # Calculating ------------------------------------- 36 | # cast 47430 i/100ms 37 | # cast fail 5023 i/100ms 38 | # to_s 27435 i/100ms 39 | # to_s fail 29609 i/100ms 40 | # regexp 37620 i/100ms 41 | # regexp fail 32557 i/100ms 42 | # ------------------------------------------------- 43 | # cast 2283762.5 (±6.8%) i/s - 11383200 in 5.012934s 44 | # cast fail 63108.8 (±6.7%) i/s - 316449 in 5.038518s 45 | # to_s 593069.3 (±8.8%) i/s - 2962980 in 5.042459s 46 | # to_s fail 857217.1 (±10.0%) i/s - 4263696 in 5.033024s 47 | # regexp 1383194.8 (±6.7%) i/s - 6884460 in 5.008275s 48 | # regexp fail 723390.2 (±5.8%) i/s - 3613827 in 5.016494s -------------------------------------------------------------------------------- /benchmarks/numeric_detection_int.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'benchmark/ips' 3 | 4 | int = '220000' 5 | bad_int = '22.to.2' 6 | 7 | Benchmark.ips do |x| 8 | x.report('cast') do 9 | Integer(int) rescue false 10 | end 11 | 12 | x.report('cast fail') do 13 | Integer(bad_int) rescue false 14 | end 15 | 16 | x.report('to_s') do 17 | int.to_i.to_s == int 18 | end 19 | 20 | x.report('to_s fail') do 21 | bad_int.to_i.to_s == bad_int 22 | end 23 | 24 | x.report('regexp') do 25 | int =~ /^\d+$/ 26 | end 27 | 28 | x.report('regexp fail') do 29 | bad_int =~ /^\d+$/ 30 | end 31 | end 32 | 33 | # 1.9.3-p448 34 | # 35 | # Calculating ------------------------------------- 36 | # cast 57485 i/100ms 37 | # cast fail 5549 i/100ms 38 | # to_s 47509 i/100ms 39 | # to_s fail 50573 i/100ms 40 | # regexp 45187 i/100ms 41 | # regexp fail 42566 i/100ms 42 | # ------------------------------------------------- 43 | # cast 2353703.4 (±4.9%) i/s - 11726940 in 4.998270s 44 | # cast fail 65590.2 (±4.6%) i/s - 327391 in 5.003511s 45 | # to_s 1420892.0 (±6.8%) i/s - 7078841 in 5.011462s 46 | # to_s fail 1717948.8 (±6.0%) i/s - 8546837 in 4.998672s 47 | # regexp 1525729.9 (±7.0%) i/s - 7591416 in 5.007105s 48 | # regexp fail 1154461.1 (±5.5%) i/s - 5788976 in 5.035311s -------------------------------------------------------------------------------- /benchmarks/worker_check_vs_forking_check.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'benchmark/ips' 3 | require 'librato/rails' 4 | 5 | module Librato::Rails 6 | @pid == $$ 7 | end 8 | 9 | Benchmark.ips do |x| 10 | x.report('worker check') do 11 | Librato::Rails.check_worker 12 | end 13 | 14 | x.report('forking server check') do 15 | Librato::Rails.send(:forking_server?) 16 | end 17 | end -------------------------------------------------------------------------------- /certs/librato-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDLjCCAhagAwIBAgIBADANBgkqhkiG9w0BAQUFADA9MQ0wCwYDVQQDDARydWJ5 3 | MRcwFQYKCZImiZPyLGQBGRYHbGlicmF0bzETMBEGCgmSJomT8ixkARkWA2NvbTAe 4 | Fw0xNzAxMTExODI3MDdaFw0xODAxMTExODI3MDdaMD0xDTALBgNVBAMMBHJ1Ynkx 5 | FzAVBgoJkiaJk/IsZAEZFgdsaWJyYXRvMRMwEQYKCZImiZPyLGQBGRYDY29tMIIB 6 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA58LirwsWnKL1uuClQ0uwA1XL 7 | GpxDuFzSosipiPkzZY3hiHazC8SHDREZQXlm7ITX/o+dueNoB7dt0BR3RPVipH7V 8 | 7cvbCUaZNjEXR5Lal6PsmUsbMTrddkvj3e7flmJv+kMj+teeJ7MDeJTU5wXXV9pD 9 | ThiCDILJMF5CdP8Jru4rSBOP6RmmzYU+0cN5+5pt9xqrycA+Poo2ZuoUMCMsSBvl 10 | PimM3PPvoWTuE41GTn/bLoOVoXQmdwZIbwUSVH8rCDjZSlttOst+xrBw4KG0dYUp 11 | 2UvEe8iCyqEMQ8fEZ7EXuP2WMVICutFbz8Pt4hIMq+OTnDX+mIfma7GvPaKAFwID 12 | AQABozkwNzAJBgNVHRMEAjAAMB0GA1UdDgQWBBQBSxu9Jj4VTrXTpLujPwk9Kzwp 13 | 2jALBgNVHQ8EBAMCBLAwDQYJKoZIhvcNAQEFBQADggEBACvNsw1pGv72xp3LiDlZ 14 | 0tphNP/85RcYyJMklslG3tIYblyo71xHW1UbK5ArUK6k0BN43MuDn3pqLJQttVmi 15 | bUdA3yYi13GeSrrAMlr4nH8Yt/Bn/XpZGliouJUBwh1BjG6dSj1iuR4/9pt9/LtO 16 | QTdIc+07qGspypT0Uh/w/BodEcGuAaZZFlkU9vottTe6wWNnM6hfRExiSIsr+oVe 17 | s8s83ObshjuSzjOqS56IBtNlPEL+D6ghjZZLP3lS6l9p70Pcpcl+IcE4veqZmmKC 18 | sGepgRclC6UbZh+yQ3alXVghM2qsonAwI/rTNmFJN9kQn6nP9+f1Uf/qZFNcjn4L 19 | 9bg= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /lib/librato-rack.rb: -------------------------------------------------------------------------------- 1 | require 'librato/rack' -------------------------------------------------------------------------------- /lib/librato/collector.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Librato 4 | # collects and stores measurement values over time so they can be 5 | # reported periodically to the Metrics service 6 | # 7 | class Collector 8 | extend Forwardable 9 | 10 | def_delegators :counters, :increment 11 | def_delegators :aggregate, :measure, :timing 12 | 13 | attr_reader :tags 14 | 15 | def initialize(options={}) 16 | @tags = options[:tags] 17 | end 18 | 19 | # access to internal aggregator object 20 | def aggregate 21 | @aggregator_cache ||= Aggregator.new(prefix: @prefix, default_tags: @tags) 22 | end 23 | 24 | # access to internal counters object 25 | def counters 26 | @counter_cache ||= CounterCache.new(default_tags: @tags) 27 | end 28 | 29 | # remove any accumulated but unsent metrics 30 | def delete_all 31 | aggregate.delete_all 32 | counters.delete_all 33 | end 34 | alias :clear :delete_all 35 | 36 | def group(prefix) 37 | group = Group.new(self, prefix) 38 | yield group 39 | end 40 | 41 | # update prefix 42 | def prefix=(new_prefix) 43 | @prefix = new_prefix 44 | aggregate.prefix = @prefix 45 | end 46 | 47 | def prefix 48 | @prefix 49 | end 50 | 51 | end 52 | end 53 | 54 | require_relative 'collector/aggregator' 55 | require_relative 'collector/counter_cache' 56 | require_relative 'collector/exceptions' 57 | require_relative 'collector/group' 58 | -------------------------------------------------------------------------------- /lib/librato/collector/aggregator.rb: -------------------------------------------------------------------------------- 1 | require 'hetchy' 2 | 3 | module Librato 4 | class Collector 5 | # maintains storage of timing and measurement type measurements 6 | # 7 | class Aggregator 8 | SEPARATOR = "$$" 9 | 10 | extend Forwardable 11 | 12 | def_delegators :@cache, :empty?, :prefix, :prefix= 13 | 14 | attr_reader :default_tags 15 | 16 | def initialize(options={}) 17 | @cache = Librato::Metrics::Aggregator.new(prefix: options[:prefix]) 18 | @percentiles = {} 19 | @lock = Mutex.new 20 | @default_tags = options.fetch(:default_tags, {}) 21 | end 22 | 23 | def [](key) 24 | fetch(key) 25 | end 26 | 27 | # retrieve current value of a metric/source/percentage. this exists 28 | # primarily for debugging/testing and isn't called routinely. 29 | def fetch(key, options={}) 30 | return nil if @cache.empty? 31 | return fetch_percentile(key, options) if options[:percentile] 32 | measurements = nil 33 | tags = options[:tags] || @default_tags 34 | @lock.synchronize { measurements = @cache.queued[:measurements] } 35 | measurements.each do |metric| 36 | if metric[:name] == key.to_s 37 | return metric if !tags && !metric[:tags] 38 | return metric if tags == metric[:tags] 39 | end 40 | end 41 | nil 42 | end 43 | 44 | # clear all stored values 45 | def delete_all 46 | @lock.synchronize { clear_storage } 47 | end 48 | 49 | # transfer all measurements to queue and reset internal status 50 | def flush_to(queue, opts={}) 51 | queued = nil 52 | @lock.synchronize do 53 | return if @cache.empty? 54 | queued = @cache.queued 55 | flush_percentiles(queue, opts) unless @percentiles.empty? 56 | clear_storage unless opts[:preserve] 57 | end 58 | queue.merge!(queued) if queued 59 | end 60 | 61 | # @example Simple measurement 62 | # measure 'sources_returned', sources.length 63 | # 64 | # @example Simple timing in milliseconds 65 | # timing 'twitter.lookup', 2.31 66 | # 67 | # @example Block-based timing 68 | # timing 'db.query' do 69 | # do_my_query 70 | # end 71 | # 72 | # @example Custom source 73 | # measure 'user.all_orders', user.order_count, :source => user.id 74 | # 75 | def measure(*args, &block) 76 | options = {} 77 | event = args[0].to_s 78 | returned = nil 79 | 80 | # handle block or specified argument 81 | if block_given? 82 | start = Time.now 83 | returned = yield 84 | value = ((Time.now - start) * 1000.0).to_i 85 | elsif args[1] 86 | value = args[1] 87 | else 88 | raise "no value provided" 89 | end 90 | 91 | # detect options hash if present 92 | if args.length > 1 and args[-1].respond_to?(:each) 93 | options = args[-1] 94 | end 95 | 96 | percentiles = Array(options[:percentile]) 97 | source = options[:source] 98 | tags_option = options[:tags] 99 | tags_option = { source: source } if source && !tags_option 100 | tags = 101 | if tags_option && options[:inherit_tags] 102 | @default_tags.merge(tags_option) 103 | elsif tags_option 104 | tags_option 105 | else 106 | @default_tags 107 | end 108 | 109 | @lock.synchronize do 110 | payload = { value: value } 111 | payload.merge!({ tags: tags }) if tags 112 | @cache.add event => payload 113 | 114 | percentiles.each do |perc| 115 | store = fetch_percentile_store(event, payload) 116 | store[:reservoir] << value 117 | track_percentile(store, perc) 118 | end 119 | end 120 | returned 121 | end 122 | alias :timing :measure 123 | 124 | private 125 | 126 | def clear_storage 127 | @cache.clear 128 | @percentiles.each do |key, val| 129 | val[:reservoir].clear 130 | val[:percs].clear 131 | end 132 | end 133 | 134 | def fetch_percentile(key, options) 135 | store = fetch_percentile_store(key, options) 136 | return nil unless store 137 | store[:reservoir].percentile(options[:percentile]) 138 | end 139 | 140 | def fetch_percentile_store(event, options) 141 | keyname = event 142 | 143 | if options[:tags] 144 | keyname = Librato::Metrics::Util.build_key_for(keyname, options[:tags]) 145 | end 146 | 147 | @percentiles[keyname] ||= { 148 | name: event, 149 | reservoir: Hetchy::Reservoir.new(size: 1000), 150 | percs: Set.new 151 | } 152 | @percentiles[keyname].merge!({ tags: options[:tags] }) if options && options[:tags] 153 | @percentiles[keyname] 154 | end 155 | 156 | def flush_percentiles(queue, opts) 157 | @percentiles.each do |key, val| 158 | val[:percs].each do |perc| 159 | perc_name = perc.to_s[0,5].gsub('.','') 160 | payload = 161 | if val[:tags] 162 | { value: val[:reservoir].percentile(perc), tags: val[:tags] } 163 | else 164 | val[:reservoir].percentile(perc) 165 | end 166 | queue.add "#{val[:name]}.p#{perc_name}" => payload 167 | end 168 | end 169 | end 170 | 171 | def track_percentile(store, perc) 172 | if perc < 0.0 || perc > 100.0 173 | raise InvalidPercentile, "Percentiles must be between 0.0 and 100.0" 174 | end 175 | store[:percs].add(perc) 176 | end 177 | 178 | end 179 | 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /lib/librato/collector/counter_cache.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module Librato 4 | class Collector 5 | # maintains storage of a set of incrementable, counter-like 6 | # measurements 7 | # 8 | class CounterCache 9 | SEPARATOR = '%%' 10 | INTEGER_CLASS = 1.class 11 | 12 | extend Forwardable 13 | 14 | def_delegators :@cache, :empty? 15 | 16 | attr_reader :default_tags 17 | 18 | def initialize(options={}) 19 | @cache = {} 20 | @lock = Mutex.new 21 | @sporadics = Set.new 22 | @default_tags = options.fetch(:default_tags, {}) 23 | end 24 | 25 | # Retrieve the current value for a given metric. This is a short 26 | # form for convenience which only retrieves metrics with no custom 27 | # source specified. For more options see #fetch. 28 | # 29 | # @param [String|Symbol] key metric name 30 | # @return [Integer|Float] current value 31 | def [](key) 32 | fetch(key) 33 | end 34 | 35 | # removes all tracked metrics. note this removes all measurement 36 | # data AND metric names any continuously tracked metrics will not 37 | # report until they get another measurement 38 | def delete_all 39 | @lock.synchronize { @cache.clear } 40 | end 41 | 42 | def fetch(key, options={}) 43 | key = key.to_s 44 | key = 45 | if options[:tags] 46 | Librato::Metrics::Util.build_key_for(key, options[:tags]) 47 | elsif @default_tags 48 | Librato::Metrics::Util.build_key_for(key, @default_tags) 49 | end 50 | @lock.synchronize { @cache[key] } 51 | end 52 | 53 | # transfer all measurements to queue and reset internal status 54 | def flush_to(queue, opts={}) 55 | counts = nil 56 | @lock.synchronize do 57 | # work off of a duplicate data set so we block for 58 | # as little time as possible 59 | # requires a deep copy of data set 60 | counts = JSON.parse(@cache.dup.to_json, symbolize_names: true) 61 | reset_cache unless opts[:preserve] 62 | end 63 | counts.each do |metric, payload| 64 | metric = metric.to_s.split(SEPARATOR).first 65 | queue.add metric => payload 66 | end 67 | end 68 | 69 | # Increment a given metric 70 | # 71 | # @example Increment metric 'foo' by 1 72 | # increment :foo 73 | # 74 | # @example Increment metric 'bar' by 2 75 | # increment :bar, :by => 2 76 | # 77 | # @example Increment metric 'foo' by 1 with a custom source 78 | # increment :foo, :source => user.id 79 | # 80 | def increment(counter, options={}) 81 | metric = counter.to_s 82 | if options.is_a?(INTEGER_CLASS) 83 | # suppport legacy style 84 | options = {by: options} 85 | end 86 | by = options[:by] || 1 87 | source = options[:source] 88 | tags_option = options[:tags] 89 | tags_option = { source: source } if source && !tags_option 90 | tags = 91 | if tags_option && options[:inherit_tags] 92 | @default_tags.merge(tags_option) 93 | elsif tags_option 94 | tags_option 95 | else 96 | @default_tags 97 | end 98 | metric = Librato::Metrics::Util.build_key_for(metric, tags) if tags 99 | if options[:sporadic] 100 | make_sporadic(metric) 101 | end 102 | @lock.synchronize do 103 | @cache[metric] = {} unless @cache[metric] 104 | @cache[metric][:name] ||= metric 105 | @cache[metric][:value] ||= 0 106 | @cache[metric][:value] += by 107 | @cache[metric][:tags] = tags if tags 108 | end 109 | end 110 | 111 | private 112 | 113 | def make_sporadic(metric) 114 | @sporadics << metric 115 | end 116 | 117 | def reset_cache 118 | # remove any source/metric pairs that aren't continuous 119 | @sporadics.each { |metric| @cache.delete(metric) } 120 | @sporadics.clear 121 | # reset all continuous source/metric pairs to 0 122 | @cache.each_key { |key| @cache[key][:value] = 0 } 123 | end 124 | 125 | end 126 | 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/librato/collector/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Librato 2 | class Collector 3 | 4 | # custom exceptions 5 | class InvalidPercentile < StandardError; end 6 | 7 | end 8 | end -------------------------------------------------------------------------------- /lib/librato/collector/group.rb: -------------------------------------------------------------------------------- 1 | module Librato 2 | class Collector 3 | # abstracts grouping together several similarly named measurements 4 | # 5 | class Group 6 | 7 | def initialize(collector, prefix) 8 | @collector, @prefix = collector, "#{prefix}." 9 | end 10 | 11 | def group(prefix) 12 | prefix = "#{@prefix}#{prefix}" 13 | yield self.class.new(@collector, prefix) 14 | end 15 | 16 | def increment(counter, options={}) 17 | counter = "#{@prefix}#{counter}" 18 | @collector.increment counter, options 19 | end 20 | 21 | def measure(*args, &block) 22 | args[0] = "#{@prefix}#{args[0]}" 23 | @collector.measure(*args, &block) 24 | end 25 | alias :timing :measure 26 | 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /lib/librato/rack.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'librato/metrics' 3 | 4 | module Librato 5 | extend SingleForwardable 6 | def_delegators :tracker, :increment, :measure, :timing, :group 7 | 8 | def self.register_tracker(tracker) 9 | @tracker = tracker 10 | end 11 | 12 | def self.tracker 13 | @tracker ||= Librato::Rack::Tracker.new(Librato::Rack::Configuration.new) 14 | end 15 | end 16 | 17 | module Librato 18 | # Middleware for rack applications. Installs tracking hearbeat for 19 | # metric submission and tracks performance metrics. 20 | # 21 | # @example A basic rack app 22 | # require 'rack' 23 | # require 'librato-rack' 24 | # 25 | # app = Rack::Builder.app do 26 | # use Librato::Rack 27 | # run lambda { |env| [200, {"Content-Type" => 'text/html'}, ["Hello!"]] } 28 | # end 29 | # 30 | # @example Using a custom config object 31 | # config = Librato::Rack::Configuration.new 32 | # config.user = 'myuser@mysite.com' 33 | # config.token = 'mytoken' 34 | # …more configuration 35 | # 36 | # use Librato::Rack, :config => config 37 | # run MyApp 38 | # 39 | class Rack 40 | RECORD_RACK_BODY = <<-'EOS' 41 | group.increment 'total' 42 | group.timing 'time', duration, percentile: 95 43 | group.increment 'slow' if duration > 200.0 44 | EOS 45 | 46 | RECORD_RACK_STATUS_BODY = <<-'EOS' 47 | status_tags = { status: status } 48 | tracker.increment "rack.request.status", tags: status_tags, inherit_tags: true 49 | tracker.timing "rack.request.status.time", duration, tags: status_tags, inherit_tags: true 50 | EOS 51 | 52 | RECORD_RACK_METHOD_BODY = <<-'EOS' 53 | method_tags = { method: http_method.downcase } 54 | tracker.increment "rack.request.method", tags: method_tags, inherit_tags: true 55 | tracker.timing "rack.request.method.time", duration, tags: method_tags, inherit_tags: true 56 | EOS 57 | 58 | attr_reader :config, :tracker 59 | 60 | def initialize(app, options={}) 61 | @app = app 62 | @config = options.fetch(:config, Configuration.new) 63 | @tracker = @config.tracker || Tracker.new(@config) 64 | Librato.register_tracker(@tracker) # create global reference 65 | 66 | build_record_request_metrics_method 67 | build_record_header_metrics_method 68 | build_record_exception_method 69 | end 70 | 71 | def call(env) 72 | check_log_output(env) unless @log_target 73 | @tracker.check_worker 74 | record_header_metrics(env) 75 | response, duration = process_request(env) 76 | record_request_metrics(response.first, env["REQUEST_METHOD"], duration) 77 | response 78 | end 79 | 80 | private 81 | 82 | # this generally will only get called on the first request 83 | # it figures out the environment-appropriate logging outlet 84 | # and notifies config and tracker about it 85 | def check_log_output(env) 86 | return if @log_target 87 | if in_heroku_env? 88 | tracker.on_heroku = true 89 | default = ::Logger.new($stdout) 90 | else 91 | default = env['rack.errors'] || $stderr 92 | end 93 | @tracker.update_log_target(config.log_target ||= default) 94 | @log_target = config.log_target 95 | end 96 | 97 | def in_heroku_env? 98 | # don't have any custom http vars anymore, check if hostname is UUID 99 | Socket.gethostname =~ /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i 100 | end 101 | 102 | def process_request(env) 103 | time = Time.now 104 | begin 105 | response = @app.call(env) 106 | rescue Exception => e 107 | record_exception(e) 108 | raise 109 | end 110 | duration = (Time.now - time) * 1000.0 111 | [response, duration] 112 | end 113 | 114 | 115 | # Dynamically construct :record_request_metrics method based on 116 | # configured metric suites 117 | def build_record_request_metrics_method 118 | body = "def record_request_metrics(status, http_method, duration)\n" 119 | body << "return if config.disable_rack_metrics\n" 120 | 121 | unless config.instance_of?(Librato::Rack::Configuration::SuitesNone) 122 | body << "tracker.group 'rack.request' do |group|\n" 123 | 124 | if tracker.suite_enabled?(:rack) 125 | body << RECORD_RACK_BODY 126 | end 127 | 128 | if tracker.suite_enabled?(:rack_status) 129 | body << RECORD_RACK_STATUS_BODY 130 | end 131 | 132 | if tracker.suite_enabled?(:rack_method) 133 | body << RECORD_RACK_METHOD_BODY 134 | end 135 | 136 | body << "end\n" 137 | end 138 | 139 | body << "end\n" 140 | 141 | instance_eval(body) 142 | end 143 | 144 | # Dynamically construct :record_header_metrics method based on 145 | # configured metric suites 146 | def build_record_header_metrics_method 147 | if tracker.suite_enabled?(:rack) 148 | define_singleton_method(:record_header_metrics) do |env| 149 | queue_start = env['HTTP_X_REQUEST_START'] || env['HTTP_X_QUEUE_START'] 150 | if queue_start 151 | queue_start = queue_start.to_s.sub('t=', '').sub('.', '') 152 | case queue_start.length 153 | when 16 # microseconds 154 | wait = ((Time.now.to_f * 1000000).to_i - queue_start.to_i) / 1000.0 155 | wait = 0 if wait < 0 # make up for potential time drift between the routing server and the application server 156 | tracker.timing 'rack.request.queue.time', wait, percentile: 95 157 | when 13 # milliseconds 158 | wait = (Time.now.to_f * 1000).to_i - queue_start.to_i 159 | wait = 0 if wait < 0 # make up for potential time drift between the routing server and the application server 160 | tracker.timing 'rack.request.queue.time', wait, percentile: 95 161 | end 162 | end 163 | end 164 | else 165 | define_singleton_method(:record_header_metrics) do |env| 166 | # no-op 167 | end 168 | end 169 | end 170 | 171 | def build_record_exception_method 172 | if tracker.suite_enabled?(:rack) 173 | define_singleton_method(:record_exception) do |exception| 174 | return if config.disable_rack_metrics 175 | tracker.increment 'rack.request.exceptions' 176 | end 177 | else 178 | define_singleton_method(:record_exception) do |exception| 179 | # no-op 180 | end 181 | end 182 | end 183 | 184 | end 185 | end 186 | 187 | require 'librato/collector' 188 | require 'librato/rack/configuration' 189 | require 'librato/rack/errors' 190 | require 'librato/rack/logger' 191 | require 'librato/rack/tracker' 192 | require 'librato/rack/validating_queue' 193 | require 'librato/rack/version' 194 | require 'librato/rack/worker' 195 | -------------------------------------------------------------------------------- /lib/librato/rack/configuration.rb: -------------------------------------------------------------------------------- 1 | module Librato 2 | class Rack 3 | # Holds configuration for Librato::Rack middleware to use. 4 | # Acquires some settings by default from environment variables, 5 | # but this allows easy setting and overrides. 6 | # 7 | # @example 8 | # config = Librato::Rack::Configuration.new 9 | # config.user = 'mimo@librato.com' 10 | # config.token = 'mytoken' 11 | # 12 | class Configuration 13 | EVENT_MODES = [:eventmachine, :synchrony] 14 | 15 | DEFAULT_SUITES = [:rack, :rack_method, :rack_status] 16 | 17 | attr_accessor :api_endpoint, :autorun, :disable_rack_metrics, 18 | :flush_interval, :log_level, :log_prefix, 19 | :log_target, :proxy, :suites, 20 | :tags, :token, :tracker, :user 21 | attr_reader :deprecations, :prefix 22 | 23 | def initialize 24 | # set up defaults 25 | self.tracker = nil 26 | self.api_endpoint = Librato::Metrics.api_endpoint 27 | self.flush_interval = 60 28 | self.log_prefix = '[librato-rack] ' 29 | @listeners = [] 30 | @deprecations = [] 31 | 32 | load_configuration 33 | end 34 | 35 | def event_mode 36 | @event_mode 37 | end 38 | 39 | # set event_mode, valid options are EVENT_MODES or 40 | # nil (the default) if not running in an evented context 41 | def event_mode=(mode) 42 | mode = mode.to_sym if mode 43 | # reject unless acceptable mode, allow for turning event_mode off 44 | if [*EVENT_MODES, nil].include?(mode) 45 | @event_mode = mode 46 | else 47 | # TODO log warning 48 | end 49 | end 50 | 51 | def has_tags? 52 | @tags && !@tags.empty? 53 | end 54 | 55 | # check environment variables and capture current state 56 | # for configuration 57 | def load_configuration 58 | self.user = ENV['LIBRATO_USER'] 59 | self.token = ENV['LIBRATO_TOKEN'] 60 | self.autorun = detect_autorun 61 | self.prefix = ENV['LIBRATO_PREFIX'] 62 | self.tags = build_tags 63 | self.log_level = ENV['LIBRATO_LOG_LEVEL'] || :info 64 | self.proxy = ENV['LIBRATO_PROXY'] || ENV['https_proxy'] || ENV['http_proxy'] 65 | self.event_mode = ENV['LIBRATO_EVENT_MODE'] 66 | self.suites = ENV['LIBRATO_SUITES'] || '' 67 | check_deprecations 68 | end 69 | 70 | def prefix=(prefix) 71 | @prefix = prefix 72 | @listeners.each { |l| l.prefix = prefix } 73 | end 74 | 75 | def register_listener(listener) 76 | @listeners << listener 77 | end 78 | 79 | def dump 80 | fields = {} 81 | %w{flush_interval log_level prefix suites tags token user}.each do |field| 82 | fields[field.to_sym] = self.send(field) 83 | end 84 | fields[:metric_suites] = metric_suites.fields 85 | fields 86 | end 87 | 88 | def metric_suites 89 | @metric_suites ||= case suites.downcase.strip 90 | when 'all' 91 | SuitesAll.new 92 | when 'none' 93 | SuitesNone.new 94 | else 95 | Suites.new(suites, default_suites) 96 | end 97 | end 98 | 99 | private 100 | 101 | def default_suites 102 | DEFAULT_SUITES 103 | end 104 | 105 | def check_deprecations 106 | if self.disable_rack_metrics 107 | deprecate "disable_rack_metrics configuration option will be removed in a future release, please use config.suites = 'none' instead." 108 | end 109 | end 110 | 111 | def deprecate(message) 112 | @deprecations << message 113 | end 114 | 115 | def detect_autorun 116 | case ENV['LIBRATO_AUTORUN'] 117 | when '0', 'FALSE' 118 | false 119 | when '1', 'TRUE' 120 | true 121 | else 122 | nil 123 | end 124 | end 125 | 126 | def build_tags 127 | tags = {} 128 | tags.tap do 129 | if ENV["LIBRATO_TAGS"] 130 | ENV["LIBRATO_TAGS"].split(",") 131 | .map { |pairs| pairs.split("=") } 132 | .map { |k,v| tags[k.to_sym] = v } 133 | 134 | if tags.all? {|k,v| k.nil? || v.nil? } 135 | raise InvalidTagConfiguration, "Invalid tag configuration format. Example: foo=bar,baz=qux" 136 | end 137 | end 138 | end 139 | end 140 | 141 | end 142 | end 143 | end 144 | 145 | require_relative 'configuration/suites' 146 | -------------------------------------------------------------------------------- /lib/librato/rack/configuration/suites.rb: -------------------------------------------------------------------------------- 1 | module Librato 2 | class Rack 3 | class Configuration 4 | 5 | class Suites 6 | attr_reader :fields 7 | def initialize(value, defaults) 8 | @fields = if value.nil? || value.empty? 9 | defaults 10 | else 11 | resolve_suites(value, defaults) 12 | end 13 | end 14 | 15 | def include?(field) 16 | fields.include?(field) 17 | end 18 | 19 | private 20 | 21 | def resolve_suites(value, defaults) 22 | suites = value.to_s.split(/\s*,\s*/) 23 | adds = suites.select { |i| i.start_with?('+') }.map { |i| i[1..-1].to_sym } 24 | subs = suites.select { |i| i.start_with?('-') }.map { |i| i[1..-1].to_sym } 25 | 26 | if adds.any? || subs.any? 27 | 28 | # Did they try to mix adds/subs with explicit config 29 | if (adds.size + subs.size) != suites.size 30 | raise InvalidSuiteConfiguration, "Invalid suite value #{value}" 31 | end 32 | 33 | (defaults | adds) - subs 34 | else 35 | suites.map(&:to_sym) 36 | end 37 | end 38 | end 39 | 40 | class SuitesAll 41 | def fields; [:all]; end 42 | 43 | def include?(value) 44 | true 45 | end 46 | end 47 | 48 | class SuitesNone 49 | def fields; []; end 50 | 51 | def include?(value) 52 | false 53 | end 54 | end 55 | 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/librato/rack/errors.rb: -------------------------------------------------------------------------------- 1 | module Librato 2 | class Rack 3 | 4 | class InvalidLogLevel < RuntimeError; end 5 | 6 | class InvalidSuiteConfiguration < RuntimeError; end 7 | 8 | class InvalidTagConfiguration < RuntimeError; end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/librato/rack/logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Librato 4 | class Rack 5 | # Wraps an available logger object and provides convenience 6 | # methods for logging using a separate set of log levels 7 | # 8 | class Logger 9 | LOG_LEVELS = [:off, :error, :warn, :info, :debug, :trace] 10 | 11 | attr_accessor :prefix 12 | attr_reader :outlet 13 | 14 | def initialize(outlet=nil) 15 | @buffer = [] 16 | self.outlet = outlet 17 | self.prefix = '' 18 | end 19 | 20 | # @example Simple logging 21 | # log :debug, 'this is a debug message' 22 | # 23 | # @example Block logging - not executed if won't be logged 24 | # log(:debug) { "found #{thingy} at #{place}" } 25 | # 26 | def log(level, message=nil, &block) 27 | return unless should_log?(level) 28 | message = prefix + (message || block.call) 29 | if outlet.nil? 30 | buffer(level, message) 31 | else 32 | write_to_outlet(level, message) 33 | end 34 | end 35 | 36 | # set log level to any of LOG_LEVELS 37 | def log_level=(level) 38 | level = level.to_sym 39 | if LOG_LEVELS.index(level) 40 | @log_level = level 41 | require 'pp' if should_log?(:debug) 42 | else 43 | raise InvalidLogLevel, "Invalid log level '#{level}'" 44 | end 45 | end 46 | 47 | def log_level 48 | @log_level ||= :info 49 | end 50 | 51 | def outlet=(outlet) 52 | @outlet = outlet 53 | flush_buffer unless (outlet.nil? || @buffer.empty?) 54 | end 55 | 56 | private 57 | 58 | def buffer(level, message) 59 | @buffer << [level, message] 60 | end 61 | 62 | def flush_buffer 63 | @buffer.each { |buffered| write_to_outlet(*buffered) } 64 | end 65 | 66 | # write message to an ruby stdlib logger object or another class with 67 | # similar interface, respecting log levels when we can map them 68 | def log_to_logger(level, message) 69 | case level 70 | when :error, :warn 71 | method = level 72 | else 73 | method = :info 74 | end 75 | outlet.send(method, message) 76 | end 77 | 78 | def should_log?(level) 79 | LOG_LEVELS.index(self.log_level) >= LOG_LEVELS.index(level) 80 | end 81 | 82 | def write_to_outlet(level, message) 83 | if outlet.respond_to?(:puts) # io obj 84 | outlet.puts(message) 85 | elsif outlet.respond_to?(:error) # logger obj 86 | log_to_logger(level, message) 87 | else 88 | raise "invalid outlet: not a Logger or IO object" 89 | end 90 | end 91 | 92 | end 93 | end 94 | end -------------------------------------------------------------------------------- /lib/librato/rack/tracker.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module Librato 4 | class Rack 5 | class Tracker 6 | extend Forwardable 7 | 8 | def_delegators :collector, :increment, :measure, :timing, :group 9 | def_delegators :logger, :log 10 | 11 | attr_reader :config 12 | attr_accessor :on_heroku 13 | 14 | def initialize(config) 15 | @config = config 16 | collector.prefix = config.prefix 17 | config.register_listener(collector) 18 | end 19 | 20 | # check to see if we should start a worker for this process. 21 | # if you are using this externally, use #start! instead as this 22 | # method may change 23 | def check_worker 24 | return if @worker # already running 25 | return unless start_worker? 26 | log(:debug) { "config: #{config.dump}" } 27 | @pid = $$ 28 | log(:debug) { ">> starting up worker for pid #{@pid}..." } 29 | 30 | @worker = Worker.new(timer: config.event_mode) 31 | @worker.run_periodically(config.flush_interval) do 32 | flush 33 | end 34 | 35 | config.deprecations.each { |d| deprecate(d) } 36 | end 37 | 38 | # primary collector object used by this tracker 39 | def collector 40 | @collector ||= Librato::Collector.new(tags: tags) 41 | end 42 | 43 | # log a deprecation message 44 | def deprecate(message) 45 | log :warn, "DEPRECATION: #{message}" 46 | end 47 | 48 | # send all current data to Metrics 49 | def flush 50 | log :debug, "flushing pid #{@pid} (#{Time.now}).." 51 | start = Time.now 52 | # thread safety is handled internally for stores 53 | queue = build_flush_queue(collector) 54 | queue.submit unless queue.empty? 55 | log(:trace) { "flushed pid #{@pid} in #{(Time.now - start)*1000.to_f}ms" } 56 | rescue Exception => error 57 | log :error, "submission failed permanently: #{error}" 58 | end 59 | 60 | # current local instrumentation to be sent on next flush 61 | # this is for debugging, don't call rapidly in production as it 62 | # may introduce latency 63 | def queued 64 | build_flush_queue(collector, true).queued 65 | end 66 | 67 | # given current state, should the tracker start a reporter thread? 68 | def should_start? 69 | if !config.user || !config.token 70 | # don't show this unless we're debugging, expected behavior 71 | log :debug, 'halting: credentials not present.' 72 | elsif config.autorun == false 73 | log :debug, 'halting: LIBRATO_AUTORUN disabled startup' 74 | elsif tags.any? { |k,v| k.to_s !~ ValidatingQueue::TAGS_KEY_REGEX || v.to_s !~ ValidatingQueue::TAGS_VALUE_REGEX } 75 | log :warn, "halting: '#{tags}' are invalid tags." 76 | elsif tags.keys.length > ValidatingQueue::DEFAULT_TAGS_LIMIT 77 | log :warn, "halting: cannot exceed default tags limit of #{ValidatingQueue::DEFAULT_TAGS_LIMIT} tag names per measurement." 78 | elsif on_heroku && !config.has_tags? 79 | log :warn, 'halting: tags must be provided in configuration.' 80 | else 81 | return true 82 | end 83 | false 84 | end 85 | 86 | # start worker thread, one per process. 87 | # if this process has been forked from an one with an active 88 | # worker thread we don't need to worry about cleanup, the worker 89 | # thread will not pass with the fork 90 | def start! 91 | check_worker if should_start? 92 | end 93 | 94 | # change output stream for logging 95 | def update_log_target(target) 96 | logger.outlet = target 97 | end 98 | 99 | def suite_enabled?(suite) 100 | config.metric_suites.include?(suite.to_sym) 101 | end 102 | 103 | private 104 | 105 | # access to client instance 106 | def client 107 | @client ||= prepare_client 108 | end 109 | 110 | # use custom faraday adapter if running in evented context 111 | def custom_adapter 112 | case config.event_mode 113 | when :eventmachine 114 | :em_http 115 | when :synchrony 116 | :em_synchrony 117 | else 118 | nil 119 | end 120 | end 121 | 122 | def build_flush_queue(collector, preserve=false) 123 | queue = ValidatingQueue.new( client: client, 124 | prefix: config.prefix, skip_measurement_times: true ) 125 | [collector.counters, collector.aggregate].each do |cache| 126 | cache.flush_to(queue, preserve: preserve) 127 | end 128 | if suite_enabled?(:rack) 129 | queue.add 'rack.processes' => { value: 1, tags: tags } 130 | end 131 | trace_queued(queue.queued) #if should_log?(:trace) 132 | queue 133 | end 134 | 135 | # trace metrics being sent 136 | def trace_queued(queued) 137 | require 'pp' 138 | log(:trace) { "Queued: " + queued.pretty_inspect } 139 | end 140 | 141 | def logger 142 | return @logger if @logger 143 | @logger = Logger.new(config.log_target) 144 | @logger.log_level = config.log_level 145 | @logger.prefix = config.log_prefix 146 | @logger 147 | end 148 | 149 | def prepare_client 150 | client = Librato::Metrics::Client.new 151 | client.authenticate config.user, config.token 152 | client.api_endpoint = config.api_endpoint 153 | client.proxy = config.proxy 154 | client.custom_user_agent = user_agent 155 | if custom_adapter 156 | client.faraday_adapter = custom_adapter 157 | end 158 | client 159 | end 160 | 161 | def ruby_engine 162 | return RUBY_ENGINE if Object.constants.include?(:RUBY_ENGINE) 163 | RUBY_DESCRIPTION.split[0] 164 | end 165 | 166 | def tags 167 | @tags ||= config.has_tags? ? config.tags : { host: Socket.gethostname.downcase } 168 | end 169 | 170 | # should we spin up a worker? wrap this in a process check 171 | # so we only actually check once per process. this allows us 172 | # to check again if the process forks. 173 | def start_worker? 174 | if @pid_checked == $$ 175 | false 176 | else 177 | @pid_checked = $$ 178 | should_start? 179 | end 180 | end 181 | 182 | def user_agent 183 | ua_chunks = [version_string] 184 | ua_chunks << "(#{ruby_engine}; #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}; #{RUBY_PLATFORM})" 185 | ua_chunks.join(' ') 186 | end 187 | 188 | def version_string 189 | "librato-rack/#{Librato::Rack::VERSION}" 190 | end 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/librato/rack/validating_queue.rb: -------------------------------------------------------------------------------- 1 | module Librato 2 | class Rack 3 | # Queue with special upfront validating logic, this should 4 | # probably be available in librato-metrics but spiking here 5 | # to work out the kinks 6 | # 7 | class ValidatingQueue < Librato::Metrics::Queue 8 | DEFAULT_TAGS_LIMIT = 4 9 | METRIC_NAME_REGEX = /\A[-.:_\w]{1,255}\z/ 10 | TAGS_KEY_REGEX = /\A[-.:_\w]{1,64}\z/ 11 | TAGS_VALUE_REGEX = /\A[-.:_?\\\/\w ]{1,255}\z/ 12 | 13 | attr_accessor :logger 14 | 15 | def submit 16 | validate_measurements 17 | 18 | super 19 | end 20 | 21 | # screen all measurements for validity before sending 22 | def validate_measurements 23 | @queued[:measurements].delete_if do |entry| 24 | name = entry[:name].to_s 25 | tags = entry[:tags] 26 | if name !~ METRIC_NAME_REGEX 27 | log :warn, "invalid metric name '#{name}', not sending." 28 | true # delete 29 | elsif tags && tags.any? { |k,v| k.to_s !~ TAGS_KEY_REGEX || v.to_s !~ TAGS_VALUE_REGEX } 30 | log :warn, "halting: '#{tags}' are invalid tags." 31 | true # delete 32 | else 33 | false # preserve 34 | end 35 | end 36 | end 37 | 38 | private 39 | 40 | def log(level, msg) 41 | return unless logger 42 | logger.log level, msg 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/librato/rack/version.rb: -------------------------------------------------------------------------------- 1 | module Librato 2 | class Rack 3 | VERSION = "2.0.5" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/librato/rack/worker.rb: -------------------------------------------------------------------------------- 1 | module Librato 2 | class Rack 3 | # Runs a given piece of code periodically, ensuring that 4 | # it will be run again at the proper interval regardless 5 | # of how long execution takes. 6 | # 7 | class Worker 8 | attr_reader :timer 9 | 10 | # available options: 11 | # * timer - type of timer to use, valid options are 12 | # :sleep (default), :eventmachine, or :synchrony 13 | # * sync - try to synchronize timer executions to whole 14 | # minutes or subdivisions thereof 15 | def initialize(options={}) 16 | @interrupt = false 17 | @timer = (options[:timer] || :sleep).to_sym 18 | @sync = options[:sync] || false 19 | end 20 | 21 | # run the given block every seconds, looping 22 | # infinitely unless @interrupt becomes true. 23 | # 24 | def run_periodically(period, &block) 25 | @proc = block # store 26 | 27 | if [:eventmachine, :synchrony].include?(timer) 28 | compensated_repeat(period) # threading is already handled 29 | else 30 | @thread = Thread.new { compensated_repeat(period) } 31 | end 32 | end 33 | 34 | # Give some structure to worker start times so when possible 35 | # they will be in sync. 36 | # 37 | def start_time(period) 38 | if @sync 39 | earliest = Time.now + period 40 | # already on a whole minute 41 | return earliest if earliest.sec == 0 42 | if period > 30 43 | # bump to whole minute 44 | earliest + (60-earliest.sec) 45 | else 46 | # ensure sync to whole minute if minute is evenly divisible 47 | earliest + (period-(earliest.sec%period)) 48 | end 49 | else 50 | if period > 30 51 | # ensure some wobble in start times, 52 | # trade a slightly irregular first period for a more even 53 | # distribution for network requests between processes 54 | start = Time.now 55 | start + (60-start.sec) + rand(60) 56 | else 57 | Time.now + period 58 | end 59 | end 60 | end 61 | 62 | # stop worker loop at the beginning of the next round 63 | # of execution 64 | def stop! 65 | @interrupt = true 66 | end 67 | 68 | private 69 | 70 | # run continuous loop executing every , will start 71 | # at if set otherwise will auto-determine 72 | # appropriate time for first run 73 | def compensated_repeat(period, first_run = nil) 74 | next_run = first_run || start_time(period) 75 | until @interrupt do 76 | now = Time.now 77 | if now >= next_run 78 | @proc.call 79 | 80 | while next_run <= now 81 | next_run += period # schedule future run 82 | end 83 | end 84 | 85 | interval = next_run - now 86 | case timer 87 | when :eventmachine 88 | EM.add_timer(interval) { compensated_repeat(period, next_run) } 89 | break 90 | when :synchrony 91 | EM::Synchrony.sleep(interval) 92 | else 93 | sleep(next_run - now) 94 | end 95 | end 96 | end 97 | 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /librato-rack.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | require "librato/rack/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "librato-rack" 7 | s.version = Librato::Rack::VERSION 8 | 9 | s.authors = ["Matt Sanders"] 10 | s.email = ["matt@librato.com", "ruby@librato.com"] 11 | s.homepage = "https://github.com/librato/librato-rack" 12 | s.license = 'BSD-3-Clause' 13 | 14 | s.summary = "Use Librato Metrics with your rack application" 15 | s.description = "Rack middleware to report key app statistics and custom instrumentation to the Librato Metrics service." 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*"] + ["LICENSE", "Rakefile", "README.md", "CHANGELOG.md"] 18 | s.test_files = Dir["test/**/*"] 19 | 20 | s.add_dependency "librato-metrics", "~> 2.1", ">= 2.1.0" 21 | s.add_dependency "hetchy", "~> 1.0" 22 | s.add_development_dependency "minitest", "~> 5" 23 | 24 | s.cert_chain = ["certs/librato-public.pem"] 25 | if ENV['GEM_SIGNING_KEY'] 26 | s.signing_key = ENV['GEM_SIGNING_KEY'] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/apps/basic.ru: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'librato-rack' 3 | 4 | # Simulate the environment variables Heroku passes along 5 | # with each request 6 | # 7 | class QueueWait 8 | def initialize(app) 9 | @app = app 10 | end 11 | 12 | def call(env) 13 | env['HTTP_X_QUEUE_START'] = (Time.now.to_f * 1000).to_i.to_s 14 | @app.call(env) 15 | end 16 | end 17 | 18 | use QueueWait 19 | use Librato::Rack 20 | 21 | def application(env) 22 | case env['PATH_INFO'] 23 | when '/status/204' 24 | [204, {"Content-Type" => 'text/html'}, ["Status 204!"]] 25 | when '/exception' 26 | raise 'exception raised!' 27 | when '/slow' 28 | sleep 0.3 29 | [200, {"Content-Type" => 'text/html'}, ["Slow request"]] 30 | else 31 | [200, {"Content-Type" => 'text/html'}, ["Hello!"]] 32 | end 33 | end 34 | 35 | run method(:application) 36 | -------------------------------------------------------------------------------- /test/apps/custom.ru: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'librato-rack' 3 | 4 | use Librato::Rack 5 | 6 | def application(env) 7 | case env['PATH_INFO'] 8 | when '/tags' 9 | tags = { region: "us-east-1" } 10 | Librato.increment "requests", tags: tags 11 | Librato.timing "requests.time", 3, tags: tags 12 | when '/increment' 13 | Librato.increment :hits 14 | when '/measure' 15 | Librato.measure 'nodes', 3 16 | when '/timing' 17 | Librato.timing 'lookup.time', 2.3 18 | when '/timing_block' 19 | Librato.timing 'sleeper' do 20 | sleep 0.01 21 | end 22 | when '/group' 23 | Librato.group 'did.a' do |g| 24 | g.increment 'thing' 25 | g.timing 'timing', 2.3 26 | end 27 | end 28 | [200, {"Content-Type" => 'text/html'}, ["Hello!"]] 29 | end 30 | 31 | run method(:application) 32 | -------------------------------------------------------------------------------- /test/apps/custom_suites.ru: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'librato-rack' 3 | 4 | config = Librato::Rack::Configuration.new 5 | config.suites = '-rack_status,-rack_method' 6 | 7 | use Librato::Rack, :config => config 8 | 9 | def application(env) 10 | case env['PATH_INFO'] 11 | when '/exception' 12 | raise 'exception raised!' 13 | else 14 | [200, {"Content-Type" => 'text/html'}, ["Hello!"]] 15 | end 16 | end 17 | 18 | run method(:application) 19 | -------------------------------------------------------------------------------- /test/apps/no_stats.ru: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'librato-rack' 3 | 4 | config = Librato::Rack::Configuration.new 5 | config.disable_rack_metrics = true 6 | 7 | use Librato::Rack, :config => config 8 | 9 | def application(env) 10 | case env['PATH_INFO'] 11 | when '/exception' 12 | raise 'exception raised!' 13 | else 14 | [200, {"Content-Type" => 'text/html'}, ["Hello!"]] 15 | end 16 | end 17 | 18 | run method(:application) -------------------------------------------------------------------------------- /test/apps/queue_wait.ru: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'librato-rack' 3 | 4 | # Simulate the environment variables Heroku passes along 5 | # with each request 6 | # 7 | class QueueWait 8 | def initialize(app) 9 | @app = app 10 | end 11 | 12 | def call(env) 13 | case env['PATH_INFO'] 14 | when '/milli' 15 | env['HTTP_X_REQUEST_START'] = (Time.now.to_f * 1000).to_i.to_s 16 | sleep 0.005 17 | when '/micro' 18 | env['HTTP_X_REQUEST_START'] = (Time.now.to_f * 1000000).to_i.to_s 19 | sleep 0.01 20 | when '/queue_start' 21 | env['HTTP_X_QUEUE_START'] = (Time.now.to_f * 1000).to_i.to_s 22 | sleep 0.015 23 | when '/with_t' 24 | env['HTTP_X_REQUEST_START'] = "t=#{(Time.now.to_f * 1000000).to_i}".to_s 25 | sleep 0.02 26 | when '/with_period' 27 | env['HTTP_X_REQUEST_START'] = "%10.3f" % Time.now 28 | sleep 0.025 29 | when '/with_time_drift' 30 | env['HTTP_X_REQUEST_START'] = ((Time.now.to_f * 1000).to_i + 10).to_s # 10s in the future 31 | sleep 0.005 32 | end 33 | @app.call(env) 34 | end 35 | end 36 | 37 | use QueueWait 38 | use Librato::Rack 39 | 40 | def application(env) 41 | [200, {"Content-Type" => 'text/html'}, ["Hello!"]] 42 | end 43 | 44 | run method(:application) 45 | -------------------------------------------------------------------------------- /test/integration/custom_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rack/test' 3 | 4 | # Tests for universal tracking for all request paths 5 | # 6 | class CustomTest < Minitest::Test 7 | include Rack::Test::Methods 8 | include EnvironmentHelpers 9 | 10 | def app 11 | Rack::Builder.parse_file('test/apps/custom.ru').first 12 | end 13 | 14 | def setup 15 | ENV["LIBRATO_TAGS"] = "hostname=metrics-web-stg-1" 16 | @tags = { hostname: "metrics-web-stg-1" } 17 | end 18 | 19 | def teardown 20 | # clear metrics before each run 21 | aggregate.delete_all 22 | counters.delete_all 23 | clear_config_env_vars 24 | end 25 | 26 | def test_increment 27 | get '/increment' 28 | assert_equal 1, counters[:hits][:value] 29 | 2.times { get '/increment' } 30 | assert_equal 3, counters[:hits][:value] 31 | end 32 | 33 | def test_measure 34 | get '/measure' 35 | assert_equal 3.0, aggregate.fetch(:nodes, @tags)[:sum] 36 | assert_equal 1, aggregate.fetch(:nodes, @tags)[:count] 37 | end 38 | 39 | def test_timing 40 | get '/timing' 41 | assert_equal 1, aggregate.fetch("lookup.time", @tags)[:count] 42 | end 43 | 44 | def test_timing_block 45 | get '/timing_block' 46 | assert_equal 1, aggregate['sleeper'][:count] 47 | assert_in_delta 10, aggregate['sleeper'][:sum], 10 48 | end 49 | 50 | def test_grouping 51 | get '/group' 52 | assert_equal 1, counters['did.a.thing'][:value] 53 | assert_equal 1, aggregate['did.a.timing'][:count] 54 | end 55 | 56 | def test_tags 57 | tags = { region: "us-east-1" } 58 | get '/tags' 59 | assert_equal 1, counters.fetch("requests", tags: tags)[:value] 60 | assert_equal 1, aggregate.fetch("requests.time", tags: tags)[:count] 61 | end 62 | 63 | private 64 | 65 | def aggregate 66 | Librato.tracker.collector.aggregate 67 | end 68 | 69 | def counters 70 | Librato.tracker.collector.counters 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /test/integration/no_stats_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rack/test' 3 | 4 | # Tests to ensure config.disable_rack_metrics disables stats 5 | # 6 | class NoStatsTest < Minitest::Test 7 | include Rack::Test::Methods 8 | 9 | def app 10 | Rack::Builder.parse_file('test/apps/no_stats.ru').first 11 | end 12 | 13 | def teardown 14 | # clear metrics before each run 15 | aggregate.delete_all 16 | counters.delete_all 17 | end 18 | 19 | def test_no_standard_counters 20 | get '/' 21 | assert last_response.ok? 22 | 23 | assert_nil counters["rack.request"] 24 | assert_nil counters["rack.request.status"] 25 | end 26 | 27 | def test_no_standard_measures 28 | get '/' 29 | assert last_response.ok? 30 | 31 | assert_nil aggregate["rack.request.time"] 32 | end 33 | 34 | def test_dont_track_exceptions 35 | begin 36 | get '/exception' 37 | rescue RuntimeError => e 38 | raise unless e.message == 'exception raised!' 39 | end 40 | assert_nil counters["rack.request.exceptions"] 41 | end 42 | 43 | private 44 | 45 | def aggregate 46 | Librato.tracker.collector.aggregate 47 | end 48 | 49 | def counters 50 | Librato.tracker.collector.counters 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /test/integration/no_suites_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rack/test' 3 | 4 | # Tests to ensure tracking is disabled when a suite is removed. These tests 5 | # largely ensure that behavior verified positively other suites doesn't 6 | # happen when specific suites are disabled. 7 | # 8 | class NoSuitesTest < Minitest::Test 9 | include Rack::Test::Methods 10 | include EnvironmentHelpers 11 | 12 | def app 13 | ENV['LIBRATO_SUITES'] = 'none' 14 | Rack::Builder.parse_file('test/apps/basic.ru').first 15 | end 16 | 17 | def teardown 18 | # clear metrics before each run 19 | aggregate.delete_all 20 | counters.delete_all 21 | clear_config_env_vars 22 | end 23 | 24 | def test_increment_total 25 | get '/' 26 | assert last_response.ok? 27 | assert_nil counters["rack.request"], "should not increment" 28 | end 29 | 30 | def test_track_queue_time 31 | get '/' 32 | assert last_response.ok? 33 | assert_nil aggregate["rack.request.queue.time"] 34 | end 35 | 36 | def test_increment_status 37 | get '/' 38 | assert last_response.ok? 39 | assert_nil counters["rack.request.status"], "should not increment" 40 | end 41 | 42 | def test_track_http_method_info 43 | get '/' 44 | assert_nil counters["rack.request.method"] 45 | 46 | post '/' 47 | assert_nil counters["rack.request.method"] 48 | end 49 | 50 | def test_increment_exception 51 | begin 52 | get '/exception' 53 | rescue RuntimeError => e 54 | raise unless e.message == 'exception raised!' 55 | end 56 | 57 | assert_nil counters["rack.request.exceptions"], 'should not increment' 58 | end 59 | 60 | private 61 | 62 | def aggregate 63 | Librato.tracker.collector.aggregate 64 | end 65 | 66 | def counters 67 | Librato.tracker.collector.counters 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /test/integration/queue_wait_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rack/test' 3 | 4 | # Tests for universal tracking for all request paths 5 | # 6 | class QueueWaitTest < Minitest::Test 7 | include Rack::Test::Methods 8 | 9 | def app 10 | Rack::Builder.parse_file('test/apps/queue_wait.ru').first 11 | end 12 | 13 | def teardown 14 | # clear metrics before each run 15 | aggregate.delete_all 16 | counters.delete_all 17 | end 18 | 19 | def test_milliseconds 20 | get '/milli' 21 | 22 | # give jruby a bit more time since it can be slow 23 | delta = defined?(JRUBY_VERSION) ? 8 : 4 24 | 25 | # puts "milli: #{aggregate["rack.request.queue.time"].inspect}" 26 | assert_equal 1, aggregate["rack.request.queue.time"][:count], 27 | 'should track total queue time' 28 | assert_in_delta 5, aggregate["rack.request.queue.time"][:sum], delta 29 | end 30 | 31 | def test_microseconds 32 | get '/micro' 33 | 34 | # give jruby a bit more time since it can be slow 35 | delta = defined?(JRUBY_VERSION) ? 6 : 4 36 | 37 | # puts "micro: #{aggregate["rack.request.queue.time"].inspect}" 38 | assert_equal 1, aggregate["rack.request.queue.time"][:count], 39 | 'should track total queue time' 40 | assert_in_delta 10, aggregate["rack.request.queue.time"][:sum], delta 41 | end 42 | 43 | def test_queue_start 44 | get '/queue_start' 45 | 46 | # puts "micro: #{aggregate["rack.request.queue.time"].inspect}" 47 | assert_equal 1, aggregate["rack.request.queue.time"][:count], 48 | 'should track total queue time' 49 | assert_in_delta 15, aggregate["rack.request.queue.time"][:sum], 6 50 | end 51 | 52 | def test_with_t 53 | get '/with_t' 54 | 55 | # give jruby a bit more time since it can be slow 56 | delta = defined?(JRUBY_VERSION) ? 10 : 6 57 | 58 | # puts "micro: #{aggregate["rack.request.queue.time"].inspect}" 59 | assert_equal 1, aggregate["rack.request.queue.time"][:count], 60 | 'should track total queue time' 61 | assert_in_delta 20, aggregate["rack.request.queue.time"][:sum], delta 62 | end 63 | 64 | def test_with_period 65 | get '/with_period' 66 | 67 | # give jruby a bit more time since it can be slow 68 | delta = defined?(JRUBY_VERSION) ? 10 : 6 69 | assert_equal 1, aggregate["rack.request.queue.time"][:count], 70 | 'should track total queue time' 71 | assert_in_delta 25, aggregate["rack.request.queue.time"][:sum], delta 72 | end 73 | 74 | def test_with_time_drift 75 | get '/with_time_drift' 76 | 77 | # puts "milli: #{aggregate["rack.request.queue.time"].inspect}" 78 | assert_equal 1, aggregate["rack.request.queue.time"][:count], 79 | 'should track total queue time' 80 | assert_in_delta 0, aggregate["rack.request.queue.time"][:sum], 4 81 | end 82 | 83 | private 84 | 85 | def aggregate 86 | Librato.tracker.collector.aggregate 87 | end 88 | 89 | def counters 90 | Librato.tracker.collector.counters 91 | end 92 | 93 | end 94 | -------------------------------------------------------------------------------- /test/integration/request_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rack/test' 3 | 4 | # Tests for universal tracking for all request paths 5 | # 6 | class RequestTest < Minitest::Test 7 | include Rack::Test::Methods 8 | include EnvironmentHelpers 9 | 10 | def app 11 | Rack::Builder.parse_file('test/apps/basic.ru').first 12 | end 13 | 14 | def setup 15 | ENV["LIBRATO_TAGS"] = "hostname=metrics-web-stg-1" 16 | @tags = { hostname: "metrics-web-stg-1" } 17 | end 18 | 19 | def teardown 20 | # clear metrics before each run 21 | aggregate.delete_all 22 | counters.delete_all 23 | clear_config_env_vars 24 | end 25 | 26 | def test_increment_total_and_status 27 | get '/' 28 | assert last_response.ok? 29 | assert_equal 1, counters["rack.request.total"][:value] 30 | assert_equal 1, counters.fetch("rack.request.status", tags: @tags.merge({ status: 200 }))[:value] 31 | 32 | get '/status/204' 33 | assert_equal 2, counters["rack.request.total"][:value] 34 | assert_equal 1, counters.fetch("rack.request.status", tags: @tags.merge({ status: 200 }))[:value], "should not increment" 35 | assert_equal 1, counters.fetch("rack.request.status", tags: @tags.merge({ status: 204 }))[:value], "should increment" 36 | end 37 | 38 | def test_request_times 39 | get '/' 40 | 41 | # common for all paths 42 | assert_equal 1, aggregate["rack.request.time"][:count], 43 | 'should track total request time' 44 | 45 | # should calculate p95 value 46 | assert aggregate.fetch("rack.request.time", tags: @tags, percentile: 95) > 0.0 47 | 48 | # status specific 49 | assert_equal 1, aggregate.fetch("rack.request.status.time", tags: @tags.merge({ status: 200 }))[:count] 50 | end 51 | 52 | def test_track_http_method_info 53 | get '/' 54 | 55 | assert_equal 1, counters.fetch("rack.request.method", tags: @tags.merge({ method: "GET" }))[:value] 56 | assert_equal 1, aggregate.fetch("rack.request.method.time", tags: @tags.merge({ method: "get" }))[:count] 57 | 58 | post '/' 59 | 60 | assert_equal 1, counters.fetch("rack.request.method", tags: @tags.merge({ method: "POST" }))[:value] 61 | assert_equal 1, aggregate.fetch("rack.request.method.time", tags: @tags.merge({ method: "post" }))[:count] 62 | end 63 | 64 | def test_request_method_not_mutated 65 | get '/', {}, {'REQUEST_METHOD' => "GET".freeze} 66 | 67 | assert_equal 1, counters.fetch("rack.request.method", tags: @tags.merge({ method: "GET" }))[:value] 68 | assert_equal 1, aggregate.fetch("rack.request.method.time", tags: @tags.merge({ method: "get" }))[:count] 69 | end 70 | 71 | def test_track_exceptions 72 | begin 73 | get '/exception' 74 | rescue RuntimeError => e 75 | raise unless e.message == 'exception raised!' 76 | end 77 | assert_equal 1, counters["rack.request.exceptions"][:value] 78 | end 79 | 80 | def test_track_slow_requests 81 | get '/slow' 82 | assert_equal 1, counters["rack.request.slow"][:value] 83 | end 84 | 85 | private 86 | 87 | def aggregate 88 | Librato.tracker.collector.aggregate 89 | end 90 | 91 | def counters 92 | Librato.tracker.collector.counters 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /test/integration/suites_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rack/test' 3 | 4 | # Tests to ensure config suites work 5 | # 6 | class SuitesTest < Minitest::Test 7 | include Rack::Test::Methods 8 | 9 | def app 10 | Rack::Builder.parse_file('test/apps/custom_suites.ru').first 11 | end 12 | 13 | def teardown 14 | # clear metrics before each run 15 | aggregate.delete_all 16 | counters.delete_all 17 | end 18 | 19 | def test_no_rack_status 20 | get '/' 21 | assert last_response.ok? 22 | 23 | # rack.request metrics (rack suite) should get logged 24 | assert_equal 1, counters["rack.request.total"][:value] 25 | assert_equal 1, aggregate["rack.request.time"][:count] 26 | 27 | # rack.request.method metrics (rack_method suite) should not get logged 28 | assert_nil counters.fetch("rack.request.method", tags: { method: "GET" }) 29 | assert_nil aggregate.fetch("rack.request.method.time", tags: { method: "get" }) 30 | 31 | # rack.request.status metrics (rack_status suite) should not get logged 32 | assert_nil counters.fetch("rack.request.status", tags: { status: 200, status_message: "OK" }) 33 | assert_nil counters.fetch("rack.request.status.time", tags: { status: 200, status_message: "OK" }) 34 | end 35 | 36 | private 37 | 38 | def aggregate 39 | Librato.tracker.collector.aggregate 40 | end 41 | 42 | def counters 43 | Librato.tracker.collector.counters 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /test/remote/tracker_test.rb: -------------------------------------------------------------------------------- 1 | # # encoding: UTF-8 2 | require 'test_helper' 3 | require 'rack/test' 4 | 5 | # Tests for universal tracking for all request paths 6 | # 7 | class TrackerRemoteTest < Minitest::Test 8 | 9 | # These tests connect to the Metrics server with an account and verify remote 10 | # functions. They will only run if the below environment variables are set. 11 | # 12 | # BE CAREFUL, running these tests will DELETE ALL metrics currently in the 13 | # test account. 14 | # 15 | if ENV['LIBRATO_RACK_TEST_EMAIL'] && ENV['LIBRATO_RACK_TEST_API_KEY'] 16 | 17 | def setup 18 | config = Librato::Rack::Configuration.new 19 | config.user = ENV['LIBRATO_RACK_TEST_EMAIL'] 20 | config.token = ENV['LIBRATO_RACK_TEST_API_KEY'] 21 | if ENV['LIBRATO_RACK_TEST_API_ENDPOINT'] 22 | config.api_endpoint = ENV['LIBRATO_RACK_TEST_API_ENDPOINT'] 23 | end 24 | config.log_target = File.open('/dev/null', 'w') # ignore logs 25 | @tracker = Librato::Rack::Tracker.new(config) 26 | delete_all_metrics 27 | end 28 | 29 | def test_flush_counters 30 | tracker.increment :foo # simple 31 | tracker.increment :bar, 2 # specified 32 | tracker.increment :foo # multincrement 33 | tracker.increment :foo, tags: { hostname: "baz" }, by: 3 # custom source 34 | @queued = tracker.queued 35 | tracker.flush 36 | 37 | sleep 15 # TODO: retry logic for replica delay 38 | 39 | # metrics are SSA, so should exist but won't have measurements yet 40 | metric_names = client.metrics.map { |m| m['name'] } 41 | assert metric_names.include?('foo'), 'foo should be present' 42 | assert metric_names.include?('bar'), 'bar should be present' 43 | 44 | # interogate queued payload for expected values 45 | assert_equal [:host], @queued[:measurements].first[:tags].keys 46 | assert_equal 2, queued('foo') 47 | 48 | # custom source 49 | assert_equal 3, queued("foo", tags: { hostname: "baz" }) 50 | 51 | # different counter 52 | assert_equal 2, queued('bar') 53 | end 54 | 55 | def test_counter_persistent_through_flush 56 | tracker.increment 'knightrider' 57 | tracker.increment 'badguys', sporadic: true 58 | assert_equal 1, collector.counters['knightrider'][:value] 59 | assert_equal 1, collector.counters['badguys'][:value] 60 | 61 | tracker.flush 62 | assert_equal 0, collector.counters['knightrider'][:value] 63 | assert_nil collector.counters['badguys'] 64 | end 65 | 66 | def test_flush_should_send_measures_and_timings 67 | tracker.timing "request.time", 122.1 68 | tracker.measure 'items_bought', 20 69 | tracker.timing "request.time", 81.3 70 | tracker.timing "jobs.queued", 5, tags: { hostname: "worker.3" } 71 | @queued = tracker.queued 72 | tracker.flush 73 | 74 | sleep 15 # TODO: retry logic for replica delay 75 | 76 | # metrics are SSA, so should exist but won't have measurements yet 77 | metric_names = client.metrics.map { |m| m['name'] } 78 | assert metric_names.include?('request.time'), 'request.time should be present' 79 | assert metric_names.include?('items_bought'), 'request.time.db should be present' 80 | 81 | assert_equal 2, queued("request.time", tags: tags)[:count] 82 | assert_in_delta 203.4, queued("request.time", tags: tags)[:sum], 0.1 83 | 84 | assert_equal 1, queued("items_bought", tags: tags)[:count] 85 | assert_in_delta 20, queued("items_bought", tags: tags)[:sum], 0.1 86 | 87 | assert_equal 1, queued("jobs.queued", tags: { hostname: "worker.3" })[:count] 88 | assert_in_delta 5, queued("jobs.queued", tags: { hostname: "worker.3" })[:sum], 0.1 89 | end 90 | 91 | def test_flush_should_purge_measures_and_timings 92 | tracker.timing "request.time", 122.1 93 | tracker.measure 'items_bought', 20 94 | tracker.flush 95 | 96 | assert collector.aggregate.empty?, 97 | 'measures and timings should be cleared with flush' 98 | end 99 | 100 | def test_flush_respects_prefix 101 | tags = { hostname: "metrics-web-stg-1" } 102 | config.prefix = 'testyprefix' 103 | 104 | tracker.timing "mytime", 221.1, tags: tags 105 | tracker.increment 'mycount', 4 106 | @queued = tracker.queued 107 | tracker.flush 108 | 109 | sleep 15 # TODO: retry logic for replica delay 110 | 111 | metric_names = client.metrics.map { |m| m['name'] } 112 | 113 | assert metric_names.include?('testyprefix.mytime'), 114 | 'testyprefix.mytime should be present' 115 | assert metric_names.include?('testyprefix.mycount'), ' 116 | testyprefix.mycount should be present' 117 | 118 | assert_equal 1, queued("testyprefix.mytime", tags: tags)[:count] 119 | assert_equal 4, queued('testyprefix.mycount') 120 | end 121 | 122 | def test_flush_recovers_from_failure 123 | # create a metric foo of counter type 124 | client.submit test_counter: { type: :counter, value: 12 } 125 | 126 | # failing flush - submit a foo measurement as a gauge (type mismatch) 127 | tracker.measure :test_counter, 2.12 128 | 129 | # won't be accepted 130 | tracker.flush 131 | 132 | tracker.measure :boo, 2.12 133 | tracker.flush 134 | 135 | metric_names = client.metrics.map { |m| m['name'] } 136 | assert metric_names.include?('boo') 137 | end 138 | 139 | def test_flush_handles_invalid_metric_names 140 | tracker.increment :foo # valid 141 | tracker.increment 'fübar' # invalid 142 | tracker.measure 'fu/bar/baz', 12.1 # invalid 143 | @queued = tracker.queued 144 | tracker.flush 145 | 146 | metric_names = client.metrics.map { |m| m['name'] } 147 | assert metric_names.include?('foo') 148 | 149 | # should be sending value for foo 150 | assert_equal 1.0, queued('foo') 151 | end 152 | 153 | def test_flush_handles_invalid_tags 154 | tracker.increment :foo, tags: { hostname: "atreides" } # valid 155 | tracker.increment :bar, tags: { hostname: "glébnöst" } # invalid 156 | tracker.measure 'baz', 2.25, tags: { hostname: "b/l/ak/nok" } # invalid 157 | @queued = tracker.queued 158 | tracker.flush 159 | 160 | metric_names = client.metrics.map { |m| m['name'] } 161 | assert metric_names.include?('foo') 162 | 163 | assert_equal 1.0, queued("foo", tags: { hostname: "atreides" }) 164 | end 165 | 166 | private 167 | 168 | def tracker 169 | @tracker 170 | end 171 | 172 | def client 173 | @tracker.send(:client) 174 | end 175 | 176 | def collector 177 | @tracker.collector 178 | end 179 | 180 | def config 181 | @tracker.config 182 | end 183 | 184 | # wrapper to make api format more easy to query 185 | def queued(name, opts={}) 186 | raise "No queued found" unless @queued 187 | tags_query = opts.fetch(:tags, nil) 188 | 189 | @queued[:measurements].each do |measurement| 190 | if measurement[:name] == name.to_s && measurement[:tags] == tags_query || measurement[:name] == name.to_s && @queued[:tags] == tags_query 191 | if measurement[:count] 192 | # complex metric, return the whole hash 193 | return measurement 194 | else 195 | # return just the value 196 | return measurement[:value] 197 | end 198 | end 199 | end 200 | raise "No queued entry with '#{name}' found." 201 | end 202 | 203 | def tags 204 | @tracker.send(:tags) 205 | end 206 | 207 | def delete_all_metrics 208 | metric_names = client.metrics.map { |metric| metric['name'] } 209 | client.delete_metrics(*metric_names) if !metric_names.empty? 210 | end 211 | 212 | else 213 | # ENV vars not set 214 | puts "Skipping remote tests..." 215 | end 216 | 217 | end 218 | -------------------------------------------------------------------------------- /test/support/environment_helpers.rb: -------------------------------------------------------------------------------- 1 | # Helper methods for environment management that are shared 2 | # between test files. 3 | # 4 | module EnvironmentHelpers 5 | 6 | def clear_config_env_vars 7 | ENV.delete('LIBRATO_USER') 8 | ENV.delete('LIBRATO_TOKEN') 9 | ENV.delete('LIBRATO_PROXY') 10 | ENV.delete("LIBRATO_TAGS") 11 | ENV.delete('LIBRATO_PREFIX') 12 | ENV.delete('LIBRATO_SUITES') 13 | ENV.delete('LIBRATO_LOG_LEVEL') 14 | ENV.delete('LIBRATO_EVENT_MODE') 15 | # system 16 | ENV.delete('http_proxy') 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup 3 | 4 | require 'pry' 5 | require 'minitest/autorun' 6 | 7 | require 'librato/rack' 8 | 9 | require_relative 'support/environment_helpers' 10 | -------------------------------------------------------------------------------- /test/unit/collector/aggregator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Librato 4 | class Collector 5 | class AggregatorTest < Minitest::Test 6 | 7 | def setup 8 | @tags = { hostname: "metrics-web-stg-1" } 9 | @agg = Aggregator.new(default_tags: @tags) 10 | end 11 | 12 | def test_adding_timings 13 | @agg.timing 'request.time.total', 23.7 14 | @agg.timing 'request.time.db', 5.3 15 | @agg.timing 'request.time.total', 64.3 16 | 17 | assert_equal 2, @agg['request.time.total'][:count] 18 | assert_equal 88.0, @agg['request.time.total'][:sum] 19 | end 20 | 21 | def test_block_timing 22 | @agg.timing 'my.task' do 23 | sleep 0.2 24 | end 25 | assert_in_delta @agg['my.task'][:sum], 200, 50 26 | 27 | @agg.timing('another.task') { sleep 0.1 } 28 | assert_in_delta @agg['another.task'][:sum], 100, 50 29 | end 30 | 31 | def test_percentiles 32 | # simple case 33 | [0.1, 0.2, 0.3].each do |val| 34 | @agg.timing 'a.sample.thing', val, percentile: 50 35 | end 36 | assert_equal 0.2, @agg.fetch("a.sample.thing", percentile: 50, tags: @tags), 37 | "can calculate percentile" 38 | 39 | # multiple percentiles 40 | [0.2, 0.35].each do |val| 41 | @agg.timing 'a.sample.thing', val, percentile: [80, 95] 42 | end 43 | assert_equal 0.31, @agg.fetch("a.sample.thing", percentile: 65, tags: @tags), 44 | "can calculate another percentile simultaneously" 45 | assert_equal 0.35, @agg.fetch("a.sample.thing", percentile: 95, tags: @tags), 46 | "can calculate another percentile simultaneously" 47 | 48 | # ensure storage is efficient: this is a little gross because we 49 | # have to inquire past the public interface, but important to verify 50 | assert_equal 1, @agg.instance_variable_get('@percentiles').length, 51 | 'maintains all samples for same metric/source in one pool' 52 | end 53 | 54 | def test_percentiles_invalid 55 | # less than 0.0 56 | assert_raises(Librato::Collector::InvalidPercentile) { 57 | @agg.timing 'a.sample.thing', 123, percentile: -25.5 58 | } 59 | 60 | # greater than 100.0 61 | assert_raises(Librato::Collector::InvalidPercentile) { 62 | @agg.timing 'a.sample.thing', 123, percentile: 100.2 63 | } 64 | end 65 | 66 | def test_percentiles_with_tags 67 | Array(1..10).each do |val| 68 | @agg.timing "a.sample.thing", val, percentile: 50 69 | end 70 | assert_equal 5.5, 71 | @agg.fetch("a.sample.thing", tags: @tags, percentile: 50), 72 | "can calculate percentile with tags" 73 | end 74 | 75 | # Todo: mult percentiles, block form, with source, invalid percentile 76 | 77 | def test_return_values 78 | simple = @agg.timing 'simple', 20 79 | assert_nil simple 80 | 81 | timing = @agg.timing 'foo' do 82 | sleep 0.1 83 | 'bar' 84 | end 85 | assert_equal 'bar', timing 86 | end 87 | 88 | def test_custom_tags 89 | tags_1 = { hostname: "douglas_adams" } 90 | # tags are kept separate 91 | @agg.measure 'meaning.of.life', 1 92 | @agg.measure "meaning.of.life", 42, tags: tags_1 93 | assert_equal 1.0, @agg.fetch('meaning.of.life')[:sum] 94 | assert_equal 42.0, @agg.fetch("meaning.of.life", tags: tags_1)[:sum] 95 | 96 | tags_2 = { hostname: "mine" } 97 | # tags work with time blocks 98 | @agg.timing "mytiming", tags: tags_2 do 99 | sleep 0.02 100 | end 101 | assert_in_delta @agg.fetch("mytiming", tags: tags_2)[:sum], 20, 10 102 | end 103 | 104 | def test_legacy_source 105 | legacy_agg = Aggregator.new 106 | legacy_agg.measure "feature_flag.check", 5, source: "new_feature" 107 | assert_equal 5, legacy_agg.fetch("feature_flag.check", tags: { source: "new_feature" })[:sum] 108 | end 109 | 110 | def test_flush 111 | tags = { hostname: "douglas_adams" } 112 | @agg.measure 'meaning.of.life', 1 113 | @agg.measure "meaning.of.life", 42, tags: tags 114 | 115 | q = Librato::Metrics::Queue.new 116 | @agg.flush_to(q) 117 | 118 | expected = Set.new([ 119 | {:name=>"meaning.of.life", :count=>1, :sum=>1.0, :min=>1.0, :max=>1.0, :tags=>@tags}, 120 | {:name=>"meaning.of.life", :count=>1, :sum=>42.0, :min=>42.0, :max=>42.0, :tags=>tags} 121 | ]) 122 | assert_equal expected, Set.new(q.queued[:measurements]) 123 | end 124 | 125 | def test_flush_percentiles 126 | [1,2,3].each { |i| @agg.timing 'a.timing', i, percentile: 95 } 127 | [1,2,3].each { |i| @agg.timing "b.timing", i, tags: { hostname: "f" }, percentile: [50, 99.9] } 128 | 129 | q = Librato::Metrics::Queue.new(tags: { region: "us-east-1" }) 130 | @agg.flush_to(q) 131 | 132 | queued = q.queued[:measurements] 133 | a_timing = queued.detect{ |q| q[:name] == 'a.timing.p95' } 134 | b_timing_50 = queued.detect{ |q| q[:name] == 'b.timing.p50' } 135 | b_timing_999 = queued.detect{ |q| q[:name] == 'b.timing.p999' } 136 | 137 | refute_nil a_timing, 'sending a.timing percentile' 138 | refute_nil b_timing_50, 'sending b.timing 50th percentile' 139 | refute_nil b_timing_999, 'sending a.timing 99.9th percentile' 140 | 141 | assert_equal 3, a_timing[:value] 142 | assert_equal 2, b_timing_50[:value] 143 | assert_equal 3, b_timing_999[:value] 144 | 145 | assert_equal "f", b_timing_50[:tags][:hostname], "proper tags set" 146 | assert_equal "f", b_timing_999[:tags][:hostname], "proper tags set" 147 | 148 | # flushing clears percentages to track 149 | measurement = @agg.send(:fetch_percentile_store, 'a.timing', tags: @tags) 150 | assert_equal 0, measurement[:percs].length, 'clears percentiles' 151 | end 152 | 153 | def test_default_tags 154 | default_tags = { queue: 'priority' } 155 | agg = Aggregator.new(default_tags: default_tags) 156 | agg.measure 'jobs.queued', 1 157 | 158 | assert_equal 1, agg['jobs.queued'][:sum] 159 | assert_equal default_tags, agg['jobs.queued'][:tags] 160 | end 161 | 162 | def test_tags_option 163 | default_tags = { queue: 'priority' } 164 | tags_option = { worker: 'worker.12' } 165 | agg = Aggregator.new(default_tags: default_tags) 166 | agg.measure 'jobs.queued', 1, tags: tags_option 167 | 168 | assert_equal 1, agg.fetch('jobs.queued', tags: tags_option)[:sum] 169 | assert_equal tags_option, agg.fetch('jobs.queued', tags: tags_option)[:tags] 170 | end 171 | 172 | def test_inherit_tags 173 | default_tags = { queue: 'priority' } 174 | tags_option = { worker: 'worker.12' } 175 | merged_tags = default_tags.merge(tags_option) 176 | agg = Aggregator.new(default_tags: default_tags) 177 | agg.measure 'jobs.queued', 1, tags: tags_option, inherit_tags: true 178 | 179 | assert_equal 1, agg.fetch('jobs.queued', tags: merged_tags)[:sum] 180 | assert_equal merged_tags, agg.fetch('jobs.queued', tags: merged_tags)[:tags] 181 | end 182 | 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /test/unit/collector/counter_cache_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Librato 4 | class Collector 5 | class CounterCacheTest < Minitest::Test 6 | 7 | def test_basic_operations 8 | cc = CounterCache.new(default_tags: { host: 'metricsweb-stagevpc-1' }) 9 | cc.increment :foo 10 | assert_equal 1, cc[:foo][:value] 11 | 12 | # accepts optional argument 13 | cc.increment :foo, :by => 5 14 | assert_equal 6, cc[:foo][:value] 15 | 16 | # legacy style 17 | cc.increment :foo, 2 18 | assert_equal 8, cc[:foo][:value] 19 | 20 | # strings or symbols work 21 | cc.increment 'foo' 22 | assert_equal 9, cc['foo'][:value] 23 | end 24 | 25 | def test_custom_tags 26 | cc = CounterCache.new 27 | 28 | cc.increment :foo, tags: { hostname: "bar" } 29 | assert_equal 1, cc.fetch(:foo, tags: { hostname: "bar" })[:value] 30 | 31 | # symbols also work 32 | cc.increment :foo, tags: { hostname: :baz } 33 | assert_equal 1, cc.fetch(:foo, tags: { hostname: :baz })[:value] 34 | 35 | # strings and symbols are interchangable 36 | cc.increment :foo, tags: { hostname: :bar } 37 | assert_equal 2, cc.fetch(:foo, tags: { hostname: "bar" })[:value] 38 | 39 | # custom source and custom increment 40 | cc.increment :foo, tags: { hostname: "boombah" }, by: 10 41 | assert_equal 10, cc.fetch(:foo, tags: { hostname: "boombah" })[:value] 42 | end 43 | 44 | def test_legacy_source 45 | cc = CounterCache.new 46 | 47 | cc.increment :foo, source: "bar" 48 | 49 | assert_equal 1, cc.fetch(:foo, tags: { source: "bar" })[:value] 50 | end 51 | 52 | def test_sporadic 53 | cc = CounterCache.new(default_tags: { host: 'metricsweb-stagevpc-1' }) 54 | 55 | cc.increment :foo 56 | cc.increment :foo, tags: { hostname: "bar" } 57 | 58 | cc.increment :baz, :sporadic => true 59 | cc.increment :baz, tags: { hostname: 118 }, sporadic: true 60 | assert_equal 1, cc[:baz][:value] 61 | assert_equal 1, cc.fetch(:baz, tags: { hostname: 118 })[:value] 62 | 63 | # persist values once 64 | cc.flush_to(Librato::Metrics::Queue.new) 65 | 66 | # normal values persist 67 | assert_equal 0, cc[:foo][:value] 68 | assert_equal 0, cc.fetch(:foo, tags: { hostname: "bar" })[:value] 69 | 70 | # sporadic do not 71 | assert_nil cc[:baz] 72 | assert_nil cc.fetch(:baz, tags: { hostname: 118 }) 73 | 74 | # add a different sporadic metric 75 | cc.increment :bazoom, :sporadic => true 76 | assert_equal 1, cc[:bazoom][:value] 77 | 78 | # persist values again 79 | cc.flush_to(Librato::Metrics::Queue.new) 80 | assert_nil cc[:bazoom] 81 | end 82 | 83 | def test_flushing 84 | default_tags = { host: 'metricsweb-stagevpc-1' } 85 | cc = CounterCache.new(default_tags: default_tags) 86 | tags = { hostname: "foobar" } 87 | 88 | cc.increment :foo 89 | cc.increment :bar, :by => 2 90 | cc.increment :foo, tags: tags 91 | cc.increment :foo, tags: tags, by: 3 92 | 93 | q = Librato::Metrics::Queue.new(tags: { region: "us-east-1" }) 94 | cc.flush_to(q) 95 | 96 | expected = Set.new [{:name=>"foo", :value=>1, :tags=>default_tags}, 97 | {:name=>"foo", :value=>4, :tags=>tags}, 98 | {:name=>"bar", :value=>2, :tags=>default_tags}] 99 | queued = Set.new(q.measurements) 100 | queued.each { |hash| hash.delete(:time) } 101 | assert_equal queued, expected 102 | end 103 | 104 | def test_default_tags 105 | default_tags = { host: 'metricsweb-stagevpc-1' } 106 | cc = CounterCache.new(default_tags: default_tags) 107 | cc.increment 'user.signup' 108 | 109 | assert_equal 1, cc.fetch('user.signup')[:value] 110 | assert_equal default_tags, cc.fetch('user.signup')[:tags] 111 | end 112 | 113 | def test_tags_option 114 | default_tags = { host: 'metricsweb-stagevpc-1' } 115 | tags_option = { plan: 'developer' } 116 | cc = CounterCache.new(default_tags: default_tags) 117 | cc.increment 'user.signup', tags: tags_option 118 | 119 | assert_equal 1, cc.fetch('user.signup', tags: tags_option)[:value] 120 | assert_equal tags_option, cc.fetch('user.signup', tags: tags_option)[:tags] 121 | end 122 | 123 | def test_inherit_tags 124 | default_tags = { host: 'metricsweb-stagevpc-1' } 125 | tags_option = { plan: 'developer' } 126 | merged_tags = default_tags.merge(tags_option) 127 | cc = CounterCache.new(default_tags: default_tags) 128 | cc.increment 'user.signup', tags: tags_option, inherit_tags: true 129 | 130 | assert_equal 1, cc.fetch('user.signup', tags: merged_tags)[:value] 131 | assert_equal merged_tags, cc.fetch('user.signup', tags: merged_tags)[:tags] 132 | end 133 | 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /test/unit/collector/group_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Librato 4 | class Collector 5 | class GroupTest < Minitest::Test 6 | 7 | def setup 8 | @tags = { region: "us-east-1" } 9 | end 10 | 11 | def test_increment 12 | collector = Collector.new(tags: { host: 'metricsweb-stagevpc-1' }) 13 | collector.group 'foo' do |g| 14 | g.increment :bar 15 | g.increment :baz, tags: @tags 16 | end 17 | assert_equal 1, collector.counters['foo.bar'][:value] 18 | assert_equal 1, collector.counters.fetch("foo.baz", tags: @tags)[:value] 19 | end 20 | 21 | def test_measure 22 | collector = Collector.new(tags: { host: 'metricsweb-stagevpc-1' }) 23 | collector.group 'foo' do |g| 24 | g.measure :baz, 23, tags: @tags 25 | end 26 | assert_equal 23, collector.aggregate.fetch("foo.baz", tags: @tags)[:sum] 27 | end 28 | 29 | def test_timing 30 | collector = Collector.new(tags: { host: 'metricsweb-stagevpc-1' }) 31 | collector.group 'foo' do |g| 32 | g.timing :bam, 32.0, tags: @tags 33 | end 34 | assert_equal 32.0, collector.aggregate.fetch("foo.bam", tags: @tags)[:sum] 35 | end 36 | 37 | def test_timing_block 38 | collector = Collector.new(tags: { host: 'metricsweb-stagevpc-1' }) 39 | collector.group 'foo' do |g| 40 | g.timing :bak, tags: @tags do 41 | sleep 0.01 42 | end 43 | end 44 | assert_in_delta 10.0, collector.aggregate.fetch("foo.bak", tags: @tags)[:sum], 2 45 | end 46 | 47 | def test_nesting 48 | collector = Collector.new(tags: { host: 'metricsweb-stagevpc-1' }) 49 | collector.group 'foo' do |g| 50 | g.group :bar do |b| 51 | b.increment :baz, 2 52 | end 53 | end 54 | assert_equal 2, collector.counters['foo.bar.baz'][:value] 55 | end 56 | 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/unit/collector_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Librato 4 | class CollectorTest < Minitest::Test 5 | 6 | def test_proxy_object_access 7 | collector = Collector.new 8 | assert collector.aggregate, 'should have aggregate object' 9 | assert collector.counters, 'should have counter object' 10 | end 11 | 12 | def test_basic_grouping 13 | collector = Collector.new(tags: { host: 'metricsweb-stagevpc-1' }) 14 | tags = { region: "us-east-1" } 15 | collector.group 'foo' do |g| 16 | g.increment :bar 17 | g.measure :baz, 23, tags: tags 18 | end 19 | assert_equal 1, collector.counters["foo.bar"][:value] 20 | assert_equal 23, collector.aggregate.fetch("foo.baz", tags: tags)[:sum] 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/unit/rack/configuration/suites_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Librato 4 | class Rack 5 | class SuitesTest < Minitest::Test 6 | include EnvironmentHelpers 7 | 8 | def setup; clear_config_env_vars; end 9 | def teardown; clear_config_env_vars; end 10 | 11 | def test_suites_defaults 12 | config = Configuration.new 13 | assert config.metric_suites.include?(:rack), "should include 'rack' by default" 14 | refute config.metric_suites.include?(:foo), "should not include 'foo' by default" 15 | end 16 | 17 | def test_suites_configured_by_explicit_list 18 | ENV['LIBRATO_SUITES'] = 'abc, jkl,prq , xyz' 19 | config = Configuration.new 20 | [:abc, :jkl, :prq, :xyz].each do |suite| 21 | assert config.metric_suites.include?(suite), "expected '#{suite}' to be active" 22 | end 23 | 24 | Librato::Rack::Configuration::DEFAULT_SUITES.each do |suite| 25 | refute config.metric_suites.include?(suite), "should not include '#{suite}' by default" 26 | end 27 | 28 | refute config.metric_suites.include?(:something_else), 'should not include unspecified' 29 | end 30 | 31 | def test_suites_configured_by_inclusion 32 | ENV['LIBRATO_SUITES'] = '+abc, +jkl,+prq' 33 | config = Configuration.new 34 | 35 | [:abc, :jkl, :prq].each do |suite| 36 | assert config.metric_suites.include?(suite), "expected '#{suite.to_s}' to be active" 37 | end 38 | 39 | Librato::Rack::Configuration::DEFAULT_SUITES.each do |suite| 40 | assert config.metric_suites.include?(suite), "should include '#{suite}' by default" 41 | end 42 | end 43 | 44 | def test_suites_configured_by_exclusion 45 | ENV['LIBRATO_SUITES'] = '-rack_method,-jkl' 46 | config = Configuration.new 47 | 48 | [:rack_method, :jkl].each do |suite| 49 | refute config.metric_suites.include?(suite), "expected '#{suite.to_s}' to be disabled" 50 | end 51 | 52 | assert config.metric_suites.include?(:rack), "should include 'rack' by default" 53 | assert config.metric_suites.include?(:rack_status), "should include 'rack_status' by default" 54 | end 55 | 56 | def test_suites_configured_by_inclusion_and_exclusion 57 | ENV['LIBRATO_SUITES'] = '-rack_method, +foo' 58 | config = Configuration.new 59 | 60 | assert config.metric_suites.include?(:rack), "should include 'rack' by default" 61 | assert config.metric_suites.include?(:rack_status), "should include 'rack_status' by default" 62 | assert config.metric_suites.include?(:foo), "expected 'foo' to be active" 63 | refute config.metric_suites.include?(:rack_method), "expected 'rack_method' to be disabled" 64 | end 65 | 66 | def test_invalid_suite_config 67 | ENV['LIBRATO_SUITES'] = '-rack_method, +foo ,bar' 68 | 69 | assert_raises(Librato::Rack::InvalidSuiteConfiguration) { 70 | Configuration.new.metric_suites 71 | } 72 | end 73 | 74 | def test_suites_all 75 | ENV['LIBRATO_SUITES'] = 'all' 76 | config = Configuration.new 77 | 78 | [:foo, :bar, :baz].each do |suite| 79 | assert config.metric_suites.include?(suite), "expected '#{suite}' to be active" 80 | end 81 | end 82 | 83 | def test_suites_none 84 | ENV['LIBRATO_SUITES'] = 'NONE' 85 | config = Configuration.new 86 | 87 | [:foo, :bar, :baz].each do |suite| 88 | refute config.metric_suites.include?(suite), "expected '#{suite.to_s}' to be disabled" 89 | end 90 | end 91 | 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test/unit/rack/configuration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Librato 4 | class Rack 5 | class ConfigurationTest < Minitest::Test 6 | include EnvironmentHelpers 7 | 8 | def setup; clear_config_env_vars; end 9 | def teardown; clear_config_env_vars; end 10 | 11 | def test_defaults 12 | config = Configuration.new 13 | assert_equal 60, config.flush_interval 14 | assert_equal Librato::Metrics.api_endpoint, config.api_endpoint 15 | assert_equal '', config.suites 16 | assert_equal Hash.new, config.tags 17 | end 18 | 19 | def test_environment_variable_config 20 | ENV['LIBRATO_USER'] = 'foo@bar.com' 21 | ENV['LIBRATO_TOKEN'] = 'api_key' 22 | ENV["LIBRATO_TAGS"] = "hostname=metrics-web-stg-1" 23 | ENV['LIBRATO_PROXY'] = 'http://localhost:8080' 24 | ENV['LIBRATO_SUITES'] = 'foo,bar' 25 | expected_tags = { hostname: "metrics-web-stg-1" } 26 | config = Configuration.new 27 | assert_equal 'foo@bar.com', config.user 28 | assert_equal 'api_key', config.token 29 | assert_equal expected_tags, config.tags 30 | assert_equal 'http://localhost:8080', config.proxy 31 | assert_equal 'foo,bar', config.suites 32 | #assert Librato::Rails.explicit_source, 'source is explicit' 33 | end 34 | 35 | def test_http_proxy_env_variable_config 36 | ENV['http_proxy'] = 'http://localhost:8888' 37 | config = Configuration.new 38 | assert_equal 'http://localhost:8888', config.proxy 39 | end 40 | 41 | def test_has_tags 42 | config = Configuration.new 43 | assert !config.has_tags? 44 | config.tags = { hostname: "tessaract" } 45 | assert config.has_tags? 46 | config.tags = nil 47 | assert !config.has_tags?, "tags are not valid when nil" 48 | config.tags = {} 49 | assert !config.has_tags?, "tags are not valid when empty" 50 | end 51 | 52 | def test_invalid_tags_env_var 53 | ENV["LIBRATO_TAGS"] = "loljk" 54 | assert_raises Librato::Rack::InvalidTagConfiguration do 55 | config = Configuration.new 56 | end 57 | end 58 | 59 | def test_prefix_change_notification 60 | config = Configuration.new 61 | listener = listener_object 62 | config.register_listener(listener) 63 | config.prefix = 'newfoo' 64 | assert_equal 'newfoo', listener.prefix 65 | end 66 | 67 | def test_event_mode 68 | config = Configuration.new 69 | assert_nil config.event_mode 70 | 71 | config.event_mode = :synchrony 72 | assert_equal :synchrony, config.event_mode 73 | 74 | # handle string config 75 | config.event_mode = 'eventmachine' 76 | assert_equal :eventmachine, config.event_mode 77 | 78 | # handle invalid 79 | config2 = Configuration.new 80 | config2.event_mode = 'fooballoo' 81 | assert_nil config2.event_mode 82 | 83 | # env detection 84 | ENV['LIBRATO_EVENT_MODE'] = 'eventmachine' 85 | config3 = Configuration.new 86 | assert_equal :eventmachine, config3.event_mode 87 | end 88 | 89 | private 90 | 91 | def listener_object 92 | listener = Object.new 93 | def listener.prefix=(prefix) 94 | @prefix = prefix 95 | end 96 | def listener.prefix 97 | @prefix 98 | end 99 | listener 100 | end 101 | 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/unit/rack/logger_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'stringio' 3 | 4 | module Librato 5 | class Rack 6 | class LoggerTest < Minitest::Test 7 | 8 | def setup 9 | @buffer = StringIO.new 10 | #log_object = ::Logger.new(@buffer) # stdlib logger 11 | @logger = Logger.new(@buffer) # rack logger 12 | end 13 | 14 | def test_log_levels 15 | assert_equal :info, @logger.log_level, 'should default to info' 16 | 17 | @logger.log_level = :debug 18 | assert_equal :debug, @logger.log_level, 'should accept symbols' 19 | 20 | @logger.log_level = 'trace' 21 | assert_equal :trace, @logger.log_level, 'should accept strings' 22 | 23 | assert_raises(InvalidLogLevel) { @logger.log_level = :foo } 24 | end 25 | 26 | def test_simple_logging 27 | @logger.log_level = :info 28 | 29 | # logging at log level 30 | @logger.log :info, 'a log message' 31 | assert_equal 1, buffer_lines.length, 'should have added a line' 32 | assert buffer_lines[0].index('a log message'), 'should log message' 33 | 34 | # logging above level 35 | @logger.log :error, 'an error message' 36 | assert_equal 2, buffer_lines.length, 'should have added a line' 37 | assert buffer_lines[1].index('an error message'), 'should log message' 38 | 39 | # logging below level 40 | @logger.log :debug, 'a debug message' 41 | assert_equal 2, buffer_lines.length, 'should not have added a line' 42 | end 43 | 44 | def test_logging_through_stdlib_logger_object 45 | stdlib_logger = ::Logger.new(@buffer) 46 | @logger = Logger.new(stdlib_logger) 47 | 48 | @logger.log_level = :info 49 | 50 | # logging at log level 51 | @logger.log :info, 'a log message' 52 | assert_equal 1, buffer_lines.length, 'should have added a line' 53 | assert buffer_lines[0].index('a log message'), 'should log message' 54 | 55 | # logging above level 56 | @logger.log :error, 'an error message' 57 | assert_equal 2, buffer_lines.length, 'should have added a line' 58 | assert buffer_lines[1].index('an error message'), 'should log message' 59 | 60 | # logging below level 61 | @logger.log :debug, 'a debug message' 62 | assert_equal 2, buffer_lines.length, 'should not have added a line' 63 | end 64 | 65 | def test_block_logging 66 | @logger.log_level = :info 67 | 68 | # logging at log level 69 | @logger.log(:info) { "log statement" } 70 | assert_equal 1, buffer_lines.length, 'should have added a line' 71 | assert buffer_lines[0].index('log statement'), 'should log message' 72 | end 73 | 74 | def test_log_prefix 75 | assert_equal '', @logger.prefix 76 | 77 | @logger.prefix = '[test prefix] ' 78 | @logger.log :error, 'an error message' 79 | assert buffer_lines[0].index('[test prefix] '), 'should use prefix' 80 | end 81 | 82 | def test_log_buffering 83 | buffer = StringIO.new 84 | logger = Logger.new # no outlet provided 85 | logger.prefix = '' 86 | 87 | logger.log :error, 'some business' 88 | logger.log :error, 'some more business' 89 | logger.outlet = buffer 90 | logger.log :error, 'some ongoing business' 91 | 92 | buffer.rewind 93 | lines = buffer.readlines 94 | assert_equal 'some business', lines[0].chomp 95 | assert_equal 'some more business', lines[1].chomp 96 | assert_equal 'some ongoing business', lines[2].chomp 97 | end 98 | 99 | private 100 | 101 | def buffer_lines 102 | @buffer.rewind 103 | @buffer.readlines 104 | end 105 | 106 | end 107 | end 108 | end -------------------------------------------------------------------------------- /test/unit/rack/tracker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Librato 4 | class Rack 5 | class TrackerTest < Minitest::Test 6 | 7 | def test_sets_prefix 8 | config = Configuration.new 9 | config.prefix = 'first' 10 | 11 | tracker = Tracker.new(config) 12 | assert_equal 'first', tracker.collector.prefix 13 | 14 | config.prefix = 'second' 15 | assert_equal 'second', tracker.collector.prefix 16 | end 17 | 18 | def test_requires_tags_on_heroku 19 | config = Configuration.new 20 | config.user, config.token = 'foo', 'bar' 21 | @buffer = StringIO.new 22 | config.log_target = @buffer 23 | tracker = Tracker.new(config) 24 | tracker.on_heroku = true 25 | 26 | assert_equal false, tracker.send(:should_start?), 27 | 'should not start with implicit tags on heroku' 28 | assert buffer_lines[0].index("tags must be provided") 29 | 30 | config.tags = { hostname: "myapp" } 31 | new_tracker = Tracker.new(config) 32 | assert_equal true, new_tracker.send(:should_start?) 33 | end 34 | 35 | def test_autorun_can_prevent_startup 36 | ENV['LIBRATO_AUTORUN']='0' 37 | config = Configuration.new 38 | config.user, config.token = 'foo', 'bar' 39 | @buffer = StringIO.new 40 | config.log_target = @buffer 41 | tracker = Tracker.new(config) 42 | 43 | assert_equal false, tracker.send(:should_start?), 44 | 'should not start if autorun set to 0' 45 | 46 | ENV.delete('LIBRATO_AUTORUN') 47 | end 48 | 49 | def test_invalid_tags_can_prevent_startup 50 | config = Configuration.new 51 | config.user, config.token = "foo", "bar" 52 | @buffer = StringIO.new 53 | config.log_target = @buffer 54 | config.tags = { hostname: "!!!" } 55 | tracker_1 = Tracker.new(config) 56 | 57 | assert_equal false, tracker_1.send(:should_start?) 58 | assert buffer_lines.to_s.include?("invalid tags") 59 | 60 | config.tags = { "!!!" => "metrics-web-stg-1" } 61 | tracker_2 = Tracker.new(config) 62 | 63 | assert_equal false, tracker_2.send(:should_start?) 64 | assert buffer_lines.to_s.include?("invalid tags") 65 | end 66 | 67 | def test_exceeding_default_tags_limit_can_prevent_startup 68 | config = Configuration.new 69 | config.user, config.token = "foo", "bar" 70 | @buffer = StringIO.new 71 | config.log_target = @buffer 72 | config.tags = { a: 1, b: 2, c: 3, d: 4 } 73 | tracker_1 = Tracker.new(config) 74 | 75 | assert_equal true, tracker_1.send(:should_start?) 76 | 77 | config.tags = { a: 1, b: 2, c: 3, d: 4, e: 5 } 78 | 79 | tracker_2 = Tracker.new(config) 80 | 81 | assert_equal false, tracker_2.send(:should_start?) 82 | assert buffer_lines.to_s.include?("cannot exceed default tags limit") 83 | end 84 | 85 | def test_suite_configured 86 | ENV['LIBRATO_SUITES'] = 'abc,prq' 87 | 88 | tracker = Tracker.new(Configuration.new) 89 | assert tracker.suite_enabled?(:abc) 90 | assert tracker.suite_enabled?(:prq) 91 | refute tracker.suite_enabled?(:xyz) 92 | ensure 93 | ENV.delete('LIBRATO_SUITES') 94 | end 95 | 96 | def test_rack_process_queued 97 | ENV['LIBRATO_SUITES'] = 'all' 98 | tracker = Tracker.new(Configuration.new) 99 | refute_nil tracker.queued[:measurements] 100 | refute_nil tracker.queued[:measurements].detect { |measurement| measurement[:name] == 'rack.processes' } 101 | ensure 102 | ENV.delete('LIBRATO_SUITES') 103 | end 104 | 105 | def test_rack_process_not_queued 106 | ENV['LIBRATO_SUITES'] = 'none' 107 | tracker = Tracker.new(Configuration.new) 108 | assert_nil tracker.queued[:measurements] 109 | ensure 110 | ENV.delete('LIBRATO_SUITES') 111 | end 112 | 113 | private 114 | 115 | def buffer_lines 116 | @buffer.rewind 117 | @buffer.readlines 118 | end 119 | 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/unit/rack/validating_queue_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Librato 4 | class Rack 5 | class ValidatingQueueTest < Minitest::Test 6 | def setup 7 | @queue = ValidatingQueue.new 8 | end 9 | 10 | def test_valid_metric_name_tag_name_tag_value 11 | @queue.add valid_metric_name: { 12 | value: rand(100), 13 | tags: { valid_tag_name: 'valid_tag_value' } 14 | } 15 | @queue.validate_measurements 16 | refute_empty @queue.queued[:measurements] 17 | end 18 | 19 | def test_valid_tag_value_with_whitespace 20 | @queue.add valid_metric_name: { 21 | value: rand(100), 22 | tags: { valid_tag_name: 'valid tag value' } 23 | } 24 | @queue.validate_measurements 25 | refute_empty @queue.queued[:measurements] 26 | end 27 | 28 | def test_valid_tag_value_with_slashes 29 | @queue.add valid_metric_name: { 30 | value: rand(100), 31 | tags: { valid_tag_name: 'valid/tag/value' } 32 | } 33 | @queue.validate_measurements 34 | refute_empty @queue.queued[:measurements] 35 | end 36 | 37 | def test_valid_tag_value_with_question_mark 38 | @queue.add valid_metric_name: { 39 | value: rand(100), 40 | tags: { valid_tag_name: 'valid_tag_value?' } 41 | } 42 | @queue.validate_measurements 43 | refute_empty @queue.queued[:measurements] 44 | end 45 | 46 | def test_invalid_metric_name 47 | @queue.add 'invalid metric name' => { 48 | value: rand(100), 49 | tags: { valid_tag_name: 'valid_tag_value' } 50 | } 51 | @queue.validate_measurements 52 | assert_empty @queue.queued[:measurements] 53 | end 54 | 55 | def test_invalid_tag_name 56 | @queue.add valid_metric_name: { 57 | value: rand(100), 58 | tags: { 'invalid_tag_name!' => 'valid_tag_value' } 59 | } 60 | @queue.validate_measurements 61 | assert_empty @queue.queued[:measurements] 62 | end 63 | 64 | def test_invalid_tag_value 65 | @queue.add valid_metric_name: { 66 | value: rand(100), 67 | tags: { valid_tag_name: 'invalid_tag_value!' } 68 | } 69 | @queue.validate_measurements 70 | assert_empty @queue.queued[:measurements] 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/unit/rack/worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'stringio' 3 | 4 | require 'eventmachine' 5 | require 'em-synchrony' 6 | 7 | module Librato 8 | class Rack 9 | class WorkerTest < Minitest::Test 10 | 11 | def test_basic_use 12 | worker = Worker.new 13 | counter = 0 14 | 15 | thread = Thread.new do 16 | worker.run_periodically(0.1) do 17 | counter += 1 18 | end 19 | end 20 | 21 | sleep 0.45 22 | assert_in_delta 4, counter, 1 23 | 24 | worker.stop! 25 | thread.join 26 | end 27 | 28 | def test_start_time 29 | worker = Worker.new 30 | 31 | 20.times do 32 | time = Time.now 33 | start = worker.start_time(60) 34 | assert start >= time + 1, 'should be more than 1 second from when run' 35 | assert start <= time + 120, 'should not be more than 60 seconds from when run' 36 | end 37 | end 38 | 39 | def test_start_time_with_sync 40 | worker = Worker.new(sync: true) 41 | 42 | time = Time.now 43 | start = worker.start_time(60) 44 | assert start >= time + 60, 'should be more than 60 seconds from when run' 45 | assert_equal 0, start.sec, 'should start on a whole minute' 46 | 47 | time = Time.now 48 | start = worker.start_time(10) 49 | assert start >= time + 10, 'should be more than 10 seconds from when run' 50 | assert_equal 0, start.sec%10, 'should be evenly divisible with whole minutes' 51 | end 52 | 53 | def test_timer_type 54 | worker = Worker.new 55 | assert_equal :sleep, worker.timer 56 | 57 | em_worker = Worker.new(:timer => 'eventmachine') 58 | assert_equal :eventmachine, em_worker.timer 59 | 60 | # tolerate explicit nils 61 | worker = Worker.new(:timer => nil) 62 | assert_equal :sleep, worker.timer 63 | end 64 | 65 | def test_eventmachine_timer 66 | # flaps a bit on jruby; skip for now 67 | return if defined?(JRUBY_VERSION) 68 | worker = Worker.new(:timer => :eventmachine) 69 | counter = 0 70 | 71 | thread = Thread.new do 72 | EventMachine.run do 73 | worker.run_periodically(0.1) do 74 | counter += 1 75 | end 76 | EM.add_timer(0.6) { worker.stop!; EM.stop } 77 | end 78 | end 79 | 80 | sleep 0.45 81 | assert_in_delta 4, counter, 1 82 | thread.join 83 | end 84 | 85 | def test_em_synchrony_timer 86 | worker = Worker.new(:timer => :synchrony) 87 | counter = 0 88 | 89 | thread = Thread.new do 90 | EM.synchrony do 91 | worker.run_periodically(0.1) do 92 | counter += 1 93 | end 94 | EventMachine.stop 95 | end 96 | end 97 | 98 | sleep 0.45 99 | assert_in_delta 4, counter, 1 100 | Thread.kill(thread) 101 | end 102 | 103 | end 104 | end 105 | end --------------------------------------------------------------------------------