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