stackify-ruby-apm 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +76 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +68 -0
  6. data/README.md +23 -0
  7. data/Rakefile +6 -0
  8. data/lib/stackify-ruby-apm.rb +4 -0
  9. data/lib/stackify/agent.rb +186 -0
  10. data/lib/stackify/config.rb +221 -0
  11. data/lib/stackify/context.rb +24 -0
  12. data/lib/stackify/context/request.rb +12 -0
  13. data/lib/stackify/context/request/socket.rb +21 -0
  14. data/lib/stackify/context/request/url.rb +44 -0
  15. data/lib/stackify/context/response.rb +24 -0
  16. data/lib/stackify/context_builder.rb +81 -0
  17. data/lib/stackify/error.rb +24 -0
  18. data/lib/stackify/error/exception.rb +36 -0
  19. data/lib/stackify/error/log.rb +25 -0
  20. data/lib/stackify/error_builder.rb +65 -0
  21. data/lib/stackify/instrumenter.rb +118 -0
  22. data/lib/stackify/internal_error.rb +5 -0
  23. data/lib/stackify/log.rb +51 -0
  24. data/lib/stackify/logger.rb +10 -0
  25. data/lib/stackify/middleware.rb +78 -0
  26. data/lib/stackify/naively_hashable.rb +25 -0
  27. data/lib/stackify/normalizers.rb +71 -0
  28. data/lib/stackify/normalizers/action_controller.rb +24 -0
  29. data/lib/stackify/normalizers/action_mailer.rb +23 -0
  30. data/lib/stackify/normalizers/action_view.rb +72 -0
  31. data/lib/stackify/normalizers/active_record.rb +71 -0
  32. data/lib/stackify/railtie.rb +50 -0
  33. data/lib/stackify/root_info.rb +58 -0
  34. data/lib/stackify/serializers.rb +27 -0
  35. data/lib/stackify/serializers/errors.rb +45 -0
  36. data/lib/stackify/serializers/transactions.rb +71 -0
  37. data/lib/stackify/span.rb +71 -0
  38. data/lib/stackify/span/context.rb +26 -0
  39. data/lib/stackify/spies.rb +89 -0
  40. data/lib/stackify/spies/action_dispatch.rb +26 -0
  41. data/lib/stackify/spies/httpclient.rb +47 -0
  42. data/lib/stackify/spies/mongo.rb +66 -0
  43. data/lib/stackify/spies/net_http.rb +47 -0
  44. data/lib/stackify/spies/sinatra.rb +50 -0
  45. data/lib/stackify/spies/tilt.rb +28 -0
  46. data/lib/stackify/stacktrace.rb +19 -0
  47. data/lib/stackify/stacktrace/frame.rb +50 -0
  48. data/lib/stackify/stacktrace_builder.rb +101 -0
  49. data/lib/stackify/subscriber.rb +113 -0
  50. data/lib/stackify/trace_logger.rb +66 -0
  51. data/lib/stackify/transaction.rb +123 -0
  52. data/lib/stackify/util.rb +23 -0
  53. data/lib/stackify/util/dig.rb +31 -0
  54. data/lib/stackify/util/inflector.rb +91 -0
  55. data/lib/stackify/util/inspector.rb +59 -0
  56. data/lib/stackify/util/lru_cache.rb +49 -0
  57. data/lib/stackify/version.rb +4 -0
  58. data/lib/stackify/worker.rb +119 -0
  59. data/lib/stackify_ruby_apm.rb +130 -0
  60. data/stackify-ruby-apm.gemspec +30 -0
  61. metadata +187 -0
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ # This module monkey patches usually in Exceptions class
3
+ module StackifyRubyAPM
4
+ # @api private
5
+ module Spies
6
+ # @api private
7
+ class ActionDispatchSpy
8
+ def install
9
+ ::ActionDispatch::ShowExceptions.class_eval do
10
+ alias render_exception_without_apm render_exception
11
+
12
+ def render_exception(env, exception)
13
+ StackifyRubyAPM.report(exception)
14
+ render_exception_without_apm env, exception
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ register(
21
+ 'ActionDispatch::ShowExceptions',
22
+ 'action_dispatch/show_exception',
23
+ ActionDispatchSpy.new
24
+ )
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ require 'stackify/log'
2
+ require 'stackify/logger'
3
+
4
+ module StackifyRubyAPM
5
+ # @api private
6
+ module Spies
7
+ # @api private
8
+ class HTTPClientSpy
9
+ include Log
10
+
11
+ # This class monkeypatches the HTTPClient class with request method.
12
+ def install
13
+ HTTPClient.class_eval do
14
+ alias request_without_apm request
15
+
16
+ def request(method, uri, *args, &block)
17
+ req = nil
18
+ unless StackifyRubyAPM.current_transaction
19
+ return request_without_apm(method, uri, *args, &block)
20
+ end
21
+ method = method.upcase
22
+ uri = uri.strip
23
+ name = "#{method} #{uri}"
24
+ type = "ext.httpclient.#{method}"
25
+
26
+ req = request_without_apm(method, uri, *args, &block)
27
+ ctx = Span::Context.new(
28
+ CATEGORY: 'Web External',
29
+ SUBCATEGORY: 'Execute',
30
+ URL: uri,
31
+ STATUS: req.status_code,
32
+ METHOD: method
33
+ )
34
+
35
+ StackifyRubyAPM.span name, type, context: ctx do
36
+ req
37
+ end
38
+
39
+ end
40
+ end
41
+ end
42
+ # rubocop:enable Metrics/MethodLength
43
+ end
44
+
45
+ register 'HTTPClient', 'httpclient', HTTPClientSpy.new
46
+ end
47
+ end
@@ -0,0 +1,66 @@
1
+ module StackifyRubyAPM
2
+ # @api private
3
+ module Spies
4
+ # @api private
5
+ # This class monkeypatch the Mongo::Monitoring::Global module in mongoDB(http://api.mongodb.com/ruby/current/Mongo/Monitoring/Global.html)
6
+ class MongoSpy
7
+ def install
8
+ ::Mongo::Monitoring::Global.subscribe(
9
+ ::Mongo::Monitoring::COMMAND,
10
+ Subscriber.new
11
+ )
12
+ end
13
+
14
+ # @api private
15
+ class Subscriber
16
+ TYPE = 'db.mongodb.query'.freeze
17
+
18
+ def initialize
19
+ @events = {}
20
+ end
21
+
22
+ def started(event)
23
+ push_event(event)
24
+ end
25
+
26
+ def failed(event)
27
+ pop_event(event)
28
+ end
29
+
30
+ def succeeded(event)
31
+ pop_event(event)
32
+ end
33
+
34
+ private
35
+
36
+ def push_event(event)
37
+ return unless StackifyRubyAPM.current_transaction
38
+
39
+ document = event.command.find
40
+ col = nil
41
+ if document
42
+ document.each_with_index do |val, idx|
43
+ col = val[1] if idx == 0
44
+ end
45
+ end
46
+
47
+ ctx = Span::Context.new(
48
+ CATEGORY: 'MongoDB',
49
+ MONGODB_COLLECTION: col,
50
+ )
51
+ span = StackifyRubyAPM.span(event.command_name, TYPE, context: ctx)
52
+ @events[event.operation_id] = span
53
+ end
54
+
55
+ def pop_event(event)
56
+ return unless StackifyRubyAPM.current_transaction
57
+
58
+ span = @events.delete(event.operation_id)
59
+ span && span.done
60
+ end
61
+ end
62
+ end
63
+
64
+ register 'Mongo', 'mongo', MongoSpy.new
65
+ end
66
+ end
@@ -0,0 +1,47 @@
1
+ module StackifyRubyAPM
2
+ # @api private
3
+ module Spies
4
+ # @api private
5
+ class NetHTTPSpy
6
+ # This class monkeypatch the Net::HTTP class with request method.
7
+ def install
8
+ Net::HTTP.class_eval do
9
+ alias request_without_apm request
10
+
11
+ def request(req, body = nil, &block)
12
+ result = nil
13
+ unless StackifyRubyAPM.current_transaction
14
+ request_without_apm(req, body, &block)
15
+ end
16
+
17
+ host, = req['host'] && req['host'].split(':')
18
+ method = req.method
19
+
20
+ host ||= address
21
+
22
+ name = "#{method} #{host}"
23
+ type = "ext.net_http.#{method}"
24
+
25
+ result = request_without_apm(req, body, &block)
26
+
27
+ ctx = Span::Context.new(
28
+ CATEGORY: 'Web External',
29
+ SUBCATEGORY: 'Execute',
30
+ URL: req.uri,
31
+ STATUS: result.code.to_i,
32
+ METHOD: method
33
+ )
34
+
35
+ StackifyRubyAPM.span name, type, context: ctx do
36
+ return result
37
+ end
38
+ end
39
+ end
40
+ end
41
+ # rubocop:enable Metrics/MethodLength
42
+ end
43
+
44
+
45
+ register 'Net::HTTP', 'net/http', NetHTTPSpy.new
46
+ end
47
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Monkey patch for the Sinatra::Base class which is responsible for handling requests.
4
+ #
5
+
6
+ module StackifyRubyAPM
7
+ # @api private
8
+ module Spies
9
+ # @api private
10
+ class SinatraSpy
11
+ # rubocop:disable Metrics/MethodLength
12
+ def install
13
+ ::Sinatra::Base.class_eval do
14
+ alias dispatch_without_apm! dispatch!
15
+ alias compile_template_without_apm compile_template
16
+
17
+ # Sets transaction name from Sinatra env's route name
18
+ #
19
+ def dispatch!(*args, &block)
20
+ dispatch_without_apm!(*args, &block).tap do
21
+ next unless (transaction = StackifyRubyAPM.current_transaction)
22
+ next unless (route = env['sinatra.route'])
23
+
24
+ transaction.name = route
25
+ end
26
+ end
27
+
28
+ # Tilt engine templating
29
+ #
30
+ def compile_template(engine, data, opts, *args, &block)
31
+ opts[:__stackify_apm_template_name] =
32
+ case data
33
+ when Symbol then data.to_s
34
+ else format('Inline %s', engine)
35
+ end
36
+
37
+ compile_template_without_apm(engine, data, opts, *args, &block)
38
+ end
39
+ end
40
+ end
41
+ # rubocop:enable Metrics/MethodLength
42
+ end
43
+
44
+ # Registers Sinatra spy, go to: /stackify/spies.rb
45
+ #
46
+ register 'Sinatra::Base', 'sinatra/base', SinatraSpy.new
47
+
48
+ require 'stackify/spies/tilt'
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ # @api private
5
+ module Spies
6
+ # @api private
7
+ class TiltSpy
8
+ # This class monkeypatch the Tilt::Template class with render method.
9
+ TYPE = 'template.tilt'.freeze
10
+
11
+ def install
12
+ ::Tilt::Template.class_eval do
13
+ alias render_without_apm render
14
+
15
+ def render(*args, &block)
16
+ name = options[:__stackify_apm_template_name] || 'Unknown template'
17
+
18
+ StackifyRubyAPM.span name, TYPE do
19
+ render_without_apm(*args, &block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ register 'Tilt::Template', 'tilt/template', TiltSpy.new
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This module starts creation for frames
4
+ #
5
+ module StackifyRubyAPM
6
+ # @api private
7
+ class Stacktrace
8
+ attr_accessor :frames
9
+
10
+ def length
11
+ frames.length
12
+ end
13
+
14
+ def to_a
15
+ frames.map(&:to_h)
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ class Stacktrace
5
+ # @api private
6
+ class Frame
7
+ include NaivelyHashable
8
+
9
+ attr_accessor(
10
+ :abs_path,
11
+ :filename,
12
+ :function,
13
+ :vars,
14
+ :pre_context,
15
+ :context_line,
16
+ :post_context,
17
+ :library_frame,
18
+ :lineno,
19
+ :module,
20
+ :Method,
21
+ :colno
22
+ )
23
+
24
+ # rubocop:disable Metrics/AbcSize
25
+ def build_context(context_line_count)
26
+ return unless abs_path && context_line_count > 0
27
+
28
+ padding = (context_line_count - 1) / 2
29
+ from = lineno - padding - 1
30
+ from = 0 if from < 0
31
+ to = lineno + padding - 1
32
+ file_lines = read_lines(abs_path, from..to)
33
+
34
+ self.context_line = file_lines[padding]
35
+ self.pre_context = file_lines.first(padding)
36
+ self.post_context = file_lines.last(padding)
37
+ end
38
+ # rubocop:enable Metrics/AbcSize
39
+
40
+ private
41
+
42
+ def read_lines(path, range)
43
+ File.readlines(path)[range]
44
+ rescue Errno::ENOENT
45
+ []
46
+ end
47
+ end
48
+ end
49
+ end
50
+
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This module gathers the lists of frames and build them into hash format
4
+ #
5
+ require 'stackify/stacktrace/frame'
6
+ require 'stackify/util/lru_cache'
7
+
8
+ module StackifyRubyAPM
9
+ # @api private
10
+ class StacktraceBuilder
11
+ JAVA_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/
12
+ RUBY_FORMAT = /^(.+?):(\d+)(?::in `(.+?)')?$/
13
+
14
+ RUBY_VERS_REGEX = %r{ruby(/gems)?[-/](\d+\.)+\d}
15
+ JRUBY_ORG_REGEX = %r{org/jruby}
16
+
17
+ def initialize(agent)
18
+ @config = agent.config
19
+ @cache = Util::LruCache.new(2048, &method(:build_frame))
20
+ end
21
+
22
+ attr_reader :config
23
+
24
+ def build(backtrace, type:)
25
+ Stacktrace.new.tap do |s|
26
+ s.frames = backtrace.map do |line|
27
+ @cache[[line, type]]
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
35
+ def build_frame(cache, keys)
36
+ line, type = keys
37
+ abs_path, lineno, function, _module_name = parse_line(line)
38
+ current_filename = strip_load_path(abs_path)
39
+
40
+ frame = Stacktrace::Frame.new
41
+ #frame.abs_path = abs_path
42
+ #frame.filename = strip_load_path(abs_path)
43
+ frame.Method = "#{function} (#{current_filename}:#{lineno})"
44
+ #frame.lineno = lineno.to_i
45
+ #frame.library_frame = library_frame?(config, abs_path)
46
+
47
+ line_count =
48
+ context_lines_for(config, type, library_frame: frame.library_frame)
49
+ frame.build_context line_count
50
+
51
+ cache[[line, type]] = frame
52
+ end
53
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
54
+
55
+ def parse_line(line)
56
+ ruby_match = line.match(RUBY_FORMAT)
57
+
58
+ if ruby_match
59
+ _, file, number, method = ruby_match.to_a
60
+ file.sub!(/\.class$/, '.rb')
61
+ module_name = nil
62
+ else
63
+ java_match = line.match(JAVA_FORMAT)
64
+ _, module_name, method, file, number = java_match.to_a
65
+ end
66
+
67
+ [file, number, method, module_name]
68
+ end
69
+
70
+ def library_frame?(config, abs_path)
71
+ return false unless abs_path
72
+
73
+ if abs_path.start_with?(config.root_path)
74
+ return true if abs_path.start_with?(config.root_path + '/vendor')
75
+ return false
76
+ end
77
+
78
+ return true if abs_path.match(RUBY_VERS_REGEX)
79
+ return true if abs_path.match(JRUBY_ORG_REGEX)
80
+
81
+ false
82
+ end
83
+
84
+ def strip_load_path(path)
85
+ return nil if path.nil?
86
+
87
+ prefix =
88
+ $LOAD_PATH
89
+ .map(&:to_s)
90
+ .select { |s| path.start_with?(s) }
91
+ .max_by(&:length)
92
+
93
+ prefix ? path[prefix.chomp(File::SEPARATOR).length + 1..-1] : path
94
+ end
95
+
96
+ def context_lines_for(config, type, library_frame:)
97
+ key = "source_lines_#{type}_#{library_frame ? 'library' : 'app'}_frames"
98
+ config.send(key.to_sym)
99
+ end
100
+ end
101
+ end