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