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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +76 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +68 -0
  6. data/README.md +23 -0
  7. data/Rakefile +6 -0
  8. data/lib/stackify-ruby-apm.rb +4 -0
  9. data/lib/stackify/agent.rb +186 -0
  10. data/lib/stackify/config.rb +221 -0
  11. data/lib/stackify/context.rb +24 -0
  12. data/lib/stackify/context/request.rb +12 -0
  13. data/lib/stackify/context/request/socket.rb +21 -0
  14. data/lib/stackify/context/request/url.rb +44 -0
  15. data/lib/stackify/context/response.rb +24 -0
  16. data/lib/stackify/context_builder.rb +81 -0
  17. data/lib/stackify/error.rb +24 -0
  18. data/lib/stackify/error/exception.rb +36 -0
  19. data/lib/stackify/error/log.rb +25 -0
  20. data/lib/stackify/error_builder.rb +65 -0
  21. data/lib/stackify/instrumenter.rb +118 -0
  22. data/lib/stackify/internal_error.rb +5 -0
  23. data/lib/stackify/log.rb +51 -0
  24. data/lib/stackify/logger.rb +10 -0
  25. data/lib/stackify/middleware.rb +78 -0
  26. data/lib/stackify/naively_hashable.rb +25 -0
  27. data/lib/stackify/normalizers.rb +71 -0
  28. data/lib/stackify/normalizers/action_controller.rb +24 -0
  29. data/lib/stackify/normalizers/action_mailer.rb +23 -0
  30. data/lib/stackify/normalizers/action_view.rb +72 -0
  31. data/lib/stackify/normalizers/active_record.rb +71 -0
  32. data/lib/stackify/railtie.rb +50 -0
  33. data/lib/stackify/root_info.rb +58 -0
  34. data/lib/stackify/serializers.rb +27 -0
  35. data/lib/stackify/serializers/errors.rb +45 -0
  36. data/lib/stackify/serializers/transactions.rb +71 -0
  37. data/lib/stackify/span.rb +71 -0
  38. data/lib/stackify/span/context.rb +26 -0
  39. data/lib/stackify/spies.rb +89 -0
  40. data/lib/stackify/spies/action_dispatch.rb +26 -0
  41. data/lib/stackify/spies/httpclient.rb +47 -0
  42. data/lib/stackify/spies/mongo.rb +66 -0
  43. data/lib/stackify/spies/net_http.rb +47 -0
  44. data/lib/stackify/spies/sinatra.rb +50 -0
  45. data/lib/stackify/spies/tilt.rb +28 -0
  46. data/lib/stackify/stacktrace.rb +19 -0
  47. data/lib/stackify/stacktrace/frame.rb +50 -0
  48. data/lib/stackify/stacktrace_builder.rb +101 -0
  49. data/lib/stackify/subscriber.rb +113 -0
  50. data/lib/stackify/trace_logger.rb +66 -0
  51. data/lib/stackify/transaction.rb +123 -0
  52. data/lib/stackify/util.rb +23 -0
  53. data/lib/stackify/util/dig.rb +31 -0
  54. data/lib/stackify/util/inflector.rb +91 -0
  55. data/lib/stackify/util/inspector.rb +59 -0
  56. data/lib/stackify/util/lru_cache.rb +49 -0
  57. data/lib/stackify/version.rb +4 -0
  58. data/lib/stackify/worker.rb +119 -0
  59. data/lib/stackify_ruby_apm.rb +130 -0
  60. data/stackify-ruby-apm.gemspec +30 -0
  61. 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