librato-rails 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -28,7 +28,9 @@ Create a `config/librato.yml` like the following:
28
28
  production:
29
29
  user: <your-email>
30
30
  token: <your-api-key>
31
-
31
+
32
+ (the file is parsed via ERB in case you need to add some magic in there - useful in some cloud environments)
33
+
32
34
  OR provide `LIBRATO_METRICS_USER` and `LIBRATO_METRICS_TOKEN` environment variables. If both env variables and a config file are present, environment variables will take precendence.
33
35
 
34
36
  Note that using a configuration file allows you to specify configurations per-environment. Submission will be disabled in any environment without credentials. However, if environment variables are set they will be used in all environments.
@@ -53,17 +55,30 @@ Use for tracking a running total of something _across_ requests, examples:
53
55
  Librato.increment 'sales_completed'
54
56
 
55
57
  # increment by five
56
- Librato.increment 'items_purchased', 5
58
+ Librato.increment 'items_purchased', :by => 5
59
+
60
+ # increment with a custom source
61
+ Librato.increment 'user.purchases', :source => user.id
57
62
 
58
63
  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
59
64
 
65
+ ###### Sporadic Increment Reporting
66
+
67
+ 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.
68
+
69
+ Especially with custom sources you may want the opposite behavior - reporting a measurement only during intervals where `increment` was called on the metric:
70
+
71
+ # report a value for 'user.uploaded_file' only during non-zero intervals
72
+ Librato.increment 'user.uploaded_file', :source => user.id, :sporadic => true
73
+
60
74
  #### measure
61
75
 
62
76
  Use when you want to track an average value _per_-request. Examples:
63
77
 
64
78
  Librato.measure 'user.social_graph.nodes', 212
65
79
 
66
- Librato.measure 'jobs.queued', 3
80
+ # report from a custom source
81
+ Librato.measure 'jobs.queued', 3, :source => 'worker.12'
67
82
 
68
83
 
69
84
  #### timing
@@ -96,6 +111,16 @@ Can also be written as:
96
111
 
97
112
  Symbols can be used interchangably with strings for metric names.
98
113
 
114
+ ## Cross-process Aggregation
115
+
116
+ `librato-rails` 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.
117
+
118
+ 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.
119
+ Current pricing applies after aggregation, so in this case you will be charged for 4 streams instead of 32.
120
+
121
+ If you want to report per-process instead, you can set `source_pids` to `true` in
122
+ your config, which will append the process id to the source name used by each thread.
123
+
99
124
  ## Contribution
100
125
 
101
126
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
@@ -3,7 +3,7 @@ module Librato
3
3
  class Aggregator
4
4
  extend Forwardable
5
5
 
6
- def_delegators :@cache, :empty?
6
+ def_delegators :@cache, :empty?, :prefix, :prefix=
7
7
 
8
8
  def initialize(options={})
9
9
  @cache = Librato::Metrics::Aggregator.new(:prefix => options[:prefix])
@@ -11,11 +11,19 @@ module Librato
11
11
  end
12
12
 
13
13
  def [](key)
14
+ fetch(key)
15
+ end
16
+
17
+ def fetch(key, options={})
14
18
  return nil if @cache.empty?
15
19
  gauges = nil
20
+ source = options[:source]
16
21
  @lock.synchronize { gauges = @cache.queued[:gauges] }
17
22
  gauges.each do |metric|
18
- return metric if metric[:name] == key.to_s
23
+ if metric[:name] == key.to_s
24
+ return metric if !source && !metric[:source]
25
+ return metric if source.to_s == metric[:source]
26
+ end
19
27
  end
20
28
  nil
21
29
  end
@@ -24,8 +32,7 @@ module Librato
24
32
  @lock.synchronize { @cache.clear }
25
33
  end
26
34
 
27
- # transfer all measurements to a queue and
28
- # reset internal status
35
+ # transfer all measurements to queue and reset internal status
29
36
  def flush_to(queue, options={})
30
37
  queued = nil
31
38
  @lock.synchronize do
@@ -36,9 +43,26 @@ module Librato
36
43
  queue.merge!(queued) if queued
37
44
  end
38
45
 
46
+ # @example Simple measurement
47
+ # measure 'sources_returned', sources.length
48
+ #
49
+ # @example Simple timing in milliseconds
50
+ # timing 'twitter.lookup', 2.31
51
+ #
52
+ # @example Block-based timing
53
+ # timing 'db.query' do
54
+ # do_my_query
55
+ # end
56
+ #
57
+ # @example Custom source
58
+ # measure 'user.all_orders', user.order_count, :source => user.id
59
+ #
39
60
  def measure(*args, &block)
61
+ options = {}
40
62
  event = args[0].to_s
41
63
  returned = nil
64
+
65
+ # handle block or specified argument
42
66
  if block_given?
43
67
  start = Time.now
44
68
  returned = yield
@@ -48,8 +72,19 @@ module Librato
48
72
  else
49
73
  raise "no value provided"
50
74
  end
75
+
76
+ # detect options hash if present
77
+ if args.length > 1 and args[-1].respond_to?(:each)
78
+ options = args[-1]
79
+ end
80
+ source = options[:source]
81
+
51
82
  @lock.synchronize do
52
- @cache.add event => value
83
+ if source
84
+ @cache.add event => {:source => source, :value => value}
85
+ else
86
+ @cache.add event => value
87
+ end
53
88
  end
54
89
  returned
55
90
  end
@@ -0,0 +1,45 @@
1
+
2
+ # an abstract collector object which can be given measurement values
3
+ # and can periodically report those values back to the Metrics service
4
+
5
+ module Librato
6
+ module Rails
7
+ class Collector
8
+ extend Forwardable
9
+
10
+ def_delegators :counters, :increment
11
+ def_delegators :aggregate, :measure, :timing
12
+
13
+ # access to internal aggregator object
14
+ def aggregate
15
+ @aggregator_cache ||= Aggregator.new(:prefix => @prefix)
16
+ end
17
+
18
+ # access to internal counters object
19
+ def counters
20
+ @counter_cache ||= CounterCache.new
21
+ end
22
+
23
+ # remove any accumulated but unsent metrics
24
+ def delete_all
25
+ aggregate.delete_all
26
+ counters.delete_all
27
+ end
28
+
29
+ def group(prefix)
30
+ group = Group.new(prefix)
31
+ yield group
32
+ end
33
+
34
+ # update prefix
35
+ def prefix=(new_prefix)
36
+ @prefix = new_prefix
37
+ aggregate.prefix = @prefix
38
+ end
39
+
40
+ def prefix
41
+ @prefix
42
+ end
43
+ end
44
+ end
45
+ end
@@ -2,6 +2,8 @@ module Librato
2
2
  module Rails
3
3
 
4
4
  class CounterCache
5
+ DEFAULT_SOURCE = '%%'
6
+
5
7
  extend Forwardable
6
8
 
7
9
  def_delegators :@cache, :empty?
@@ -9,32 +11,108 @@ module Librato
9
11
  def initialize
10
12
  @cache = {}
11
13
  @lock = Mutex.new
14
+ @sporadics = {}
12
15
  end
13
16
 
17
+ # Retrieve the current value for a given metric. This is a short
18
+ # form for convenience which only retrieves metrics with no custom
19
+ # source specified. For more options see #fetch.
20
+ #
21
+ # @param [String|Symbol] key metric name
22
+ # @return [Integer|Float] current value
14
23
  def [](key)
15
- @lock.synchronize { @cache[key.to_s] }
24
+ @lock.synchronize do
25
+ @cache[key.to_s][DEFAULT_SOURCE]
26
+ end
16
27
  end
17
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
18
32
  def delete_all
19
33
  @lock.synchronize { @cache.clear }
20
34
  end
21
35
 
36
+
37
+ def fetch(key, options={})
38
+ source = DEFAULT_SOURCE
39
+ if options[:source]
40
+ source = options[:source].to_s
41
+ end
42
+ @lock.synchronize do
43
+ return nil unless @cache[key.to_s]
44
+ @cache[key.to_s][source]
45
+ end
46
+ end
47
+
48
+ # transfer all measurements to queue and reset internal status
22
49
  def flush_to(queue)
23
50
  counts = nil
24
51
  @lock.synchronize do
25
- counts = @cache.dup
26
- @cache.each_key { |key| @cache[key] = 0 }
52
+ # work off of a duplicate data set so we block for
53
+ # as little time as possible
54
+ counts = Marshal.load(Marshal.dump(@cache))
55
+ reset_cache
27
56
  end
28
- counts.each do |key, value|
29
- queue.add key => value
57
+ counts.each do |key, data|
58
+ data.each do |source, value|
59
+ if source == DEFAULT_SOURCE
60
+ queue.add key => value
61
+ else
62
+ queue.add key => {:value => value, :source => source}
63
+ end
64
+ end
30
65
  end
31
66
  end
32
67
 
33
- def increment(counter, by=1)
68
+ # Increment a given metric
69
+ #
70
+ # @example Increment metric 'foo' by 1
71
+ # increment :foo
72
+ #
73
+ # @example Increment metric 'bar' by 2
74
+ # increment :bar, :by => 2
75
+ #
76
+ # @example Increment metric 'foo' by 1 with a custom source
77
+ # increment :foo, :source => user.id
78
+ #
79
+ def increment(counter, options={})
34
80
  counter = counter.to_s
81
+ if options.is_a?(Fixnum)
82
+ # suppport legacy style
83
+ options = {:by => options}
84
+ end
85
+ by = options[:by] || 1
86
+ source = DEFAULT_SOURCE
87
+ if options[:source]
88
+ source = options[:source].to_s
89
+ end
90
+ if options[:sporadic]
91
+ make_sporadic(counter, source)
92
+ end
35
93
  @lock.synchronize do
36
- @cache[counter] ||= 0
37
- @cache[counter] += by
94
+ @cache[counter] ||= {}
95
+ @cache[counter][source] ||= 0
96
+ @cache[counter][source] += by
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def make_sporadic(metric, source)
103
+ @sporadics[metric] ||= Set.new
104
+ @sporadics[metric] << source
105
+ end
106
+
107
+ def reset_cache
108
+ # remove any source/metric pairs that aren't continuous
109
+ @sporadics.each do |key, sources|
110
+ sources.each { |source| @cache[key].delete(source) }
111
+ end
112
+ @sporadics.clear
113
+ # reset all continuous source/metric pairs to 0
114
+ @cache.each_key do |key|
115
+ @cache[key].each_key { |source| @cache[key][source] = 0 }
38
116
  end
39
117
  end
40
118
 
@@ -8,9 +8,7 @@ module Librato
8
8
  initializer 'librato_rails.setup' do |app|
9
9
  # don't start in test mode or in the console
10
10
  unless ::Rails.env.test? || defined?(::Rails::Console)
11
- Librato::Rails.setup
12
-
13
- app.middleware.use Librato::Rack::Middleware
11
+ Librato::Rails.setup(app)
14
12
  end
15
13
  end
16
14
  end
@@ -1,5 +1,5 @@
1
1
  module Librato
2
2
  module Rails
3
- VERSION = "0.5.2"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
data/lib/librato/rails.rb CHANGED
@@ -7,6 +7,7 @@ require 'librato/metrics'
7
7
 
8
8
  require 'librato/rack'
9
9
  require 'librato/rails/aggregator'
10
+ require 'librato/rails/collector'
10
11
  require 'librato/rails/counter_cache'
11
12
  require 'librato/rails/group'
12
13
  require 'librato/rails/worker'
@@ -28,33 +29,23 @@ module Librato
28
29
  mattr_accessor :user
29
30
  mattr_accessor :token
30
31
  mattr_accessor :flush_interval
31
- mattr_accessor :prefix
32
32
  mattr_accessor :source_pids
33
33
 
34
34
  # config defaults
35
35
  self.flush_interval = 60 # seconds
36
- self.source_pids = true
36
+ self.source_pids = false # append process id to the source?
37
37
 
38
- def_delegators :counters, :increment
39
- def_delegators :aggregate, :measure, :timing
38
+ # a collector instance handles all measurement addition/storage
39
+ def_delegators :collector, :aggregate, :counters, :delete_all, :group, :increment,
40
+ :measure, :prefix, :prefix=, :timing
40
41
 
41
42
  class << self
42
43
 
43
- # access to internal aggregator object
44
- def aggregate
45
- @aggregator_cache ||= Aggregator.new(:prefix => self.prefix)
46
- end
47
-
48
44
  # set custom api endpoint
49
45
  def api_endpoint=(endpoint)
50
46
  @api_endpoint = endpoint
51
47
  end
52
48
 
53
- # access to client instance
54
- def client
55
- @client ||= prepare_client
56
- end
57
-
58
49
  # detect / update configuration
59
50
  def check_config
60
51
  if self.config_file && File.exists?(self.config_file)
@@ -80,15 +71,14 @@ module Librato
80
71
  end
81
72
  end
82
73
 
83
- # access to internal counters object
84
- def counters
85
- @counter_cache ||= CounterCache.new
74
+ # access to client instance
75
+ def client
76
+ @client ||= prepare_client
86
77
  end
87
-
88
- # remove any accumulated but unsent metrics
89
- def delete_all
90
- aggregate.delete_all
91
- counters.delete_all
78
+
79
+ # collector instance which is tracking all measurement additions
80
+ def collector
81
+ @collector ||= Collector.new
92
82
  end
93
83
 
94
84
  # send all current data to Metrics
@@ -101,12 +91,7 @@ module Librato
101
91
  logger.debug queue.queued
102
92
  queue.submit unless queue.empty?
103
93
  rescue Exception => error
104
- logger.error "[librato-rails] submission failed permanently, worker exiting: #{error}"
105
- end
106
-
107
- def group(prefix)
108
- group = Group.new(prefix)
109
- yield group
94
+ logger.error "[librato-rails] submission failed permanently: #{error}"
110
95
  end
111
96
 
112
97
  def logger
@@ -119,16 +104,13 @@ module Librato
119
104
  end
120
105
 
121
106
  # run once during Rails startup sequence
122
- def setup
107
+ def setup(app)
123
108
  check_config
124
- # return unless self.email && self.api_key
109
+ return unless credentials_present?
125
110
  logger.info "[librato-rails] starting up with #{app_server}..."
126
111
  @pid = $$
127
- if forking_server?
128
- install_worker_check
129
- else
130
- start_worker # start immediately
131
- end
112
+ app.middleware.use Librato::Rack::Middleware
113
+ start_worker unless forking_server?
132
114
  end
133
115
 
134
116
  def source
@@ -169,6 +151,10 @@ module Librato
169
151
  :other
170
152
  end
171
153
  end
154
+
155
+ def credentials_present?
156
+ self.user && self.token
157
+ end
172
158
 
173
159
  def forking_server?
174
160
  FORKING_SERVERS.include?(app_server)