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,24 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This class initializes the parameters and variables for the context of Transaction/Span
4
+ #
5
+
6
+ require 'stackify/context/request'
7
+ require 'stackify/context/request/socket'
8
+ require 'stackify/context/request/url'
9
+ require 'stackify/context/response'
10
+
11
+ module StackifyRubyAPM
12
+ # @api private
13
+ class Context
14
+ include NaivelyHashable
15
+
16
+ attr_accessor :request, :response
17
+ attr_reader :custom, :tags
18
+
19
+ def initialize
20
+ @custom = {}
21
+ @tags = {}
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ class Context
5
+ # @api private
6
+ class Request
7
+ include NaivelyHashable
8
+
9
+ attr_accessor :body, :cookies, :env, :headers, :http_version, :method, :socket, :url
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ # @api private
5
+ class Context
6
+ # @api private
7
+ class Request
8
+ # @api private
9
+ class Socket
10
+ include NaivelyHashable
11
+
12
+ def initialize(req)
13
+ @remote_addr = req.ip
14
+ @encrypted = req.scheme == 'https'
15
+ end
16
+
17
+ attr_reader :remote_addr, :encrypted
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ # @api private
5
+ class Context
6
+ # @api private
7
+ class Request
8
+ # @api private
9
+ class Url
10
+ include NaivelyHashable
11
+
12
+ SKIPPED_PORTS = {
13
+ 'http' => 80,
14
+ 'https' => 443
15
+ }.freeze
16
+
17
+ def initialize(req)
18
+ @protocol = req.scheme
19
+ @hostname = req.host
20
+ @port = req.port.to_s
21
+ @pathname = req.path
22
+ @search = req.query_string
23
+ @hash = nil
24
+ @full = build_full_url req
25
+ end
26
+
27
+ attr_reader :protocol, :hostname, :port, :pathname, :search, :hash,
28
+ :full
29
+
30
+ private
31
+
32
+ def build_full_url(req)
33
+ url = "#{req.scheme}://#{req.host}"
34
+
35
+ if req.port != SKIPPED_PORTS[req.scheme]
36
+ url += ":#{req.port}"
37
+ end
38
+
39
+ url + req.fullpath
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ class Context
5
+ # @api private
6
+ class Response
7
+ include NaivelyHashable
8
+
9
+ def initialize(
10
+ status_code,
11
+ headers: {},
12
+ headers_sent: true,
13
+ finished: true
14
+ )
15
+ @status_code = status_code
16
+ @headers = headers
17
+ @headers_sent = headers_sent
18
+ @finished = finished
19
+ end
20
+
21
+ attr_accessor :status_code, :headers, :headers_sent, :finished
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This class builds the context of the Transaction/Span
4
+ # Context represents the HTTP request information such as headers, cookies, body.
5
+ #
6
+
7
+ module StackifyRubyAPM
8
+ # @api private
9
+ class ContextBuilder
10
+ def initialize(_agent); end
11
+
12
+ # Creates context
13
+ #
14
+ def build(rack_env)
15
+ # puts "@stackify_ruby [lib/context_builder.rb] Loading ContextBuilder build(rack_env)"
16
+ context = Context.new
17
+ apply_to_request(context, rack_env)
18
+ context
19
+ end
20
+
21
+ private
22
+
23
+ # rubocop:disable Metrics/AbcSize
24
+ #
25
+ # Request format and values are assigned to context
26
+ #
27
+ def apply_to_request(context, rack_env)
28
+ # puts "@stackify_ruby [lib/context_builder.rb] Loading ContextBuilder apply_to_request(context, rack_env)"
29
+ req = rails_req?(rack_env) ? rack_env : Rack::Request.new(rack_env)
30
+ context.request = Context::Request.new unless context.request
31
+ request = context.request
32
+ request.socket = Context::Request::Socket.new(req).to_h
33
+ request.http_version = build_http_version rack_env
34
+ request.method = req.request_method
35
+ request.url = Context::Request::Url.new(req).to_h
36
+ request.headers, request.env = get_headers_and_env(rack_env)
37
+ request.body = get_body(req)
38
+
39
+ context
40
+ end
41
+ # rubocop:enable Metrics/AbcSize
42
+
43
+ def get_body(req)
44
+ return req.POST if req.form_data?
45
+
46
+ body = req.body.read
47
+ req.body.rewind
48
+ body
49
+ end
50
+
51
+ def rails_req?(env)
52
+ defined?(ActionDispatch::Request) &&
53
+ env.is_a?(ActionDispatch::Request)
54
+ end
55
+
56
+ def get_headers_and_env(rack_env)
57
+ # In Rails < 5 ActionDispatch::Request inherits from Hash
58
+ headers =
59
+ rack_env.respond_to?(:headers) ? rack_env.headers : rack_env
60
+
61
+ headers.each_with_object([{}, {}]) do |(key, value), (http, env)|
62
+ next unless key == key.upcase
63
+
64
+ if key.start_with?('HTTP_')
65
+ http[camel_key(key)] = value
66
+ else
67
+ env[key] = value
68
+ end
69
+ end
70
+ end
71
+
72
+ def camel_key(key)
73
+ key.gsub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
74
+ end
75
+
76
+ def build_http_version(rack_env)
77
+ return unless (http_version = rack_env['HTTP_VERSION'])
78
+ http_version.gsub(%r{HTTP/}, '')
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stackify/stacktrace'
4
+ require 'stackify/context'
5
+ require 'stackify/error/exception'
6
+ require 'stackify/error/log'
7
+
8
+ module StackifyRubyAPM
9
+ # @api private
10
+ class Error
11
+ def initialize(culprit: nil)
12
+ @id = SecureRandom.uuid
13
+ @culprit = culprit
14
+
15
+ @timestamp = Util.micros
16
+ @context = Context.new
17
+
18
+ @transaction_id = nil
19
+ end
20
+
21
+ attr_accessor :id, :culprit, :exception, :log, :transaction_id, :context
22
+ attr_reader :timestamp
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+
2
+ module StackifyRubyAPM
3
+ class Error
4
+ # @api private
5
+ class Exception
6
+ MOD_SPLIT = '::'.freeze
7
+
8
+ def initialize(exception, **attrs)
9
+ @message =
10
+ "#{exception.class}: #{exception.message}"
11
+ @type = exception.class.to_s
12
+ @module = format_module exception
13
+
14
+ attrs.each do |key, val|
15
+ send(:"#{key}=", val)
16
+ end
17
+ end
18
+
19
+ attr_accessor(
20
+ :attributes,
21
+ :code,
22
+ :handled,
23
+ :message,
24
+ :module,
25
+ :stacktrace,
26
+ :type
27
+ )
28
+
29
+ private
30
+
31
+ def format_module(exception)
32
+ exception.class.to_s.split(MOD_SPLIT)[0...-1].join(MOD_SPLIT)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ class Error
5
+ # @api private
6
+ class Log
7
+ def initialize(message, attrs = {})
8
+ @message = message
9
+
10
+ attrs.each do |key, val|
11
+ send(:"#{key}=", val)
12
+ end
13
+ end
14
+
15
+ attr_accessor(
16
+ :level,
17
+ :logger_name,
18
+ :message,
19
+ :param_message,
20
+ :stacktrace
21
+ )
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This module build errors/exceptions and the Frames/stacktraces affected/related to the error
4
+ #
5
+ module StackifyRubyAPM
6
+ # @api private
7
+ class ErrorBuilder
8
+ def initialize(agent)
9
+ @agent = agent
10
+ end
11
+
12
+ def build_exception(exception, handled: true)
13
+ error = Error.new
14
+ error.exception = Error::Exception.new(exception, handled: handled)
15
+
16
+ if exception.backtrace
17
+ add_stacktrace error, :exception, exception.backtrace
18
+ end
19
+
20
+ add_transaction_id error
21
+
22
+ if (transaction = StackifyRubyAPM.current_transaction)
23
+ error.context = transaction.context.dup
24
+ end
25
+
26
+ error
27
+ end
28
+
29
+ def build_log(message, backtrace: nil, **attrs)
30
+ error = Error.new
31
+ error.log = Error::Log.new(message, **attrs)
32
+
33
+ if backtrace
34
+ add_stacktrace error, :log, backtrace
35
+ end
36
+
37
+ add_transaction_id error
38
+
39
+ error
40
+ end
41
+
42
+ private
43
+
44
+ def add_stacktrace(error, kind, backtrace)
45
+ stacktrace =
46
+ @agent.stacktrace_builder.build(backtrace, type: :error)
47
+ return unless stacktrace
48
+
49
+ case kind
50
+ when :exception
51
+ error.exception.stacktrace = stacktrace
52
+ when :log
53
+ error.log.stacktrace = stacktrace
54
+ end
55
+
56
+ error.culprit = stacktrace.frames.first.function
57
+ end
58
+
59
+ def add_transaction_id(error)
60
+ return unless (transaction = StackifyRubyAPM.current_transaction)
61
+ error.transaction_id = transaction.id
62
+ end
63
+ end
64
+ end
65
+
@@ -0,0 +1,118 @@
1
+ #
2
+ # The Instrumenter will build/process the transactions and spans.
3
+ #
4
+
5
+ require 'stackify/span'
6
+ require 'stackify/transaction'
7
+
8
+ module StackifyRubyAPM
9
+ # @api private
10
+ class Instrumenter
11
+ include Log
12
+
13
+ KEY = :_stackify_transaction_key
14
+
15
+ class TransactionInfo
16
+ def initialize
17
+ self.current = nil
18
+ end
19
+
20
+ def current
21
+ # puts '@stackify_ruby [Instrumenter] [lib/instrumenter.rb] TransactionInfo.current()'
22
+ Thread.current[KEY]
23
+ end
24
+
25
+ def current=(transaction)
26
+ Thread.current[KEY] = transaction
27
+ end
28
+ end
29
+
30
+ def initialize(agent)
31
+ debug '@stackify_ruby [Instrumenter] [lib/instrumenter.rb] initialize()'
32
+ @agent = agent
33
+ @config = agent.config
34
+
35
+ @transaction_info = TransactionInfo.new
36
+ end
37
+
38
+ attr_reader :agent, :config, :pending_transactions
39
+
40
+ def stop
41
+ current_transaction.release if current_transaction
42
+ @subscriber.unregister! if @subscriber
43
+ end
44
+
45
+ def subscriber=(subscriber)
46
+ @subscriber = subscriber
47
+ @subscriber.register!
48
+ end
49
+
50
+ def current_transaction
51
+ @transaction_info.current
52
+ end
53
+
54
+ def current_transaction=(transaction)
55
+ @transaction_info.current = transaction
56
+ end
57
+
58
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
59
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
60
+ #
61
+ # Creates a new transaction or return the currently running
62
+ #
63
+ def transaction(*args)
64
+ debug "Instrumenter function transaction(*args)"
65
+ unless config.instrument
66
+ yield if block_given?
67
+ return
68
+ end
69
+
70
+ if (transaction = current_transaction)
71
+ yield transaction if block_given?
72
+ return transaction
73
+ end
74
+
75
+ transaction = Transaction.new self, *args
76
+
77
+ self.current_transaction = transaction
78
+ return transaction unless block_given?
79
+
80
+ begin
81
+ yield transaction
82
+ ensure
83
+ self.current_transaction = nil
84
+ transaction.done
85
+ end
86
+
87
+ #puts transaction.inspect
88
+
89
+ transaction
90
+ end
91
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
92
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
93
+
94
+ def span(*args, &block)
95
+ unless current_transaction
96
+ return yield if block_given?
97
+ return
98
+ end
99
+
100
+ current_transaction.span(*args, &block)
101
+ end
102
+
103
+ # Once the transaction is submitted it will be stored temporarily in queue
104
+ #
105
+ def submit_transaction(transaction)
106
+ debug '@stackify_ruby [Instrumenter] [lib/instrumenter.rb] submit_transaction(transaction):'
107
+ debug transaction.inspect
108
+ agent.enqueue_transaction transaction
109
+ return unless config.debug_transactions
110
+ end
111
+
112
+ def inspect
113
+ '<StackifyRubyAPM::Instrumenter ' \
114
+ "current_transaction=#{current_transaction.inspect}" \
115
+ '>'
116
+ end
117
+ end
118
+ end