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,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This class loads the specified config of the Rails app and starts the Agent.
|
4
|
+
#
|
5
|
+
# APM Config setting is done at stackify/config.rb
|
6
|
+
#
|
7
|
+
# After the Agent has been started:
|
8
|
+
# -> the enabled Spies (libraries) are registered
|
9
|
+
# -> the Subscriber is called to register the events
|
10
|
+
# -> the Middleware is then inserted
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'stackify/subscriber'
|
14
|
+
|
15
|
+
module StackifyRubyAPM
|
16
|
+
# @api private
|
17
|
+
# Railtie - core of the Rails framework and provides several hooks to modify the initialization process.
|
18
|
+
class Railtie < Rails::Railtie
|
19
|
+
|
20
|
+
config.stackify = ActiveSupport::OrderedOptions.new
|
21
|
+
|
22
|
+
Config::DEFAULTS.each { |option, value| config.stackify[option] = value }
|
23
|
+
|
24
|
+
# This will overwrite the default values of the config based from stackify_apm.yml initialized
|
25
|
+
|
26
|
+
initializer 'stackify_apm.initialize' do |app|
|
27
|
+
# puts '@stackify_ruby [Railtie] [lib/railtie.rb] initializer'
|
28
|
+
config = app.config.stackify.merge(app: app).tap do |c|
|
29
|
+
# Prepend Rails.root to log_path if present
|
30
|
+
if c.log_path && !c.log_path.start_with?('/')
|
31
|
+
c.log_path = Rails.root.join(c.log_path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
agent = StackifyRubyAPM.start config
|
37
|
+
if agent
|
38
|
+
agent.instrumenter.subscriber = StackifyRubyAPM::Subscriber.new(agent)
|
39
|
+
app.middleware.insert 0, Middleware # add a middleware to the application
|
40
|
+
end
|
41
|
+
rescue StandardError => e
|
42
|
+
Rails.logger.error "[Railtie] Failed to start: #{e.message}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
config.after_initialize do
|
47
|
+
require 'stackify/spies/action_dispatch'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This will format the root information of the Transaction.
|
4
|
+
#
|
5
|
+
|
6
|
+
module StackifyRubyAPM
|
7
|
+
# @api private
|
8
|
+
class RootInfo
|
9
|
+
def initialize(config, transaction)
|
10
|
+
@transaction = transaction
|
11
|
+
@config = config
|
12
|
+
end
|
13
|
+
|
14
|
+
# rubocop:disable Metrics/MethodLength
|
15
|
+
# rubocop:disable Style/StringLiterals
|
16
|
+
def build
|
17
|
+
# get process id
|
18
|
+
pid = $PID || Process.pid
|
19
|
+
|
20
|
+
base = {
|
21
|
+
CATEGORY: 'Ruby',
|
22
|
+
APPLICATION_PATH: '/',
|
23
|
+
APPLICATION_FILESYSTEM_PATH: @config.root_path,
|
24
|
+
APPLICATION_NAME: @config.application_name,
|
25
|
+
APPLICATION_ENV: @config.environment_name,
|
26
|
+
REPORTING_URL: @transaction.name,
|
27
|
+
METHOD: @transaction.context.request.method,
|
28
|
+
STATUS: @transaction.context.response.status_code,
|
29
|
+
URL: @transaction.context.request.url[:full],
|
30
|
+
TRACETYPE: 'WEBAPP',
|
31
|
+
TRACE_ID: @transaction.id,
|
32
|
+
THREAD_ID: Thread.current.object_id,
|
33
|
+
TRACE_SOURCE: 'Ruby',
|
34
|
+
TRACE_TARGET: 'RETRACE',
|
35
|
+
HOST_NAME: @config.hostname || `hostname`,
|
36
|
+
OS_TYPE: 'LINUX',
|
37
|
+
PROCESS_ID: pid,
|
38
|
+
TRACE_VERSION: '2.0'
|
39
|
+
}
|
40
|
+
end
|
41
|
+
# rubocop:enable Metrics/MethodLength
|
42
|
+
|
43
|
+
def self.build(config, transaction)
|
44
|
+
new(config, transaction).build
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def runtime
|
50
|
+
case RUBY_ENGINE
|
51
|
+
when 'ruby'
|
52
|
+
{ name: RUBY_ENGINE, version: RUBY_VERSION }
|
53
|
+
when 'jruby'
|
54
|
+
{ name: RUBY_ENGINE, version: ENV['JRUBY_VERSION'] }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'json'
|
3
|
+
# After the spans are being build it will serialize or rebuild or reformat the spans created into json format
|
4
|
+
module StackifyRubyAPM
|
5
|
+
# @api private
|
6
|
+
module Serializers
|
7
|
+
# @api private
|
8
|
+
class Serializer
|
9
|
+
def initialize(config)
|
10
|
+
@config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def micros_to_time(micros)
|
16
|
+
Time.at(ms(micros) / 1_000)
|
17
|
+
end
|
18
|
+
|
19
|
+
def ms(micros)
|
20
|
+
micros.to_f / 1_000
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'stackify/serializers/transactions'
|
27
|
+
require 'stackify/serializers/errors'
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This module gathers the built Errors and Exceptions into hash formats
|
4
|
+
#
|
5
|
+
module StackifyRubyAPM
|
6
|
+
module Serializers
|
7
|
+
# @api private
|
8
|
+
class Errors < Serializer
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
10
|
+
def build(error)
|
11
|
+
|
12
|
+
if (exception = error.exception)
|
13
|
+
|
14
|
+
base = {
|
15
|
+
CaughtBy: exception.module,
|
16
|
+
Exception: exception.type,
|
17
|
+
Message: exception.message,
|
18
|
+
Timestamp: micros_to_time(error.timestamp).utc.iso8601(3),
|
19
|
+
Frames: exception.stacktrace.to_a,
|
20
|
+
}
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
base
|
25
|
+
end
|
26
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
27
|
+
|
28
|
+
def build_all(errors)
|
29
|
+
{ exceptions: Array(errors).map(&method(:build)) }
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def build_log(log)
|
35
|
+
{
|
36
|
+
message: log.message,
|
37
|
+
level: log.level,
|
38
|
+
logger_name: log.logger_name,
|
39
|
+
param_message: log.param_message,
|
40
|
+
stacktrace: log.stacktrace.to_a
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This class builds and reformats all the gathered spans within the transaction.
|
4
|
+
#
|
5
|
+
|
6
|
+
module StackifyRubyAPM
|
7
|
+
module Serializers
|
8
|
+
# @api private
|
9
|
+
|
10
|
+
class Transactions < Serializer
|
11
|
+
include Log
|
12
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
13
|
+
def build(config, transaction)
|
14
|
+
|
15
|
+
root_span_json = {
|
16
|
+
id: -1,
|
17
|
+
call: transaction.name,
|
18
|
+
reqBegin: transaction.timestamp,
|
19
|
+
reqEnd: transaction.duration,
|
20
|
+
props: RootInfo.build(config, transaction)
|
21
|
+
}
|
22
|
+
|
23
|
+
# serialize all the spans
|
24
|
+
children_spans_json = []
|
25
|
+
transaction.spans.each do |span|
|
26
|
+
children_spans_json.push(build_span(span))
|
27
|
+
end
|
28
|
+
|
29
|
+
add_children_spans(root_span_json, children_spans_json)
|
30
|
+
|
31
|
+
root_span_json
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Add Children Spans to span_json
|
37
|
+
def add_children_spans(span_json, children_spans_json)
|
38
|
+
id = span_json[:id]
|
39
|
+
children_spans_json.each do |child_span_json|
|
40
|
+
parent_id = child_span_json[:parent_id]
|
41
|
+
if parent_id == id
|
42
|
+
child_span_copy = child_span_json.dup
|
43
|
+
add_children_spans(child_span_copy, children_spans_json)
|
44
|
+
if span_json[:stacks].nil?
|
45
|
+
span_json[:stacks] = []
|
46
|
+
end
|
47
|
+
span_json[:stacks].push(child_span_copy)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# rubocop:disable Metrics/AbcSize
|
53
|
+
def build_span(span)
|
54
|
+
span_type = span.type
|
55
|
+
span_context = span.context && span.context.to_h
|
56
|
+
|
57
|
+
{
|
58
|
+
id: span.id,
|
59
|
+
parent_id: span.parent_id,
|
60
|
+
call: span_type,
|
61
|
+
reqBegin: span.relative_start,
|
62
|
+
reqEnd: span.relative_end,
|
63
|
+
props: span_context.nil? ? { CATEGORY: 'Ruby' } : span_context,
|
64
|
+
stacks: []
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# rubocop:enable Metrics/AbcSize
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This modules creates and initializes new Span
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'securerandom'
|
7
|
+
require 'stackify/span/context'
|
8
|
+
module StackifyRubyAPM
|
9
|
+
# @api private
|
10
|
+
class Span
|
11
|
+
DEFAULT_TYPE = 'custom'.freeze
|
12
|
+
|
13
|
+
# rubocop:disable Metrics/ParameterLists
|
14
|
+
def initialize(
|
15
|
+
transaction,
|
16
|
+
id,
|
17
|
+
name,
|
18
|
+
type = nil,
|
19
|
+
parent_id: nil,
|
20
|
+
context: nil,
|
21
|
+
http_status: nil
|
22
|
+
)
|
23
|
+
@transaction = transaction
|
24
|
+
@id = id
|
25
|
+
@name = name
|
26
|
+
@type = type || DEFAULT_TYPE
|
27
|
+
@parent_id = parent_id
|
28
|
+
@context = context
|
29
|
+
@http_status = http_status
|
30
|
+
|
31
|
+
@stacktrace = nil
|
32
|
+
@original_backtrace = nil
|
33
|
+
end
|
34
|
+
# rubocop:enable Metrics/ParameterLists
|
35
|
+
|
36
|
+
attr_accessor :name, :type, :original_backtrace, :http_status
|
37
|
+
attr_reader :id, :context, :stacktrace, :duration, :parent_id, :relative_start, :relative_end
|
38
|
+
|
39
|
+
def start
|
40
|
+
@relative_start = (Time.now).to_f * 1000
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def done
|
46
|
+
@relative_end = (Time.now).to_f * 1000
|
47
|
+
@duration = (Time.now).to_f * 1000
|
48
|
+
self.original_backtrace = nil # release it
|
49
|
+
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def done?
|
54
|
+
!!duration
|
55
|
+
end
|
56
|
+
|
57
|
+
def running?
|
58
|
+
relative_start && !done?
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect
|
62
|
+
"<StackifyRubyAPM::Span id:#{id}" \
|
63
|
+
" name:#{name.inspect}" \
|
64
|
+
" type:#{type.inspect}" \
|
65
|
+
'>'
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module StackifyRubyAPM
|
3
|
+
class Span
|
4
|
+
# @api private
|
5
|
+
class Context
|
6
|
+
include NaivelyHashable
|
7
|
+
|
8
|
+
def initialize(**args)
|
9
|
+
args.each do |key, val|
|
10
|
+
send(:"#{key}=", val)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :CATEGORY,
|
15
|
+
:SUBCATEGORY,
|
16
|
+
:COMPONENT_CATEGORY,
|
17
|
+
:COMPONENT_DETAIL,
|
18
|
+
:SQL,
|
19
|
+
:MONGODB_COLLECTION,
|
20
|
+
:METHOD,
|
21
|
+
:URL,
|
22
|
+
:STATUS,
|
23
|
+
:PROVIDER
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This module communicates and registers the additional or enabled outside library method
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'forwardable'
|
7
|
+
require 'stackify/util/inflector'
|
8
|
+
|
9
|
+
module StackifyRubyAPM
|
10
|
+
# @api private
|
11
|
+
module Spies
|
12
|
+
# @api private
|
13
|
+
class Registration
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
def initialize(const_name, require_paths, spy)
|
17
|
+
@const_name = const_name
|
18
|
+
@require_paths = Array(require_paths)
|
19
|
+
@spy = spy
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :const_name, :require_paths
|
23
|
+
|
24
|
+
def_delegator :@spy, :install
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.require_hooks
|
28
|
+
@require_hooks ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.installed
|
32
|
+
@installed ||= {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.register(*args)
|
36
|
+
registration = Registration.new(*args)
|
37
|
+
|
38
|
+
if safe_defined?(registration.const_name)
|
39
|
+
registration.install
|
40
|
+
installed[registration.const_name] = registration
|
41
|
+
else
|
42
|
+
register_require_hook registration
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.register_require_hook(registration)
|
47
|
+
registration.require_paths.each do |path|
|
48
|
+
require_hooks[path] = registration
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.hook_into(name)
|
53
|
+
return unless (registration = require_hooks[name])
|
54
|
+
return unless safe_defined?(registration.const_name)
|
55
|
+
|
56
|
+
installed[registration.const_name] = registration
|
57
|
+
registration.install
|
58
|
+
|
59
|
+
registration.require_paths.each do |path|
|
60
|
+
require_hooks.delete path
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.safe_defined?(const_name)
|
65
|
+
Util::Inflector.safe_constantize(const_name)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @api private
|
71
|
+
module Kernel
|
72
|
+
private
|
73
|
+
|
74
|
+
alias require_without_apm require
|
75
|
+
|
76
|
+
def require(path)
|
77
|
+
res = require_without_apm(path)
|
78
|
+
|
79
|
+
begin
|
80
|
+
StackifyRubyAPM::Spies.hook_into(path)
|
81
|
+
rescue ::Exception => e
|
82
|
+
puts "Failed hooking into '#{path}'. Please report this at " \
|
83
|
+
'git@bitbucket.org:stackify/stackify-ruby-apm.git'
|
84
|
+
puts e.backtrace.join("\n")
|
85
|
+
end
|
86
|
+
|
87
|
+
res
|
88
|
+
end
|
89
|
+
end
|