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,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This class initializes the parameters and variables for the context of Transaction/Span
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'stackify/context/request'
|
7
|
+
require 'stackify/context/request/socket'
|
8
|
+
require 'stackify/context/request/url'
|
9
|
+
require 'stackify/context/response'
|
10
|
+
|
11
|
+
module StackifyRubyAPM
|
12
|
+
# @api private
|
13
|
+
class Context
|
14
|
+
include NaivelyHashable
|
15
|
+
|
16
|
+
attr_accessor :request, :response
|
17
|
+
attr_reader :custom, :tags
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@custom = {}
|
21
|
+
@tags = {}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StackifyRubyAPM
|
4
|
+
# @api private
|
5
|
+
class Context
|
6
|
+
# @api private
|
7
|
+
class Request
|
8
|
+
# @api private
|
9
|
+
class Socket
|
10
|
+
include NaivelyHashable
|
11
|
+
|
12
|
+
def initialize(req)
|
13
|
+
@remote_addr = req.ip
|
14
|
+
@encrypted = req.scheme == 'https'
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :remote_addr, :encrypted
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StackifyRubyAPM
|
4
|
+
# @api private
|
5
|
+
class Context
|
6
|
+
# @api private
|
7
|
+
class Request
|
8
|
+
# @api private
|
9
|
+
class Url
|
10
|
+
include NaivelyHashable
|
11
|
+
|
12
|
+
SKIPPED_PORTS = {
|
13
|
+
'http' => 80,
|
14
|
+
'https' => 443
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def initialize(req)
|
18
|
+
@protocol = req.scheme
|
19
|
+
@hostname = req.host
|
20
|
+
@port = req.port.to_s
|
21
|
+
@pathname = req.path
|
22
|
+
@search = req.query_string
|
23
|
+
@hash = nil
|
24
|
+
@full = build_full_url req
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :protocol, :hostname, :port, :pathname, :search, :hash,
|
28
|
+
:full
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def build_full_url(req)
|
33
|
+
url = "#{req.scheme}://#{req.host}"
|
34
|
+
|
35
|
+
if req.port != SKIPPED_PORTS[req.scheme]
|
36
|
+
url += ":#{req.port}"
|
37
|
+
end
|
38
|
+
|
39
|
+
url + req.fullpath
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StackifyRubyAPM
|
4
|
+
class Context
|
5
|
+
# @api private
|
6
|
+
class Response
|
7
|
+
include NaivelyHashable
|
8
|
+
|
9
|
+
def initialize(
|
10
|
+
status_code,
|
11
|
+
headers: {},
|
12
|
+
headers_sent: true,
|
13
|
+
finished: true
|
14
|
+
)
|
15
|
+
@status_code = status_code
|
16
|
+
@headers = headers
|
17
|
+
@headers_sent = headers_sent
|
18
|
+
@finished = finished
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :status_code, :headers, :headers_sent, :finished
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This class builds the context of the Transaction/Span
|
4
|
+
# Context represents the HTTP request information such as headers, cookies, body.
|
5
|
+
#
|
6
|
+
|
7
|
+
module StackifyRubyAPM
|
8
|
+
# @api private
|
9
|
+
class ContextBuilder
|
10
|
+
def initialize(_agent); end
|
11
|
+
|
12
|
+
# Creates context
|
13
|
+
#
|
14
|
+
def build(rack_env)
|
15
|
+
# puts "@stackify_ruby [lib/context_builder.rb] Loading ContextBuilder build(rack_env)"
|
16
|
+
context = Context.new
|
17
|
+
apply_to_request(context, rack_env)
|
18
|
+
context
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# rubocop:disable Metrics/AbcSize
|
24
|
+
#
|
25
|
+
# Request format and values are assigned to context
|
26
|
+
#
|
27
|
+
def apply_to_request(context, rack_env)
|
28
|
+
# puts "@stackify_ruby [lib/context_builder.rb] Loading ContextBuilder apply_to_request(context, rack_env)"
|
29
|
+
req = rails_req?(rack_env) ? rack_env : Rack::Request.new(rack_env)
|
30
|
+
context.request = Context::Request.new unless context.request
|
31
|
+
request = context.request
|
32
|
+
request.socket = Context::Request::Socket.new(req).to_h
|
33
|
+
request.http_version = build_http_version rack_env
|
34
|
+
request.method = req.request_method
|
35
|
+
request.url = Context::Request::Url.new(req).to_h
|
36
|
+
request.headers, request.env = get_headers_and_env(rack_env)
|
37
|
+
request.body = get_body(req)
|
38
|
+
|
39
|
+
context
|
40
|
+
end
|
41
|
+
# rubocop:enable Metrics/AbcSize
|
42
|
+
|
43
|
+
def get_body(req)
|
44
|
+
return req.POST if req.form_data?
|
45
|
+
|
46
|
+
body = req.body.read
|
47
|
+
req.body.rewind
|
48
|
+
body
|
49
|
+
end
|
50
|
+
|
51
|
+
def rails_req?(env)
|
52
|
+
defined?(ActionDispatch::Request) &&
|
53
|
+
env.is_a?(ActionDispatch::Request)
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_headers_and_env(rack_env)
|
57
|
+
# In Rails < 5 ActionDispatch::Request inherits from Hash
|
58
|
+
headers =
|
59
|
+
rack_env.respond_to?(:headers) ? rack_env.headers : rack_env
|
60
|
+
|
61
|
+
headers.each_with_object([{}, {}]) do |(key, value), (http, env)|
|
62
|
+
next unless key == key.upcase
|
63
|
+
|
64
|
+
if key.start_with?('HTTP_')
|
65
|
+
http[camel_key(key)] = value
|
66
|
+
else
|
67
|
+
env[key] = value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def camel_key(key)
|
73
|
+
key.gsub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_http_version(rack_env)
|
77
|
+
return unless (http_version = rack_env['HTTP_VERSION'])
|
78
|
+
http_version.gsub(%r{HTTP/}, '')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stackify/stacktrace'
|
4
|
+
require 'stackify/context'
|
5
|
+
require 'stackify/error/exception'
|
6
|
+
require 'stackify/error/log'
|
7
|
+
|
8
|
+
module StackifyRubyAPM
|
9
|
+
# @api private
|
10
|
+
class Error
|
11
|
+
def initialize(culprit: nil)
|
12
|
+
@id = SecureRandom.uuid
|
13
|
+
@culprit = culprit
|
14
|
+
|
15
|
+
@timestamp = Util.micros
|
16
|
+
@context = Context.new
|
17
|
+
|
18
|
+
@transaction_id = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :id, :culprit, :exception, :log, :transaction_id, :context
|
22
|
+
attr_reader :timestamp
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
module StackifyRubyAPM
|
3
|
+
class Error
|
4
|
+
# @api private
|
5
|
+
class Exception
|
6
|
+
MOD_SPLIT = '::'.freeze
|
7
|
+
|
8
|
+
def initialize(exception, **attrs)
|
9
|
+
@message =
|
10
|
+
"#{exception.class}: #{exception.message}"
|
11
|
+
@type = exception.class.to_s
|
12
|
+
@module = format_module exception
|
13
|
+
|
14
|
+
attrs.each do |key, val|
|
15
|
+
send(:"#{key}=", val)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_accessor(
|
20
|
+
:attributes,
|
21
|
+
:code,
|
22
|
+
:handled,
|
23
|
+
:message,
|
24
|
+
:module,
|
25
|
+
:stacktrace,
|
26
|
+
:type
|
27
|
+
)
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def format_module(exception)
|
32
|
+
exception.class.to_s.split(MOD_SPLIT)[0...-1].join(MOD_SPLIT)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StackifyRubyAPM
|
4
|
+
class Error
|
5
|
+
# @api private
|
6
|
+
class Log
|
7
|
+
def initialize(message, attrs = {})
|
8
|
+
@message = message
|
9
|
+
|
10
|
+
attrs.each do |key, val|
|
11
|
+
send(:"#{key}=", val)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor(
|
16
|
+
:level,
|
17
|
+
:logger_name,
|
18
|
+
:message,
|
19
|
+
:param_message,
|
20
|
+
:stacktrace
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This module build errors/exceptions and the Frames/stacktraces affected/related to the error
|
4
|
+
#
|
5
|
+
module StackifyRubyAPM
|
6
|
+
# @api private
|
7
|
+
class ErrorBuilder
|
8
|
+
def initialize(agent)
|
9
|
+
@agent = agent
|
10
|
+
end
|
11
|
+
|
12
|
+
def build_exception(exception, handled: true)
|
13
|
+
error = Error.new
|
14
|
+
error.exception = Error::Exception.new(exception, handled: handled)
|
15
|
+
|
16
|
+
if exception.backtrace
|
17
|
+
add_stacktrace error, :exception, exception.backtrace
|
18
|
+
end
|
19
|
+
|
20
|
+
add_transaction_id error
|
21
|
+
|
22
|
+
if (transaction = StackifyRubyAPM.current_transaction)
|
23
|
+
error.context = transaction.context.dup
|
24
|
+
end
|
25
|
+
|
26
|
+
error
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_log(message, backtrace: nil, **attrs)
|
30
|
+
error = Error.new
|
31
|
+
error.log = Error::Log.new(message, **attrs)
|
32
|
+
|
33
|
+
if backtrace
|
34
|
+
add_stacktrace error, :log, backtrace
|
35
|
+
end
|
36
|
+
|
37
|
+
add_transaction_id error
|
38
|
+
|
39
|
+
error
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def add_stacktrace(error, kind, backtrace)
|
45
|
+
stacktrace =
|
46
|
+
@agent.stacktrace_builder.build(backtrace, type: :error)
|
47
|
+
return unless stacktrace
|
48
|
+
|
49
|
+
case kind
|
50
|
+
when :exception
|
51
|
+
error.exception.stacktrace = stacktrace
|
52
|
+
when :log
|
53
|
+
error.log.stacktrace = stacktrace
|
54
|
+
end
|
55
|
+
|
56
|
+
error.culprit = stacktrace.frames.first.function
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_transaction_id(error)
|
60
|
+
return unless (transaction = StackifyRubyAPM.current_transaction)
|
61
|
+
error.transaction_id = transaction.id
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,118 @@
|
|
1
|
+
#
|
2
|
+
# The Instrumenter will build/process the transactions and spans.
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'stackify/span'
|
6
|
+
require 'stackify/transaction'
|
7
|
+
|
8
|
+
module StackifyRubyAPM
|
9
|
+
# @api private
|
10
|
+
class Instrumenter
|
11
|
+
include Log
|
12
|
+
|
13
|
+
KEY = :_stackify_transaction_key
|
14
|
+
|
15
|
+
class TransactionInfo
|
16
|
+
def initialize
|
17
|
+
self.current = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def current
|
21
|
+
# puts '@stackify_ruby [Instrumenter] [lib/instrumenter.rb] TransactionInfo.current()'
|
22
|
+
Thread.current[KEY]
|
23
|
+
end
|
24
|
+
|
25
|
+
def current=(transaction)
|
26
|
+
Thread.current[KEY] = transaction
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(agent)
|
31
|
+
debug '@stackify_ruby [Instrumenter] [lib/instrumenter.rb] initialize()'
|
32
|
+
@agent = agent
|
33
|
+
@config = agent.config
|
34
|
+
|
35
|
+
@transaction_info = TransactionInfo.new
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :agent, :config, :pending_transactions
|
39
|
+
|
40
|
+
def stop
|
41
|
+
current_transaction.release if current_transaction
|
42
|
+
@subscriber.unregister! if @subscriber
|
43
|
+
end
|
44
|
+
|
45
|
+
def subscriber=(subscriber)
|
46
|
+
@subscriber = subscriber
|
47
|
+
@subscriber.register!
|
48
|
+
end
|
49
|
+
|
50
|
+
def current_transaction
|
51
|
+
@transaction_info.current
|
52
|
+
end
|
53
|
+
|
54
|
+
def current_transaction=(transaction)
|
55
|
+
@transaction_info.current = transaction
|
56
|
+
end
|
57
|
+
|
58
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
59
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
60
|
+
#
|
61
|
+
# Creates a new transaction or return the currently running
|
62
|
+
#
|
63
|
+
def transaction(*args)
|
64
|
+
debug "Instrumenter function transaction(*args)"
|
65
|
+
unless config.instrument
|
66
|
+
yield if block_given?
|
67
|
+
return
|
68
|
+
end
|
69
|
+
|
70
|
+
if (transaction = current_transaction)
|
71
|
+
yield transaction if block_given?
|
72
|
+
return transaction
|
73
|
+
end
|
74
|
+
|
75
|
+
transaction = Transaction.new self, *args
|
76
|
+
|
77
|
+
self.current_transaction = transaction
|
78
|
+
return transaction unless block_given?
|
79
|
+
|
80
|
+
begin
|
81
|
+
yield transaction
|
82
|
+
ensure
|
83
|
+
self.current_transaction = nil
|
84
|
+
transaction.done
|
85
|
+
end
|
86
|
+
|
87
|
+
#puts transaction.inspect
|
88
|
+
|
89
|
+
transaction
|
90
|
+
end
|
91
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
92
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
93
|
+
|
94
|
+
def span(*args, &block)
|
95
|
+
unless current_transaction
|
96
|
+
return yield if block_given?
|
97
|
+
return
|
98
|
+
end
|
99
|
+
|
100
|
+
current_transaction.span(*args, &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Once the transaction is submitted it will be stored temporarily in queue
|
104
|
+
#
|
105
|
+
def submit_transaction(transaction)
|
106
|
+
debug '@stackify_ruby [Instrumenter] [lib/instrumenter.rb] submit_transaction(transaction):'
|
107
|
+
debug transaction.inspect
|
108
|
+
agent.enqueue_transaction transaction
|
109
|
+
return unless config.debug_transactions
|
110
|
+
end
|
111
|
+
|
112
|
+
def inspect
|
113
|
+
'<StackifyRubyAPM::Instrumenter ' \
|
114
|
+
"current_transaction=#{current_transaction.inspect}" \
|
115
|
+
'>'
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|