librato-rack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,24 @@
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 the <organization> nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software 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.
data/README.md ADDED
@@ -0,0 +1,176 @@
1
+ librato-rack
2
+ =======
3
+
4
+ [![Build Status](https://secure.travis-ci.org/librato/librato-rack.png?branch=master)](http://travis-ci.org/librato/librato-rack)
5
+
6
+ *Note: librato-rack is currently in beta and is currently recommended primarily for early-adopter use*
7
+
8
+ `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.
9
+
10
+ Currently Ruby 1.9.2+ is required.
11
+
12
+ ## Quick Start
13
+
14
+ Install `librato-rack` as middleware in your application:
15
+
16
+ use Librato::Rack
17
+
18
+ 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:
19
+
20
+ # keep counts of key events
21
+ Librato.increment 'user.signup'
22
+
23
+ # benchmark sections of code to verify production performance
24
+ Librato.timing 'my.complicated.work' do
25
+ # do work
26
+ end
27
+
28
+ # track averages across requests
29
+ Librato.measure 'user.social_graph.nodes', user.social_graph.size
30
+
31
+ ## Installation & Configuration
32
+
33
+ Install the gem:
34
+
35
+ $ gem install librato-rack
36
+
37
+ Or add to your Gemfile if using bundler:
38
+
39
+ gem "librato-rack"
40
+
41
+ In your rackup file or equivalent, require and add the middleware:
42
+
43
+ require 'librato-rack'
44
+ use Librato::Rack
45
+
46
+ 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:
47
+
48
+ ##### Use environment variables
49
+
50
+ 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.
51
+
52
+ * `LIBRATO_SOURCE` - the default source to use for submitted metrics. If this is not set, hostname of the executing machine will be the default source
53
+ * `LIBRATO_PREFIX` - a prefix which will be appended to all metric names
54
+ * `LIBRATO_LOG_LEVEL` - see logging section for more
55
+
56
+ ##### Use a configuration object
57
+
58
+ 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:
59
+
60
+ config = Librato::Rack::Configuration.new
61
+ config.user = 'myuser@mysite.com'
62
+ config.token = 'mytoken'
63
+ # …more configuration
64
+
65
+ use Librato::Rack, config
66
+
67
+ See the configuration class for all available options.
68
+
69
+ ##### Running on Heroku
70
+
71
+ If you are using the Librato Metrics Heroku addon, 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.
72
+
73
+ You must also specify a custom source for your app to track properly. If an explicit source is not set, `librato-rack` will not start. You can set the source in your environment:
74
+
75
+ heroku config:add LIBRATO_SOURCE=myappname
76
+
77
+ 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.
78
+
79
+ ## Custom Measurements
80
+
81
+ Tracking anything that interests you is easy with Metrics. There are four primary helpers available:
82
+
83
+ #### increment
84
+
85
+ Use for tracking a running total of something _across_ requests, examples:
86
+
87
+ # increment the 'sales_completed' metric by one
88
+ Librato.increment 'sales.completed'
89
+
90
+ # increment by five
91
+ Librato.increment 'items.purchased', :by => 5
92
+
93
+ # increment with a custom source
94
+ Librato.increment 'user.purchases', :source => user.id
95
+
96
+ 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
97
+
98
+ ###### Sporadic Increment Reporting
99
+
100
+ 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.
101
+
102
+ Especially with custom sources you may want the opposite behavior - reporting a measurement only during intervals where `increment` was called on the metric:
103
+
104
+ # report a value for 'user.uploaded_file' only during non-zero intervals
105
+ Librato.increment 'user.uploaded_file', :source => user.id, :sporadic => true
106
+
107
+ #### measure
108
+
109
+ Use when you want to track an average value _per_-request. Examples:
110
+
111
+ Librato.measure 'user.social_graph.nodes', 212
112
+
113
+ # report from a custom source
114
+ Librato.measure 'jobs.queued', 3, :source => 'worker.12'
115
+
116
+ #### timing
117
+
118
+ Like `Librato.measure` this is per-request, but specialized for timing information:
119
+
120
+ Librato.timing 'twitter.lookup.time', 21.2
121
+
122
+ The block form auto-submits the time it took for its contents to execute as the measurement value:
123
+
124
+ Librato.timing 'twitter.lookup.time' do
125
+ @twitter = Twitter.lookup(user)
126
+ end
127
+
128
+ #### group
129
+
130
+ There is also a grouping helper, to make managing nested metrics easier. So this:
131
+
132
+ Librato.measure 'memcached.gets', 20
133
+ Librato.measure 'memcached.sets', 2
134
+ Librato.measure 'memcached.hits', 18
135
+
136
+ Can also be written as:
137
+
138
+ Librato.group 'memcached' do |g|
139
+ g.measure 'gets', 20
140
+ g.measure 'sets', 2
141
+ g.measure 'hits', 18
142
+ end
143
+
144
+ Symbols can be used interchangably with strings for metric names.
145
+
146
+ ## Cross-Process Aggregation
147
+
148
+ `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 source (default is your hostname) before persisting the data.
149
+
150
+ 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.
151
+ Current pricing applies after aggregation, so in this case you will be charged for 4 streams instead of 32.
152
+
153
+ If you want to report per-process instead, you can set `source_pids` to `true` in
154
+ your config, which will append the process id to the source name used by each thread.
155
+
156
+ ## Troubleshooting
157
+
158
+ 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.
159
+
160
+ 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.
161
+
162
+ Submission times are total time but submission I/O is non-blocking - your process will continue to handle requests during submissions.
163
+
164
+ 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.
165
+
166
+ ## Contribution
167
+
168
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
169
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
170
+ * Fork the project and submit a pull request from a feature or bugfix branch.
171
+ * Please include tests. This is important so we don't break your changes unintentionally in a future version.
172
+ * 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.
173
+
174
+ ## Copyright
175
+
176
+ Copyright (c) 2013 [Librato Inc.](http://librato.com) See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
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
+ # console
9
+ desc "Open an console session preloaded with this library"
10
+ task :console do
11
+ sh "pry -rubygems -r ./lib/librato-rack.rb"
12
+ end
13
+
14
+ Bundler::GemHelper.install_tasks
15
+
16
+ require 'rake/testtask'
17
+
18
+ Rake::TestTask.new(:test) do |t|
19
+ t.libs << 'lib'
20
+ t.libs << 'test'
21
+ t.pattern = 'test/**/*_test.rb'
22
+ t.verbose = false
23
+ end
24
+
25
+ task :default => :test
@@ -0,0 +1 @@
1
+ require 'librato/rack'
@@ -0,0 +1,48 @@
1
+ module Librato
2
+ # collects and stores measurement values over time so they can be
3
+ # reported periodically to the Metrics service
4
+ #
5
+ class Collector
6
+ extend Forwardable
7
+
8
+ def_delegators :counters, :increment
9
+ def_delegators :aggregate, :measure, :timing
10
+
11
+ # access to internal aggregator object
12
+ def aggregate
13
+ @aggregator_cache ||= Aggregator.new(:prefix => @prefix)
14
+ end
15
+
16
+ # access to internal counters object
17
+ def counters
18
+ @counter_cache ||= CounterCache.new
19
+ end
20
+
21
+ # remove any accumulated but unsent metrics
22
+ def delete_all
23
+ aggregate.delete_all
24
+ counters.delete_all
25
+ end
26
+ alias :clear :delete_all
27
+
28
+ def group(prefix)
29
+ group = Group.new(self, prefix)
30
+ yield group
31
+ end
32
+
33
+ # update prefix
34
+ def prefix=(new_prefix)
35
+ @prefix = new_prefix
36
+ aggregate.prefix = @prefix
37
+ end
38
+
39
+ def prefix
40
+ @prefix
41
+ end
42
+
43
+ end
44
+ end
45
+
46
+ require 'librato/collector/aggregator'
47
+ require 'librato/collector/counter_cache'
48
+ require 'librato/collector/group'
@@ -0,0 +1,97 @@
1
+ module Librato
2
+ class Collector
3
+ # maintains storage of timing and measurement type measurements
4
+ #
5
+ class Aggregator
6
+ extend Forwardable
7
+
8
+ def_delegators :@cache, :empty?, :prefix, :prefix=
9
+
10
+ def initialize(options={})
11
+ @cache = Librato::Metrics::Aggregator.new(:prefix => options[:prefix])
12
+ @lock = Mutex.new
13
+ end
14
+
15
+ def [](key)
16
+ fetch(key)
17
+ end
18
+
19
+ def fetch(key, options={})
20
+ return nil if @cache.empty?
21
+ gauges = nil
22
+ source = options[:source]
23
+ @lock.synchronize { gauges = @cache.queued[:gauges] }
24
+ gauges.each do |metric|
25
+ if metric[:name] == key.to_s
26
+ return metric if !source && !metric[:source]
27
+ return metric if source.to_s == metric[:source]
28
+ end
29
+ end
30
+ nil
31
+ end
32
+
33
+ def delete_all
34
+ @lock.synchronize { @cache.clear }
35
+ end
36
+
37
+ # transfer all measurements to queue and reset internal status
38
+ def flush_to(queue)
39
+ queued = nil
40
+ @lock.synchronize do
41
+ return if @cache.empty?
42
+ queued = @cache.queued
43
+ @cache.clear
44
+ end
45
+ queue.merge!(queued) if queued
46
+ end
47
+
48
+ # @example Simple measurement
49
+ # measure 'sources_returned', sources.length
50
+ #
51
+ # @example Simple timing in milliseconds
52
+ # timing 'twitter.lookup', 2.31
53
+ #
54
+ # @example Block-based timing
55
+ # timing 'db.query' do
56
+ # do_my_query
57
+ # end
58
+ #
59
+ # @example Custom source
60
+ # measure 'user.all_orders', user.order_count, :source => user.id
61
+ #
62
+ def measure(*args, &block)
63
+ options = {}
64
+ event = args[0].to_s
65
+ returned = nil
66
+
67
+ # handle block or specified argument
68
+ if block_given?
69
+ start = Time.now
70
+ returned = yield
71
+ value = ((Time.now - start) * 1000.0).to_i
72
+ elsif args[1]
73
+ value = args[1]
74
+ else
75
+ raise "no value provided"
76
+ end
77
+
78
+ # detect options hash if present
79
+ if args.length > 1 and args[-1].respond_to?(:each)
80
+ options = args[-1]
81
+ end
82
+ source = options[:source]
83
+
84
+ @lock.synchronize do
85
+ if source
86
+ @cache.add event => {:source => source, :value => value}
87
+ else
88
+ @cache.add event => value
89
+ end
90
+ end
91
+ returned
92
+ end
93
+ alias :timing :measure
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,113 @@
1
+ module Librato
2
+ class Collector
3
+ # maintains storage of a set of incrementable, counter-like
4
+ # measurements
5
+ #
6
+ class CounterCache
7
+ SEPARATOR = '%%'
8
+
9
+ extend Forwardable
10
+
11
+ def_delegators :@cache, :empty?
12
+
13
+ def initialize
14
+ @cache = {}
15
+ @lock = Mutex.new
16
+ @sporadics = Set.new
17
+ end
18
+
19
+ # Retrieve the current value for a given metric. This is a short
20
+ # form for convenience which only retrieves metrics with no custom
21
+ # source specified. For more options see #fetch.
22
+ #
23
+ # @param [String|Symbol] key metric name
24
+ # @return [Integer|Float] current value
25
+ def [](key)
26
+ fetch(key)
27
+ end
28
+
29
+ # removes all tracked metrics. note this removes all measurement
30
+ # data AND metric names any continuously tracked metrics will not
31
+ # report until they get another measurement
32
+ def delete_all
33
+ @lock.synchronize { @cache.clear }
34
+ end
35
+
36
+
37
+ def fetch(key, options={})
38
+ if options[:source]
39
+ key = "#{key}#{SEPARATOR}#{options[:source]}"
40
+ end
41
+ @lock.synchronize do
42
+ @cache[key.to_s]
43
+ end
44
+ end
45
+
46
+ # transfer all measurements to queue and reset internal status
47
+ def flush_to(queue)
48
+ counts = nil
49
+ @lock.synchronize do
50
+ # work off of a duplicate data set so we block for
51
+ # as little time as possible
52
+ counts = @cache.dup
53
+ reset_cache
54
+ end
55
+ counts.each do |metric, value|
56
+ metric, source = metric.split(SEPARATOR)
57
+ if source
58
+ queue.add metric => {:value => value, :source => source}
59
+ else
60
+ queue.add metric => value
61
+ end
62
+ end
63
+ end
64
+
65
+ # Increment a given metric
66
+ #
67
+ # @example Increment metric 'foo' by 1
68
+ # increment :foo
69
+ #
70
+ # @example Increment metric 'bar' by 2
71
+ # increment :bar, :by => 2
72
+ #
73
+ # @example Increment metric 'foo' by 1 with a custom source
74
+ # increment :foo, :source => user.id
75
+ #
76
+ def increment(counter, options={})
77
+ if options.is_a?(Fixnum)
78
+ # suppport legacy style
79
+ options = {:by => options}
80
+ end
81
+ by = options[:by] || 1
82
+ if options[:source]
83
+ metric = "#{counter}#{SEPARATOR}#{options[:source]}"
84
+ else
85
+ metric = counter.to_s
86
+ end
87
+ if options[:sporadic]
88
+ make_sporadic(metric)
89
+ end
90
+ @lock.synchronize do
91
+ @cache[metric] ||= 0
92
+ @cache[metric] += by
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def make_sporadic(metric)
99
+ @sporadics << metric
100
+ end
101
+
102
+ def reset_cache
103
+ # remove any source/metric pairs that aren't continuous
104
+ @sporadics.each { |metric| @cache.delete(metric) }
105
+ @sporadics.clear
106
+ # reset all continuous source/metric pairs to 0
107
+ @cache.each_key { |key| @cache[key] = 0 }
108
+ end
109
+
110
+ end
111
+
112
+ end
113
+ end