metrician 0.0.1
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.
- 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
|
+
|