librato-rack 0.1.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.
@@ -0,0 +1,29 @@
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, by=1)
17
+ counter = "#{@prefix}#{counter}"
18
+ @collector.increment counter, by
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
@@ -0,0 +1,116 @@
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
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
+ class Rack
31
+ attr_reader :config, :tracker
32
+
33
+ def initialize(app, config = Configuration.new)
34
+ @app, @config = app, config
35
+ @tracker = Tracker.new(@config)
36
+ Librato.register_tracker(@tracker) # create global reference
37
+ end
38
+
39
+ def call(env)
40
+ check_log_output(env)
41
+ @tracker.check_worker
42
+ record_header_metrics(env)
43
+ response, duration = process_request(env)
44
+ record_request_metrics(response.first, duration)
45
+ response
46
+ end
47
+
48
+ private
49
+
50
+ def check_log_output(env)
51
+ return if @log_target
52
+ if env.keys.include?('HTTP_X_HEROKU_QUEUE_DEPTH') # on heroku
53
+ tracker.on_heroku = true
54
+ default = ::Logger.new($stdout)
55
+ else
56
+ default = env['rack.errors'] || $stderr
57
+ end
58
+ config.log_target ||= default
59
+ @log_target = config.log_target
60
+ end
61
+
62
+ def process_request(env)
63
+ time = Time.now
64
+ begin
65
+ response = @app.call(env)
66
+ rescue Exception => e
67
+ record_exception(e)
68
+ raise
69
+ end
70
+ duration = (Time.now - time) * 1000.0
71
+ [response, duration]
72
+ end
73
+
74
+ def record_header_metrics(env)
75
+ return unless env.keys.include?('HTTP_X_HEROKU_QUEUE_DEPTH')
76
+
77
+ tracker.group 'rack.heroku' do |group|
78
+ group.group 'queue' do |q|
79
+ q.measure 'depth', env['HTTP_X_HEROKU_QUEUE_DEPTH'].to_f
80
+ q.timing 'wait_time', env['HTTP_X_HEROKU_QUEUE_WAIT_TIME'].to_f
81
+ end
82
+ group.measure 'dynos', env['HTTP_X_HEROKU_DYNOS_IN_USE'].to_f
83
+ end
84
+ end
85
+
86
+ def record_request_metrics(status, duration)
87
+ tracker.group 'rack.request' do |group|
88
+ group.increment 'total'
89
+ group.timing 'time', duration
90
+ group.increment 'slow' if duration > 200.0
91
+
92
+ group.group 'status' do |s|
93
+ s.increment status
94
+ s.increment "#{status.to_s[0]}xx"
95
+
96
+ s.timing "#{status}.time", duration
97
+ s.timing "#{status.to_s[0]}xx.time", duration
98
+ end
99
+ end
100
+ end
101
+
102
+ def record_exception(exception)
103
+ tracker.increment 'rack.request.exceptions'
104
+ end
105
+
106
+ end
107
+ end
108
+
109
+ require 'librato/collector'
110
+ require 'librato/rack/configuration'
111
+ require 'librato/rack/errors'
112
+ require 'librato/rack/logger'
113
+ require 'librato/rack/tracker'
114
+ require 'librato/rack/validating_queue'
115
+ require 'librato/rack/version'
116
+ require 'librato/rack/worker'
@@ -0,0 +1,61 @@
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
+ attr_accessor :user, :token, :api_endpoint, :tracker, :source_pids,
14
+ :log_level, :flush_interval, :log_target
15
+ attr_reader :prefix, :source
16
+
17
+ def initialize
18
+ # set up defaults
19
+ self.tracker = nil
20
+ self.api_endpoint = Librato::Metrics.api_endpoint
21
+ self.flush_interval = 60
22
+ self.source_pids = false
23
+ @listeners = []
24
+
25
+ # check environment
26
+ self.user = ENV['LIBRATO_USER'] || ENV['LIBRATO_METRICS_USER']
27
+ self.token = ENV['LIBRATO_TOKEN'] || ENV['LIBRATO_METRICS_TOKEN']
28
+ self.prefix = ENV['LIBRATO_PREFIX'] || ENV['LIBRATO_METRICS_PREFIX']
29
+ self.source = ENV['LIBRATO_SOURCE'] || ENV['LIBRATO_METRICS_SOURCE']
30
+ self.log_level = ENV['LIBRATO_LOG_LEVEL'] || :info
31
+ end
32
+
33
+ def explicit_source?
34
+ !!@explicit_source
35
+ end
36
+
37
+ def prefix=(prefix)
38
+ @prefix = prefix
39
+ @listeners.each { |l| l.prefix = prefix }
40
+ end
41
+
42
+ def register_listener(listener)
43
+ @listeners << listener
44
+ end
45
+
46
+ def source=(src)
47
+ @source = src
48
+ @explicit_source = !!@source
49
+ end
50
+
51
+ def dump
52
+ fields = {}
53
+ %w{user token log_level source prefix flush_interval source_pids}.each do |field|
54
+ fields[field.to_sym] = self.send(field)
55
+ end
56
+ fields
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ module Librato
2
+ class Rack
3
+
4
+ class InvalidLogLevel < RuntimeError; end
5
+
6
+ end
7
+ end
@@ -0,0 +1,71 @@
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 :logger, :prefix
12
+
13
+ def initialize(logger)
14
+ self.logger = logger
15
+ self.prefix = '[librato-rack] '
16
+ end
17
+
18
+ # @example Simple logging
19
+ # log :debug, 'this is a debug message'
20
+ #
21
+ # @example Block logging - not executed if won't be logged
22
+ # log(:debug) { "found #{thingy} at #{place}" }
23
+ #
24
+ def log(level, message=nil, &block)
25
+ return unless should_log?(level)
26
+ message = prefix + (message || block.call)
27
+ if logger.respond_to?(:puts) # io obj
28
+ logger.puts(message)
29
+ elsif logger.respond_to?(:error) # logger obj
30
+ log_to_logger(level, message)
31
+ else
32
+ raise "invalid logger object"
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
+ private
52
+
53
+ # write message to an ruby stdlib logger object or another class with
54
+ # similar interface, respecting log levels when we can map them
55
+ def log_to_logger(level, message)
56
+ case level
57
+ when :error, :warn
58
+ method = level
59
+ else
60
+ method = :info
61
+ end
62
+ logger.send(method, message)
63
+ end
64
+
65
+ def should_log?(level)
66
+ LOG_LEVELS.index(self.log_level) >= LOG_LEVELS.index(level)
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,137 @@
1
+ require 'socket'
2
+
3
+ module Librato
4
+ class Rack
5
+ class Tracker
6
+ extend Forwardable
7
+
8
+ SOURCE_REGEX = /\A[-:A-Za-z0-9_.]{1,255}\z/
9
+
10
+ def_delegators :collector, :increment, :measure, :timing, :group
11
+ def_delegators :logger, :log
12
+
13
+ attr_reader :config
14
+ attr_accessor :on_heroku
15
+
16
+ def initialize(config)
17
+ @config = config
18
+ collector.prefix = config.prefix
19
+ config.register_listener(collector)
20
+ end
21
+
22
+ # start worker thread, one per process.
23
+ # if this process has been forked from an one with an active
24
+ # worker thread we don't need to worry about cleanup, the worker
25
+ # thread will not pass with the fork
26
+ def check_worker
27
+ return if @worker # already running
28
+ return if !should_start?
29
+ log(:debug) { "config: #{config.dump}" }
30
+ @pid = $$
31
+ log(:debug) { ">> starting up worker for pid #{@pid}..." }
32
+ @worker = Thread.new do
33
+ worker = Worker.new
34
+ worker.run_periodically(config.flush_interval) do
35
+ flush
36
+ end
37
+ end
38
+ end
39
+
40
+ # primary collector object used by this tracker
41
+ def collector
42
+ @collector ||= Librato::Collector.new
43
+ end
44
+
45
+ # send all current data to Metrics
46
+ def flush
47
+ log :debug, "flushing pid #{@pid} (#{Time.now}).."
48
+ start = Time.now
49
+ # thread safety is handled internally for stores
50
+ queue = build_flush_queue(collector)
51
+ queue.submit unless queue.empty?
52
+ log(:trace) { "flushed pid #{@pid} in #{(Time.now - start)*1000.to_f}ms" }
53
+ rescue Exception => error
54
+ log :error, "submission failed permanently: #{error}"
55
+ end
56
+
57
+ # source including process pid if indicated
58
+ def qualified_source
59
+ config.source_pids ? "#{source}.#{$$}" : source
60
+ end
61
+
62
+ private
63
+
64
+ # access to client instance
65
+ def client
66
+ @client ||= prepare_client
67
+ end
68
+
69
+ def build_flush_queue(collector)
70
+ queue = ValidatingQueue.new( :client => client, :source => qualified_source,
71
+ :prefix => config.prefix, :skip_measurement_times => true )
72
+ [collector.counters, collector.aggregate].each do |cache|
73
+ cache.flush_to(queue)
74
+ end
75
+ queue.add 'rack.processes' => 1
76
+ trace_queued(queue.queued) #if should_log?(:trace)
77
+ queue
78
+ end
79
+
80
+ # trace metrics being sent
81
+ def trace_queued(queued)
82
+ require 'pp'
83
+ log(:trace) { "Queued: " + queued.pretty_inspect }
84
+ end
85
+
86
+ def logger
87
+ return @logger if @logger
88
+ @logger = Logger.new(config.log_target)
89
+ @logger.log_level = config.log_level
90
+ @logger
91
+ end
92
+
93
+ def prepare_client
94
+ client = Librato::Metrics::Client.new
95
+ client.authenticate config.user, config.token
96
+ client.api_endpoint = config.api_endpoint
97
+ client.custom_user_agent = user_agent
98
+ client
99
+ end
100
+
101
+ def ruby_engine
102
+ return RUBY_ENGINE if Object.constants.include?(:RUBY_ENGINE)
103
+ RUBY_DESCRIPTION.split[0]
104
+ end
105
+
106
+ def should_start?
107
+ return false if @pid_checked == $$ # only check once per process
108
+ @pid_checked = $$
109
+ if !config.user || !config.token
110
+ # don't show this unless we're debugging, expected behavior
111
+ log :debug, 'halting: credentials not present.'
112
+ false
113
+ elsif qualified_source !~ SOURCE_REGEX
114
+ log :warn, "halting: '#{qualified_source}' is an invalid source name."
115
+ false
116
+ elsif on_heroku && !config.explicit_source?
117
+ log :warn, 'halting: source must be provided in configuration.'
118
+ false
119
+ else
120
+ true
121
+ end
122
+ end
123
+
124
+ def source
125
+ @source ||= (config.source || Socket.gethostname).downcase
126
+ end
127
+
128
+ def user_agent
129
+ ua_chunks = []
130
+ ua_chunks << "librato-rack/#{Librato::Rack::VERSION}"
131
+ ua_chunks << "(#{ruby_engine}; #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}; #{RUBY_PLATFORM})"
132
+ ua_chunks.join(' ')
133
+ end
134
+
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,41 @@
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
+ METRIC_NAME_REGEX = /\A[-.:_\w]{1,255}\z/
9
+ SOURCE_NAME_REGEX = /\A[-:A-Za-z0-9_.]{1,255}\z/
10
+
11
+ attr_accessor :logger
12
+
13
+ # screen all measurements for validity before sending
14
+ def submit
15
+ @queued[:gauges].delete_if do |entry|
16
+ name = entry[:name].to_s
17
+ source = entry[:source] && entry[:source].to_s
18
+ if name !~ METRIC_NAME_REGEX
19
+ log :warn, "invalid metric name '#{name}', not sending."
20
+ true # delete
21
+ elsif source && source !~ SOURCE_NAME_REGEX
22
+ log :warn, "invalid source name '#{source}', not sending."
23
+ true # delete
24
+ else
25
+ false # preserve
26
+ end
27
+ end
28
+
29
+ super
30
+ end
31
+
32
+ private
33
+
34
+ def log(level, msg)
35
+ return unless logger
36
+ logger.log level, msg
37
+ end
38
+
39
+ end
40
+ end
41
+ end