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