metrician 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +65 -0
  5. data/.rubocop_todo.yml +24 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +36 -0
  8. data/Gemfile +1 -0
  9. data/METRICS.MD +48 -0
  10. data/README.md +77 -0
  11. data/Rakefile +12 -0
  12. data/config/metrician.yaml +136 -0
  13. data/gemfiles/Gemfile.4.2.sidekiq-4 +24 -0
  14. data/gemfiles/Gemfile.4.2.sidekiq-4.lock +173 -0
  15. data/gemfiles/Gemfile.5.0.pg +24 -0
  16. data/gemfiles/Gemfile.5.0.pg.lock +182 -0
  17. data/lib/metrician.rb +80 -0
  18. data/lib/metrician/configuration.rb +33 -0
  19. data/lib/metrician/jobs.rb +32 -0
  20. data/lib/metrician/jobs/delayed_job_callbacks.rb +33 -0
  21. data/lib/metrician/jobs/resque_plugin.rb +36 -0
  22. data/lib/metrician/jobs/sidekiq_middleware.rb +28 -0
  23. data/lib/metrician/middleware.rb +64 -0
  24. data/lib/metrician/middleware/application_timing.rb +29 -0
  25. data/lib/metrician/middleware/request_timing.rb +152 -0
  26. data/lib/metrician/reporter.rb +41 -0
  27. data/lib/metrician/reporters/active_record.rb +63 -0
  28. data/lib/metrician/reporters/delayed_job.rb +17 -0
  29. data/lib/metrician/reporters/honeybadger.rb +26 -0
  30. data/lib/metrician/reporters/memcache.rb +49 -0
  31. data/lib/metrician/reporters/method_tracer.rb +70 -0
  32. data/lib/metrician/reporters/middleware.rb +22 -0
  33. data/lib/metrician/reporters/net_http.rb +28 -0
  34. data/lib/metrician/reporters/redis.rb +31 -0
  35. data/lib/metrician/reporters/resque.rb +17 -0
  36. data/lib/metrician/reporters/sidekiq.rb +19 -0
  37. data/lib/metrician/version.rb +5 -0
  38. data/metrician.gemspec +25 -0
  39. data/script/setup +72 -0
  40. data/script/test +36 -0
  41. data/spec/metrician_spec.rb +372 -0
  42. data/spec/spec_helper.rb +23 -0
  43. data/spec/support/database.rb +33 -0
  44. data/spec/support/database.sample.yml +10 -0
  45. data/spec/support/database.travis.yml +9 -0
  46. data/spec/support/models.rb +2 -0
  47. data/spec/support/test_delayed_job.rb +12 -0
  48. data/spec/support/test_resque_job.rb +8 -0
  49. data/spec/support/test_sidekiq_worker.rb +8 -0
  50. 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
@@ -0,0 +1,5 @@
1
+ module Metrician
2
+
3
+ VERSION = "0.0.1".freeze
4
+
5
+ end
@@ -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
@@ -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
+