metrician 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rubocop.yml +65 -0
- data/.rubocop_todo.yml +24 -0
- data/.ruby-version +1 -0
- data/.travis.yml +36 -0
- data/Gemfile +1 -0
- data/METRICS.MD +48 -0
- data/README.md +77 -0
- data/Rakefile +12 -0
- data/config/metrician.yaml +136 -0
- data/gemfiles/Gemfile.4.2.sidekiq-4 +24 -0
- data/gemfiles/Gemfile.4.2.sidekiq-4.lock +173 -0
- data/gemfiles/Gemfile.5.0.pg +24 -0
- data/gemfiles/Gemfile.5.0.pg.lock +182 -0
- data/lib/metrician.rb +80 -0
- data/lib/metrician/configuration.rb +33 -0
- data/lib/metrician/jobs.rb +32 -0
- data/lib/metrician/jobs/delayed_job_callbacks.rb +33 -0
- data/lib/metrician/jobs/resque_plugin.rb +36 -0
- data/lib/metrician/jobs/sidekiq_middleware.rb +28 -0
- data/lib/metrician/middleware.rb +64 -0
- data/lib/metrician/middleware/application_timing.rb +29 -0
- data/lib/metrician/middleware/request_timing.rb +152 -0
- data/lib/metrician/reporter.rb +41 -0
- data/lib/metrician/reporters/active_record.rb +63 -0
- data/lib/metrician/reporters/delayed_job.rb +17 -0
- data/lib/metrician/reporters/honeybadger.rb +26 -0
- data/lib/metrician/reporters/memcache.rb +49 -0
- data/lib/metrician/reporters/method_tracer.rb +70 -0
- data/lib/metrician/reporters/middleware.rb +22 -0
- data/lib/metrician/reporters/net_http.rb +28 -0
- data/lib/metrician/reporters/redis.rb +31 -0
- data/lib/metrician/reporters/resque.rb +17 -0
- data/lib/metrician/reporters/sidekiq.rb +19 -0
- data/lib/metrician/version.rb +5 -0
- data/metrician.gemspec +25 -0
- data/script/setup +72 -0
- data/script/test +36 -0
- data/spec/metrician_spec.rb +372 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/database.rb +33 -0
- data/spec/support/database.sample.yml +10 -0
- data/spec/support/database.travis.yml +9 -0
- data/spec/support/models.rb +2 -0
- data/spec/support/test_delayed_job.rb +12 -0
- data/spec/support/test_resque_job.rb +8 -0
- data/spec/support/test_sidekiq_worker.rb +8 -0
- metadata +188 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
module Metrician
|
2
|
+
|
3
|
+
class ActiveRecord < Reporter
|
4
|
+
|
5
|
+
def self.enabled?
|
6
|
+
!!defined?(::ActiveRecord) &&
|
7
|
+
Metrician.configuration[:database][:enabled]
|
8
|
+
end
|
9
|
+
|
10
|
+
def instrument
|
11
|
+
::ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
|
12
|
+
include QueryInterceptor
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
module QueryInterceptor
|
19
|
+
|
20
|
+
COMMAND_EXP = /^(select|update|insert|delete|show|begin|commit|rollback|describe)/i
|
21
|
+
SQL_EXP = /#{COMMAND_EXP} (?:into |from |.+? from )?(?:[`"]([a-z_]+)[`"])?/i
|
22
|
+
OTHER = "other".freeze
|
23
|
+
|
24
|
+
def self.included(instrumented_class)
|
25
|
+
return if instrumented_class.method_defined?(:log_without_metrician)
|
26
|
+
instrumented_class.class_eval do
|
27
|
+
alias_method :log_without_metrician, :log
|
28
|
+
alias_method :log, :log_with_metrician
|
29
|
+
protected :log
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def log_with_metrician(*args, &block)
|
34
|
+
start_time = Time.now.to_f
|
35
|
+
sql, name, _binds = args
|
36
|
+
sql = sql.dup.force_encoding(Encoding::BINARY)
|
37
|
+
config = Metrician.configuration[:database]
|
38
|
+
metrics = []
|
39
|
+
metrics << "database.query" if config[:query][:enabled]
|
40
|
+
if config[:command] || config[:table]
|
41
|
+
command, table = parse_sql(sql)
|
42
|
+
metrics << "database.#{command}" if config[:command] && command
|
43
|
+
metrics << "database.#{table}" if config[:table] && table
|
44
|
+
metrics << "database.#{command}.#{table}" if config[:command] && config[:table] && command && table
|
45
|
+
end
|
46
|
+
begin
|
47
|
+
log_without_metrician(*args, &block)
|
48
|
+
ensure
|
49
|
+
duration = Time.now.to_f - start_time
|
50
|
+
metrics.each { |m| Metrician.gauge(m, duration) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_sql(sql)
|
55
|
+
match = sql.match(SQL_EXP)
|
56
|
+
command = (match && match[1].downcase) || OTHER
|
57
|
+
table = (match && match[2] && match[2].downcase)
|
58
|
+
[command, table]
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Metrician
|
2
|
+
class DelayedJob < Reporter
|
3
|
+
|
4
|
+
def self.enabled?
|
5
|
+
!!defined?(::Delayed::Worker) &&
|
6
|
+
Metrician::Jobs.enabled?
|
7
|
+
end
|
8
|
+
|
9
|
+
def instrument
|
10
|
+
require "metrician/jobs/delayed_job_callbacks"
|
11
|
+
unless ::Delayed::Worker.plugins.include?(::Metrician::Jobs::DelayedJobCallbacks)
|
12
|
+
::Delayed::Worker.plugins << ::Metrician::Jobs::DelayedJobCallbacks
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Metrician
|
2
|
+
class Honeybadger < Reporter
|
3
|
+
|
4
|
+
def self.enabled?
|
5
|
+
!!defined?(::Honeybadger) &&
|
6
|
+
Metrician.configuration[:exception][:enabled]
|
7
|
+
end
|
8
|
+
|
9
|
+
def instrument
|
10
|
+
::Honeybadger::Agent.class_eval do
|
11
|
+
def notify_with_metrician(exception, options = {})
|
12
|
+
# We can differentiate whether or not we live inside a web
|
13
|
+
# request or not by determining the nil-ness of:
|
14
|
+
# context_manager.get_rack_env
|
15
|
+
notify_without_metrician(exception, options)
|
16
|
+
ensure
|
17
|
+
Metrician.increment("exception.raise") if Metrician.configuration[:exception][:raise][:enabled]
|
18
|
+
Metrician.increment("exception.raise.#{Metrician.dotify(exception.class.name.underscore)}") if exception && Metrician.configuration[:exception][:exception_specific][:enabled]
|
19
|
+
end
|
20
|
+
alias_method :notify_without_metrician, :notify
|
21
|
+
alias_method :notify, :notify_with_metrician
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Metrician
|
2
|
+
class Memcache < Reporter
|
3
|
+
|
4
|
+
METHODS = %i[get delete cas prepend append replace decrement increment add set].freeze
|
5
|
+
|
6
|
+
def self.memcached_gem?
|
7
|
+
!!defined?(::Memcached)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.dalli_gem?
|
11
|
+
!!defined?(::Dalli) && !!defined?(::Dalli::Client)
|
12
|
+
end
|
13
|
+
|
14
|
+
def client_class
|
15
|
+
if self.class.memcached_gem?
|
16
|
+
Memcached
|
17
|
+
elsif self.class.dalli_gem?
|
18
|
+
Dalli::Client
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.enabled?
|
23
|
+
(memcached_gem? || dalli_gem?) &&
|
24
|
+
Metrician.configuration[:cache][:enabled]
|
25
|
+
end
|
26
|
+
|
27
|
+
def instrument
|
28
|
+
return if client_class.method_defined?(:get_with_metrician_trace)
|
29
|
+
METHODS.each do |method_name|
|
30
|
+
next unless client_class.method_defined?(method_name)
|
31
|
+
client_class.class_eval <<-EOD
|
32
|
+
def #{method_name}_with_metrician_trace(*args, &blk)
|
33
|
+
start_time = Time.now
|
34
|
+
begin
|
35
|
+
#{method_name}_without_metrician_trace(*args, &blk)
|
36
|
+
ensure
|
37
|
+
duration = (Time.now - start_time).to_f
|
38
|
+
Metrician.gauge("cache.command", duration) if Metrician.configuration[:cache][:command][:enabled]
|
39
|
+
Metrician.gauge("cache.command.#{method_name}", duration) if Metrician.configuration[:cache][:command_specific][:enabled]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
alias #{method_name}_without_metrician_trace #{method_name}
|
43
|
+
alias #{method_name} #{method_name}_with_metrician_trace
|
44
|
+
EOD
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Metrician
|
2
|
+
|
3
|
+
class MethodTracer < Reporter
|
4
|
+
|
5
|
+
def self.enabled?
|
6
|
+
Metrician.configuration[:method_tracer][:enabled]
|
7
|
+
end
|
8
|
+
|
9
|
+
def instrument
|
10
|
+
Module.send(:include, TracingMethodInterceptor)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
module TracingMethodInterceptor
|
16
|
+
|
17
|
+
def self.default_metric_name(klass, is_klass_method, method_name)
|
18
|
+
name = klass.name.underscore
|
19
|
+
name = "#{name}.self" if is_klass_method
|
20
|
+
"tracer.#{name}.#{method_name}".downcase.tr_s("^a-zA-Z0-9.", "_")
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.traceable_method?(klass, method_name)
|
24
|
+
klass.method_defined?(method_name) ||
|
25
|
+
klass.private_method_defined?(method_name) ||
|
26
|
+
klass.methods.include?(method_name.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.already_traced_method?(klass, is_klass_method, traced_name)
|
30
|
+
is_klass_method ?
|
31
|
+
klass.methods.include?(traced_name) :
|
32
|
+
klass.method_defined?(traced_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.code_to_eval(is_klass_method, method_name, traced_name, untraced_name, metric_name)
|
36
|
+
<<-EOD
|
37
|
+
#{'class << self' if is_klass_method}
|
38
|
+
def #{traced_name}(*args, &block)
|
39
|
+
start_time = Time.now
|
40
|
+
begin
|
41
|
+
#{untraced_name}(*args, &block)
|
42
|
+
ensure
|
43
|
+
Metrician.gauge("#{metric_name}", (Time.now - start_time).to_f)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
alias :#{untraced_name} :#{method_name}
|
47
|
+
alias :#{method_name} :#{traced_name}
|
48
|
+
#{'end' if is_klass_method}
|
49
|
+
EOD
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_metrician_method_tracer(method_name, metric_name = nil)
|
53
|
+
return false unless TracingMethodInterceptor.traceable_method?(self, method_name)
|
54
|
+
|
55
|
+
is_klass_method = methods.include?(method_name.to_s)
|
56
|
+
traced_name = "with_metrician_trace_#{method_name}"
|
57
|
+
return false if TracingMethodInterceptor.already_traced_method?(self, is_klass_method, traced_name)
|
58
|
+
|
59
|
+
metric_name ||= TracingMethodInterceptor.default_metric_name(self, is_klass_method, method_name)
|
60
|
+
untraced_name = "without_metrician_trace_#{method_name}"
|
61
|
+
|
62
|
+
traced_method_code =
|
63
|
+
TracingMethodInterceptor.code_to_eval(is_klass_method, method_name, traced_name,
|
64
|
+
untraced_name, metric_name)
|
65
|
+
class_eval(traced_method_code, __FILE__, __LINE__)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Metrician
|
2
|
+
module Reporters
|
3
|
+
class Middleware < Reporter
|
4
|
+
def self.enabled?
|
5
|
+
defined?(Rails) &&
|
6
|
+
Metrician::Middleware.enabled?
|
7
|
+
end
|
8
|
+
|
9
|
+
def instrument
|
10
|
+
require "metrician/middleware/request_timing"
|
11
|
+
require "metrician/middleware/application_timing"
|
12
|
+
|
13
|
+
app = Rails.application
|
14
|
+
return if app.nil?
|
15
|
+
|
16
|
+
app.middleware.insert_before(0, Metrician::Middleware::RequestTiming)
|
17
|
+
app.middleware.insert_after(Metrician::Middleware::RequestTiming, Rack::ContentLength)
|
18
|
+
app.middleware.use(Metrician::Middleware::ApplicationTiming)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "net/http"
|
2
|
+
|
3
|
+
module Metrician
|
4
|
+
class NetHttp < Reporter
|
5
|
+
|
6
|
+
def self.enabled?
|
7
|
+
!!defined?(Net::HTTP) &&
|
8
|
+
Metrician.configuration[:external_service][:enabled]
|
9
|
+
end
|
10
|
+
|
11
|
+
def instrument
|
12
|
+
return if ::Net::HTTP.method_defined?(:request_with_metrician_trace)
|
13
|
+
::Net::HTTP.class_eval do
|
14
|
+
def request_with_metrician_trace(req, body = nil, &block)
|
15
|
+
start_time = Time.now
|
16
|
+
begin
|
17
|
+
request_without_metrician_trace(req, body, &block)
|
18
|
+
ensure
|
19
|
+
Metrician.gauge("service.request", (Time.now - start_time).to_f) if Metrician.configuration[:external_service][:request][:enabled]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
alias_method :request_without_metrician_trace, :request
|
23
|
+
alias_method :request, :request_with_metrician_trace
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Metrician
|
2
|
+
class Redis < Reporter
|
3
|
+
|
4
|
+
def self.enabled?
|
5
|
+
!!defined?(::Redis) &&
|
6
|
+
Metrician.configuration[:cache][:enabled]
|
7
|
+
end
|
8
|
+
|
9
|
+
def instrument
|
10
|
+
return if ::Redis::Client.method_defined?(:call_with_metrician_trace)
|
11
|
+
::Redis::Client.class_eval do
|
12
|
+
def call_with_metrician_trace(*args, &blk)
|
13
|
+
start_time = Time.now
|
14
|
+
begin
|
15
|
+
call_without_metrician_trace(*args, &blk)
|
16
|
+
ensure
|
17
|
+
duration = (Time.now - start_time).to_f
|
18
|
+
Metrician.gauge("cache.command", duration) if Metrician.configuration[:cache][:command][:enabled]
|
19
|
+
if Metrician.configuration[:cache][:command_specific][:enabled]
|
20
|
+
method_name = args[0].is_a?(Array) ? args[0][0] : args[0]
|
21
|
+
Metrician.gauge("cache.command.#{method_name}", duration)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
alias_method :call_without_metrician_trace, :call
|
26
|
+
alias_method :call, :call_with_metrician_trace
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Metrician
|
2
|
+
class Resque < Reporter
|
3
|
+
|
4
|
+
def self.enabled?
|
5
|
+
!!defined?(::Resque) &&
|
6
|
+
Metrician::Jobs.enabled?
|
7
|
+
end
|
8
|
+
|
9
|
+
def instrument
|
10
|
+
require "metrician/jobs/resque_plugin"
|
11
|
+
unless ::Resque::Job.respond_to?(:around_perform_with_metrician)
|
12
|
+
::Resque::Job.send(:extend, Metrician::Jobs::ResquePlugin)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Metrician
|
2
|
+
class Sidekiq < Reporter
|
3
|
+
|
4
|
+
def self.enabled?
|
5
|
+
!!defined?(::Sidekiq) &&
|
6
|
+
Metrician::Jobs.enabled?
|
7
|
+
end
|
8
|
+
|
9
|
+
def instrument
|
10
|
+
require "metrician/jobs/sidekiq_middleware"
|
11
|
+
::Sidekiq.configure_server do |config|
|
12
|
+
config.server_middleware do |chain|
|
13
|
+
chain.add ::Metrician::Jobs::SidekiqMiddleware
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/metrician.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "metrician/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "metrician"
|
6
|
+
s.version = Metrician::VERSION
|
7
|
+
s.authors = ["Expected Behavior"]
|
8
|
+
s.email = ["support@instrumentalapp.com"]
|
9
|
+
s.homepage = "http://instrumentalapp.com/"
|
10
|
+
s.summary = "Automatic Application Metric Collection for Ruby Applications"
|
11
|
+
s.description = "Automatically report the most important metrics about your ruby application, from request timing to job execution."
|
12
|
+
s.license = "MIT"
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
17
|
+
s.require_paths = %w[lib app]
|
18
|
+
|
19
|
+
s.add_development_dependency("instrumental_agent", "~> 0")
|
20
|
+
s.add_development_dependency("rubocop", "~> 0")
|
21
|
+
s.add_development_dependency("bundler", "~> 1.14")
|
22
|
+
s.add_development_dependency("rake", "~> 10.0")
|
23
|
+
s.add_development_dependency("rspec", "~> 3.0")
|
24
|
+
s.add_development_dependency("byebug", "~> 0")
|
25
|
+
end
|
data/script/setup
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -e
|
3
|
+
cd "$(dirname "$0")/.."
|
4
|
+
HOMEBREW_PREFIX=$(brew --config | grep HOMEBREW_PREFIX | sed 's/HOMEBREW_PREFIX: //')
|
5
|
+
|
6
|
+
brew list redis > /dev/null 2>&1 || (
|
7
|
+
brew install redis
|
8
|
+
brew services restart redis
|
9
|
+
)
|
10
|
+
|
11
|
+
brew list mysql > /dev/null 2>&1 || (
|
12
|
+
brew install mysql
|
13
|
+
brew services start mysql
|
14
|
+
)
|
15
|
+
|
16
|
+
brew list postgresql > /dev/null 2>&1 || brew install postgresql
|
17
|
+
|
18
|
+
# TODO: Install all rubies in .travis
|
19
|
+
rbenv which ruby >/dev/null 2>&1 || (brew upgrade ruby-build || true; rbenv install)
|
20
|
+
|
21
|
+
# Setup rbenv so we can switch rubies below
|
22
|
+
eval "$(rbenv init - --no-rehash)"
|
23
|
+
|
24
|
+
if [ ! -e Gemfile ]; then
|
25
|
+
tput bold # bold text
|
26
|
+
tput setaf 2 # red text
|
27
|
+
echo "Gemfile symlink is broken. Should point to a known good Gemfile-x variant in ./gemfiles"
|
28
|
+
tput sgr0 # reset to default text
|
29
|
+
exit 199
|
30
|
+
elif [ ! -e Gemfile.lock ]; then
|
31
|
+
tput bold # bold text
|
32
|
+
tput setaf 2 # red text
|
33
|
+
echo "Gemfile.lock symlink is broken. Should point to a known good Gemfile-x.lock variant in ./gemfiles"
|
34
|
+
tput sgr0 # reset to default text
|
35
|
+
exit 200
|
36
|
+
fi
|
37
|
+
|
38
|
+
for ruby_version in `ruby -ryaml -e 'puts YAML.load(File.read(".travis.yml"))["rvm"].join(" ")'`; do
|
39
|
+
rbenv versions --bare | grep "^${ruby_version}$" || rbenv install $ruby_version
|
40
|
+
rbenv shell $ruby_version
|
41
|
+
|
42
|
+
gem list -i bundler >/dev/null || gem install bundler
|
43
|
+
|
44
|
+
gem list -i gemika >/dev/null || gem install gemika
|
45
|
+
|
46
|
+
rake matrix:install
|
47
|
+
done
|
48
|
+
|
49
|
+
if ! psql postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'metrician_test'" | grep -q 1; then
|
50
|
+
psql -c 'create database metrician_test;' postgres
|
51
|
+
fi
|
52
|
+
mysql -e 'create database IF NOT EXISTS metrician_test;' -u root
|
53
|
+
|
54
|
+
if [ ! -f spec/support/database.yml ]; then
|
55
|
+
tput bold # bold text
|
56
|
+
tput setaf 2 # red text
|
57
|
+
echo "spec/support/database.yml doesn't exist, run:"
|
58
|
+
echo " cp spec/support/database.sample.yml spec/support/database.yml"
|
59
|
+
echo
|
60
|
+
echo "and update the credentials to match your system"
|
61
|
+
tput sgr0 # reset to default text
|
62
|
+
else
|
63
|
+
tput bold # bold text
|
64
|
+
tput setaf 2 # green text
|
65
|
+
echo "****************************************************************"
|
66
|
+
echo "* *"
|
67
|
+
echo "* Good to go! *"
|
68
|
+
echo "* *"
|
69
|
+
echo "****************************************************************"
|
70
|
+
tput sgr0 # reset to default text
|
71
|
+
fi
|
72
|
+
|