stackify-ruby-apm 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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