librato-rack 0.1.0

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