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