librato-rails 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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)