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,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# The Subscriber registers the existing events.
|
4
|
+
# This will trigger (together with the Middleware) once there is a web request.
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'active_support/notifications'
|
8
|
+
require 'stackify/normalizers'
|
9
|
+
|
10
|
+
module StackifyRubyAPM
|
11
|
+
# @api private
|
12
|
+
class Subscriber
|
13
|
+
include Log
|
14
|
+
|
15
|
+
def initialize(agent)
|
16
|
+
debug '@stackify_ruby [Subscriber] [lib/subscriber.rb] initialize()'
|
17
|
+
debug agent.inspect
|
18
|
+
@agent = agent
|
19
|
+
@normalizers = Normalizers.build(agent.config)
|
20
|
+
end
|
21
|
+
|
22
|
+
def register!
|
23
|
+
unregister! if @subscription
|
24
|
+
|
25
|
+
@subscription =
|
26
|
+
ActiveSupport::Notifications.subscribe(notifications_regex, self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def unregister!
|
30
|
+
ActiveSupport::Notifications.unsubscribe @subscription
|
31
|
+
@subscription = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# AS::Notifications API
|
35
|
+
|
36
|
+
Notification = Struct.new(:id, :span)
|
37
|
+
|
38
|
+
# [call] Called when the rails version is 3.x
|
39
|
+
def call(name, started, finished, id, payload)
|
40
|
+
return unless (transaction = @agent.current_transaction)
|
41
|
+
debug '@stackify_ruby [Subscriber] [lib/subscriber.rb] call():'
|
42
|
+
debug id
|
43
|
+
debug name
|
44
|
+
debug transaction
|
45
|
+
|
46
|
+
normalized = @normalizers.normalize(transaction, name, payload)
|
47
|
+
|
48
|
+
if started
|
49
|
+
span =
|
50
|
+
if normalized == :skip
|
51
|
+
nil
|
52
|
+
else
|
53
|
+
name, type, context = normalized
|
54
|
+
@agent.span(name, type, context: context)
|
55
|
+
end
|
56
|
+
transaction.notifications << Notification.new(id, span)
|
57
|
+
end
|
58
|
+
|
59
|
+
if finished
|
60
|
+
while (notification = transaction.notifications.pop)
|
61
|
+
next unless notification.id == id
|
62
|
+
|
63
|
+
if (span = notification.span)
|
64
|
+
span.done
|
65
|
+
end
|
66
|
+
return
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# [start] Called when the rails version is NOT 3.x
|
72
|
+
def start(name, id, payload)
|
73
|
+
return unless (transaction = @agent.current_transaction)
|
74
|
+
debug '@stackify_ruby [Subscriber] [lib/subscriber.rb] start():'
|
75
|
+
debug id
|
76
|
+
debug name
|
77
|
+
debug transaction
|
78
|
+
normalized = @normalizers.normalize(transaction, name, payload)
|
79
|
+
|
80
|
+
span =
|
81
|
+
if normalized == :skip
|
82
|
+
nil
|
83
|
+
else
|
84
|
+
name, type, context = normalized
|
85
|
+
@agent.span(name, type, context: context)
|
86
|
+
end
|
87
|
+
|
88
|
+
transaction.notifications << Notification.new(id, span)
|
89
|
+
end
|
90
|
+
|
91
|
+
# [finish] Called when the rails version is NOT 3.x
|
92
|
+
def finish(_name, id, _payload)
|
93
|
+
# debug "AS::Notification#finish:#{name}:#{id}"
|
94
|
+
debug '@stackify_ruby [Subscriber] [lib/subscriber.rb] finish():'
|
95
|
+
return unless (transaction = @agent.current_transaction)
|
96
|
+
|
97
|
+
while (notification = transaction.notifications.pop)
|
98
|
+
next unless notification.id == id
|
99
|
+
|
100
|
+
if (span = notification.span)
|
101
|
+
span.done
|
102
|
+
end
|
103
|
+
return
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def notifications_regex
|
110
|
+
@notifications_regex ||= /(#{@normalizers.keys.join('|')})/
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This class generates/appends the json log file after every Web Request
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'stackify/root_info'
|
7
|
+
require 'stackify/serializers'
|
8
|
+
|
9
|
+
module StackifyRubyAPM
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
class TraceLogger
|
13
|
+
include Log
|
14
|
+
|
15
|
+
def initialize(config)
|
16
|
+
@config = config
|
17
|
+
@trace_file_counter = 0
|
18
|
+
@process_id = 0
|
19
|
+
@transaction_serializers = Serializers::Transactions.new(config)
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_accessor :trace_file_counter, :process_id
|
23
|
+
|
24
|
+
def post(transactions = [])
|
25
|
+
|
26
|
+
# convert transactions to json
|
27
|
+
json_traces = []
|
28
|
+
transactions.each do |transaction|
|
29
|
+
# convert transaction to json
|
30
|
+
json_transaction = @transaction_serializers.build(@config, transaction).to_json
|
31
|
+
|
32
|
+
# add to json traces array
|
33
|
+
json_traces.push(json_transaction)
|
34
|
+
end
|
35
|
+
|
36
|
+
pid = $PID || Process.pid
|
37
|
+
host_name = @config.hostname || `hostname`
|
38
|
+
date_now = Time.now
|
39
|
+
current_time = date_now.strftime("%Y-%m-%d, %H:%M:%S.%6N")
|
40
|
+
trace_path = @config.log_trace_path
|
41
|
+
trace_file_result = @config.check_lastlog_needs_new(trace_path)
|
42
|
+
current_trace_file = trace_file_result['latest_file']
|
43
|
+
|
44
|
+
if trace_file_result['new_flagger'] == true
|
45
|
+
@trace_file_counter = @trace_file_counter + 1
|
46
|
+
file_ctr = @trace_file_counter
|
47
|
+
current_trace_file = trace_path + host_name + "#" + pid.to_s + "-" + file_ctr.to_s + ".log"
|
48
|
+
else
|
49
|
+
if @process_id != pid
|
50
|
+
@trace_file_counter = 1
|
51
|
+
@process_id = pid
|
52
|
+
file_ctr = @trace_file_counter
|
53
|
+
current_trace_file = trace_path + host_name + "#" + pid.to_s + "-" + file_ctr.to_s + ".log"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
current_trace_file = current_trace_file.gsub(/[[:space:]]/, '')
|
58
|
+
open(current_trace_file, 'a') {|f|
|
59
|
+
json_traces.each do |json_trace|
|
60
|
+
f.puts current_time + "> " + json_trace + "\n"
|
61
|
+
end
|
62
|
+
}
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This class creates and initializes a new transaction.
|
4
|
+
#
|
5
|
+
|
6
|
+
module StackifyRubyAPM
|
7
|
+
require 'securerandom'
|
8
|
+
# @api private
|
9
|
+
class Transaction
|
10
|
+
DEFAULT_TYPE = 'custom'.freeze
|
11
|
+
|
12
|
+
# rubocop:disable Metrics/MethodLength
|
13
|
+
def initialize instrumenter, name = nil, type = nil, context: nil
|
14
|
+
# puts "Loads transaction new initialize instrumenter, name = nil, type = nil, context: nil"
|
15
|
+
@id = SecureRandom.uuid
|
16
|
+
@instrumenter = instrumenter
|
17
|
+
@name = name
|
18
|
+
@type = type || DEFAULT_TYPE
|
19
|
+
@timestamp = (Time.now).to_f * 1000
|
20
|
+
|
21
|
+
@spans = []
|
22
|
+
@span_id_ticker = -1
|
23
|
+
@dropped_spans = 0
|
24
|
+
|
25
|
+
@notifications = [] # for AS::Notifications
|
26
|
+
@context = context || Context.new
|
27
|
+
|
28
|
+
yield self if block_given?
|
29
|
+
end
|
30
|
+
# rubocop:enable Metrics/MethodLength
|
31
|
+
|
32
|
+
attr_accessor :name, :type, :http_status
|
33
|
+
attr_reader :id, :context, :duration, :dropped_spans,
|
34
|
+
:timestamp, :spans, :result, :notifications, :instrumenter
|
35
|
+
|
36
|
+
def release
|
37
|
+
@instrumenter.current_transaction = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def done result = nil
|
41
|
+
@duration = (Time.now).to_f * 1000
|
42
|
+
@result = result
|
43
|
+
@http_status = result
|
44
|
+
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def done?
|
49
|
+
!!@duration
|
50
|
+
end
|
51
|
+
|
52
|
+
def submit result = nil, status: nil, headers: {}
|
53
|
+
done result unless duration
|
54
|
+
if status
|
55
|
+
context.response = Context::Response.new(status, headers: headers)
|
56
|
+
end
|
57
|
+
|
58
|
+
release
|
59
|
+
@instrumenter.submit_transaction self
|
60
|
+
|
61
|
+
self
|
62
|
+
end
|
63
|
+
# rubocop:disable Metrics/MethodLength
|
64
|
+
def span name, type = nil, backtrace: nil, context: nil
|
65
|
+
span = build_and_start_span(name, type, context, backtrace)
|
66
|
+
return span unless block_given?
|
67
|
+
|
68
|
+
begin
|
69
|
+
result = yield span
|
70
|
+
ensure
|
71
|
+
span.done
|
72
|
+
end
|
73
|
+
|
74
|
+
result
|
75
|
+
end
|
76
|
+
# rubocop:enable Metrics/MethodLength
|
77
|
+
|
78
|
+
def current_span
|
79
|
+
spans.reverse.lazy.find(&:running?)
|
80
|
+
end
|
81
|
+
|
82
|
+
def inspect
|
83
|
+
"<StackifyRubyAPM::Transaction id:#{id}" \
|
84
|
+
" name:#{name.inspect}" \
|
85
|
+
" type:#{type.inspect}" \
|
86
|
+
'>'
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def next_span_id
|
92
|
+
@span_id_ticker += 1
|
93
|
+
end
|
94
|
+
|
95
|
+
def next_span name, type, context
|
96
|
+
Span.new(
|
97
|
+
self,
|
98
|
+
next_span_id,
|
99
|
+
name,
|
100
|
+
type,
|
101
|
+
parent_id: current_span.nil? ? -1 : current_span.id,
|
102
|
+
context: context,
|
103
|
+
http_status: @http_status
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def span_frames_min_duration?
|
108
|
+
@instrumenter.agent.config.span_frames_min_duration != 0
|
109
|
+
end
|
110
|
+
|
111
|
+
def build_and_start_span name, type, context, backtrace
|
112
|
+
span = next_span(name, type, context)
|
113
|
+
spans << span
|
114
|
+
|
115
|
+
if backtrace && span_frames_min_duration?
|
116
|
+
span.original_backtrace = backtrace
|
117
|
+
end
|
118
|
+
|
119
|
+
span.start
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# This module utilizes or handles the time usage or duration of transaction
|
2
|
+
module StackifyRubyAPM
|
3
|
+
# @api private
|
4
|
+
module Util
|
5
|
+
def self.nearest_minute(target = Time.now.utc)
|
6
|
+
target - target.to_i % 60
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.micros(target = Time.now.utc)
|
10
|
+
target.to_i * 1_000_000 + target.usec
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.inspect_transaction(transaction)
|
14
|
+
Inspector.new.transaction transaction
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.git_sha
|
18
|
+
sha = `git rev-parse --verify HEAD 2>&1`.chomp
|
19
|
+
$? && $?.success? ? sha : nil # rubocop:disable Style/SpecialGlobalVars
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
require 'stackify/util/inspector'
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Backport Enumerable#dig to Ruby < 2.3
|
2
|
+
#
|
3
|
+
# Implementation from
|
4
|
+
# https://github.com/Invoca/ruby_dig/blob/master/lib/ruby_dig.rb
|
5
|
+
|
6
|
+
# @api private
|
7
|
+
module RubyDig
|
8
|
+
def dig(key, *rest)
|
9
|
+
value = self[key]
|
10
|
+
|
11
|
+
if value.nil? || rest.empty?
|
12
|
+
value
|
13
|
+
elsif value.respond_to?(:dig)
|
14
|
+
value.dig(*rest)
|
15
|
+
else
|
16
|
+
raise TypeError, "#{value.class} does not respond to `#dig'"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
if RUBY_VERSION < '2.3'
|
22
|
+
# @api private
|
23
|
+
class Array
|
24
|
+
include RubyDig
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
class Hash
|
29
|
+
include RubyDig
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module StackifyRubyAPM
|
2
|
+
# rubocop:disable all
|
3
|
+
module Util
|
4
|
+
# From https://github.com/rails/rails/blob/v5.2.0/activesupport/lib/active_support/inflector/methods.rb#L254-L332
|
5
|
+
module Inflector
|
6
|
+
extend self
|
7
|
+
|
8
|
+
#
|
9
|
+
# Tries to find a constant with the name specified in the argument string.
|
10
|
+
#
|
11
|
+
# constantize('Module') # => Module
|
12
|
+
# constantize('Foo::Bar') # => Foo::Bar
|
13
|
+
#
|
14
|
+
# The name is assumed to be the one of a top-level constant, no matter
|
15
|
+
# whether it starts with "::" or not. No lexical context is taken into
|
16
|
+
# account:
|
17
|
+
#
|
18
|
+
# C = 'outside'
|
19
|
+
# module M
|
20
|
+
# C = 'inside'
|
21
|
+
# C # => 'inside'
|
22
|
+
# constantize('C') # => 'outside', same as ::C
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# NameError is raised when the name is not in CamelCase or the constant is
|
26
|
+
# unknown.
|
27
|
+
def constantize(camel_cased_word)
|
28
|
+
names = camel_cased_word.split("::".freeze)
|
29
|
+
|
30
|
+
# Trigger a built-in NameError exception including the ill-formed constant in the message.
|
31
|
+
Object.const_get(camel_cased_word) if names.empty?
|
32
|
+
|
33
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
34
|
+
names.shift if names.size > 1 && names.first.empty?
|
35
|
+
|
36
|
+
names.inject(Object) do |constant, name|
|
37
|
+
if constant == Object
|
38
|
+
constant.const_get(name)
|
39
|
+
else
|
40
|
+
candidate = constant.const_get(name)
|
41
|
+
next candidate if constant.const_defined?(name, false)
|
42
|
+
next candidate unless Object.const_defined?(name)
|
43
|
+
|
44
|
+
# Go down the ancestors to check if it is owned directly. The check
|
45
|
+
# stops when we reach Object or the end of ancestors tree.
|
46
|
+
constant = constant.ancestors.inject(constant) do |const, ancestor|
|
47
|
+
break const if ancestor == Object
|
48
|
+
break ancestor if ancestor.const_defined?(name, false)
|
49
|
+
const
|
50
|
+
end
|
51
|
+
|
52
|
+
# owner is in Object, so raise
|
53
|
+
constant.const_get(name, false)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Tries to find a constant with the name specified in the argument string.
|
59
|
+
#
|
60
|
+
# safe_constantize('Module') # => Module
|
61
|
+
# safe_constantize('Foo::Bar') # => Foo::Bar
|
62
|
+
#
|
63
|
+
# The name is assumed to be the one of a top-level constant, no matter
|
64
|
+
# whether it starts with "::" or not. No lexical context is taken into
|
65
|
+
# account:
|
66
|
+
#
|
67
|
+
# C = 'outside'
|
68
|
+
# module M
|
69
|
+
# C = 'inside'
|
70
|
+
# C # => 'inside'
|
71
|
+
# safe_constantize('C') # => 'outside', same as ::C
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# +nil+ is returned when the name is not in CamelCase or the constant (or
|
75
|
+
# part of it) is unknown.
|
76
|
+
#
|
77
|
+
# safe_constantize('blargle') # => nil
|
78
|
+
# safe_constantize('UnknownModule') # => nil
|
79
|
+
# safe_constantize('UnknownModule::Foo::Bar') # => nil
|
80
|
+
def safe_constantize(camel_cased_word)
|
81
|
+
constantize(camel_cased_word)
|
82
|
+
rescue NameError => e
|
83
|
+
raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
|
84
|
+
e.name.to_s == camel_cased_word.to_s)
|
85
|
+
rescue ArgumentError => e
|
86
|
+
raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
# rubocop:enable all
|
91
|
+
end
|