stackify-ruby-apm 0.9.0

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 (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