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.
- data/LICENSE +24 -0
- data/README.md +176 -0
- data/Rakefile +25 -0
- data/lib/librato-rack.rb +1 -0
- data/lib/librato/collector.rb +48 -0
- data/lib/librato/collector/aggregator.rb +97 -0
- data/lib/librato/collector/counter_cache.rb +113 -0
- data/lib/librato/collector/group.rb +29 -0
- data/lib/librato/rack.rb +116 -0
- data/lib/librato/rack/configuration.rb +61 -0
- data/lib/librato/rack/errors.rb +7 -0
- data/lib/librato/rack/logger.rb +71 -0
- data/lib/librato/rack/tracker.rb +137 -0
- data/lib/librato/rack/validating_queue.rb +41 -0
- data/lib/librato/rack/version.rb +5 -0
- data/lib/librato/rack/worker.rb +49 -0
- data/test/apps/basic.ru +20 -0
- data/test/apps/custom.ru +27 -0
- data/test/apps/heroku.ru +27 -0
- data/test/integration/custom_test.rb +59 -0
- data/test/integration/heroku_test.rb +36 -0
- data/test/integration/request_test.rb +69 -0
- data/test/remote/tracker_test.rb +198 -0
- data/test/test_helper.rb +22 -0
- data/test/unit/collector/aggregator_test.rb +69 -0
- data/test/unit/collector/counter_cache_test.rb +96 -0
- data/test/unit/collector/group_test.rb +53 -0
- data/test/unit/collector_test.rb +23 -0
- data/test/unit/rack/configuration_test.rb +86 -0
- data/test/unit/rack/logger_test.rb +91 -0
- data/test/unit/rack/tracker_test.rb +44 -0
- data/test/unit/rack/worker_test.rb +36 -0
- metadata +132 -0
@@ -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
|
data/lib/librato/rack.rb
ADDED
@@ -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,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
|