stackify-ruby-apm 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +76 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +68 -0
- data/README.md +23 -0
- data/Rakefile +6 -0
- data/lib/stackify-ruby-apm.rb +4 -0
- data/lib/stackify/agent.rb +186 -0
- data/lib/stackify/config.rb +221 -0
- data/lib/stackify/context.rb +24 -0
- data/lib/stackify/context/request.rb +12 -0
- data/lib/stackify/context/request/socket.rb +21 -0
- data/lib/stackify/context/request/url.rb +44 -0
- data/lib/stackify/context/response.rb +24 -0
- data/lib/stackify/context_builder.rb +81 -0
- data/lib/stackify/error.rb +24 -0
- data/lib/stackify/error/exception.rb +36 -0
- data/lib/stackify/error/log.rb +25 -0
- data/lib/stackify/error_builder.rb +65 -0
- data/lib/stackify/instrumenter.rb +118 -0
- data/lib/stackify/internal_error.rb +5 -0
- data/lib/stackify/log.rb +51 -0
- data/lib/stackify/logger.rb +10 -0
- data/lib/stackify/middleware.rb +78 -0
- data/lib/stackify/naively_hashable.rb +25 -0
- data/lib/stackify/normalizers.rb +71 -0
- data/lib/stackify/normalizers/action_controller.rb +24 -0
- data/lib/stackify/normalizers/action_mailer.rb +23 -0
- data/lib/stackify/normalizers/action_view.rb +72 -0
- data/lib/stackify/normalizers/active_record.rb +71 -0
- data/lib/stackify/railtie.rb +50 -0
- data/lib/stackify/root_info.rb +58 -0
- data/lib/stackify/serializers.rb +27 -0
- data/lib/stackify/serializers/errors.rb +45 -0
- data/lib/stackify/serializers/transactions.rb +71 -0
- data/lib/stackify/span.rb +71 -0
- data/lib/stackify/span/context.rb +26 -0
- data/lib/stackify/spies.rb +89 -0
- data/lib/stackify/spies/action_dispatch.rb +26 -0
- data/lib/stackify/spies/httpclient.rb +47 -0
- data/lib/stackify/spies/mongo.rb +66 -0
- data/lib/stackify/spies/net_http.rb +47 -0
- data/lib/stackify/spies/sinatra.rb +50 -0
- data/lib/stackify/spies/tilt.rb +28 -0
- data/lib/stackify/stacktrace.rb +19 -0
- data/lib/stackify/stacktrace/frame.rb +50 -0
- data/lib/stackify/stacktrace_builder.rb +101 -0
- data/lib/stackify/subscriber.rb +113 -0
- data/lib/stackify/trace_logger.rb +66 -0
- data/lib/stackify/transaction.rb +123 -0
- data/lib/stackify/util.rb +23 -0
- data/lib/stackify/util/dig.rb +31 -0
- data/lib/stackify/util/inflector.rb +91 -0
- data/lib/stackify/util/inspector.rb +59 -0
- data/lib/stackify/util/lru_cache.rb +49 -0
- data/lib/stackify/version.rb +4 -0
- data/lib/stackify/worker.rb +119 -0
- data/lib/stackify_ruby_apm.rb +130 -0
- data/stackify-ruby-apm.gemspec +30 -0
- 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
|