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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ class InternalError < StandardError; end
5
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ # @api private
5
+ # This module will format the process logs perform between the Agent and Web App
6
+ module Log
7
+ PREFIX = '[StackifyRubyAPM] '.freeze
8
+
9
+ def debug(msg, *args, &block)
10
+ log(:debug, msg, *args, &block)
11
+ end
12
+
13
+ def info(msg, *args, &block)
14
+ log(:info, msg, *args, &block)
15
+ end
16
+
17
+ def warn(msg, *args, &block)
18
+ log(:warn, msg, *args, &block)
19
+ end
20
+
21
+ def error(msg, *args, &block)
22
+ log(:error, msg, *args, &block)
23
+ end
24
+
25
+ def fatal(msg, *args, &block)
26
+ log(:fatal, msg, *args, &block)
27
+ end
28
+
29
+ def log(lvl, msg, *args)
30
+ return unless logger
31
+
32
+ formatted_msg = prepend_prefix(format(msg.to_s, *args))
33
+
34
+ return logger.send(lvl, formatted_msg) unless block_given?
35
+
36
+ # TODO: dont evaluate block if level is higher
37
+ logger.send(lvl, "#{formatted_msg}\n#{yield}")
38
+ end
39
+
40
+ private
41
+
42
+ def prepend_prefix(str)
43
+ "#{PREFIX}#{str}"
44
+ end
45
+
46
+ def logger
47
+ return false unless (config = instance_variable_get(:@config))
48
+ config.logger
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,10 @@
1
+ module StackifyRubyAPM
2
+ # Starts the Monkey patch for the generated log by not including the header
3
+ #
4
+ # @param file
5
+ # @removed the header of the generated log
6
+ class Logger::LogDevice
7
+ def add_log_header(file)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This class monkey patches the Rack Middleware method: e.g., call(env)
4
+ # It also handles the submission of transactions to Agent.
5
+ #
6
+ # If Agent is running, build_transaction(env) is called:
7
+ #
8
+ # -> BUILDS transaction: /stackify_ruby_apm.rb (#self.transaction)
9
+ # -> builds a [Context] from a Rack `env`: /stackify_ruby_apm.rb (#self.build_context)
10
+ # -> context may include information about the request,
11
+ # response, current user and more
12
+ #
13
+ # -> SUBMITS transaction: stackify/transaction.rb (#submit)
14
+ # -> instrumenter submits transaction
15
+ # -> worker will run (if != running), responsible for sending transactions to APM
16
+ # for every 10 secs (based on config.flush_interval)
17
+ # -> transaction will be stored in queue by Agent
18
+ # -> worker will constantly execute transaction sending to APM
19
+ #
20
+
21
+ module StackifyRubyAPM
22
+
23
+ class Middleware
24
+ def initialize(app)
25
+ # puts "@stackify_ruby [Middleware] [lib/middleware.rb] initialize(app)"
26
+ @app = app
27
+ end
28
+
29
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
30
+ # This is where the requests are received, built into a transaction, and then submitted
31
+ #
32
+ def call(env)
33
+ begin
34
+
35
+ #if running? && !path_ignored?(env)
36
+ if running?
37
+ transaction = build_transaction(env)
38
+ end
39
+
40
+ resp = @app.call env
41
+ # puts "@stackify_ruby [Middleware] [lib/middleware.rb] Loads call(env) in middleware module"
42
+
43
+ submit_transaction(transaction, *resp) if transaction
44
+ rescue InternalError
45
+ raise # Don't report StackifyRubyAPM errors
46
+ rescue ::Exception => e
47
+ #puts "@stackify_ruby [Middleware] [lib/middleware.rb] Error Message: #{e.message}"
48
+ transaction.submit('500', status: 500) if transaction
49
+ raise
50
+ ensure
51
+ transaction.release if transaction
52
+ end
53
+
54
+ resp
55
+ end
56
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
57
+
58
+ def submit_transaction(transaction, status, headers, _body)
59
+ result = status.to_i
60
+ transaction.submit(result, status: status, headers: headers)
61
+ end
62
+
63
+ # Start of transaction building with params: name, type, context
64
+ #
65
+ def build_transaction(env)
66
+ # puts "@stackify_ruby [Middleware] [lib/middleware.rb] middleware build transaction env"
67
+ StackifyRubyAPM.transaction 'Rack', 'request', context: StackifyRubyAPM.build_context(env)
68
+ end
69
+
70
+ def running?
71
+ StackifyRubyAPM.running?
72
+ end
73
+
74
+ def config
75
+ StackifyRubyAPM.agent.config
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This module converts its result to a hash.
4
+ #
5
+
6
+ module StackifyRubyAPM
7
+ # @api private
8
+ module NaivelyHashable
9
+ def naively_hashable?
10
+ true
11
+ end
12
+
13
+ def to_h
14
+ instance_variables.each_with_object({}) do |name, h|
15
+
16
+ key = name.to_s.delete('@').to_sym
17
+ value = instance_variable_get(name)
18
+ is_hashable =
19
+ value.respond_to?(:naively_hashable?) && value.naively_hashable?
20
+
21
+ h[key] = is_hashable ? value.to_h : value
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Collects and registers existing events and classes
4
+ #
5
+
6
+ module StackifyRubyAPM # :nodoc:
7
+ # @api private
8
+ module Normalizers
9
+ # @api privagte
10
+ class Normalizer
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+
15
+ def self.register(name)
16
+ Normalizers.register(name, self)
17
+ end
18
+ end
19
+
20
+ def self.register(name, klass)
21
+ @registered ||= {}
22
+ @registered[name] = klass
23
+ end
24
+
25
+ def self.build(config)
26
+ # puts '@stackify_ruby [Normalizers] [lib/normalizer.rb] self.build(config)'
27
+ normalizers = @registered.each_with_object({}) do |(name, klass), built|
28
+ built[name] = klass.new(config)
29
+ end
30
+ Collection.new(normalizers)
31
+ end
32
+
33
+ # @api private
34
+ class Collection
35
+ # @api private
36
+ class SkipNormalizer
37
+ def initialize; end
38
+
39
+ def normalize(*_args)
40
+ :skip
41
+ end
42
+ end
43
+
44
+ def initialize(normalizers)
45
+ @normalizers = normalizers
46
+ @default = SkipNormalizer.new
47
+ end
48
+
49
+ def for(name)
50
+ @normalizers.fetch(name, @default)
51
+ end
52
+
53
+ def keys
54
+ @normalizers.keys
55
+ end
56
+
57
+ def normalize(transaction, name, payload)
58
+ self.for(name).normalize(transaction, name, payload)
59
+ end
60
+ end
61
+ end
62
+
63
+ %w[
64
+ action_controller
65
+ action_mailer
66
+ action_view
67
+ active_record
68
+ ].each do |lib|
69
+ require "stackify/normalizers/#{lib}"
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ module Normalizers
5
+ module ActionController
6
+ # @api private
7
+ class ProcessActionNormalizer < Normalizer
8
+ register 'process_action.action_controller'
9
+ TYPE = 'app.controller.action'.freeze
10
+
11
+ def normalize(transaction, _name, payload)
12
+ transaction.name = endpoint(payload)
13
+ [transaction.name, TYPE, nil]
14
+ end
15
+
16
+ private
17
+
18
+ def endpoint(payload)
19
+ "#{payload[:controller]}##{payload[:action]}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ module Normalizers
5
+ module ActionMailer
6
+ # @api private
7
+ class ProcessActionNormalizer < Normalizer
8
+ register 'process.action_mailer'
9
+ TYPE = 'app.mailer.action'.freeze
10
+
11
+ def normalize(_transaction, _name, payload)
12
+ [endpoint(payload), TYPE, nil]
13
+ end
14
+
15
+ private
16
+
17
+ def endpoint(payload)
18
+ "#{payload[:mailer]}##{payload[:action]}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ module Normalizers
5
+ module ActionView
6
+ # @api private
7
+ class RenderNormalizer < Normalizer
8
+ private
9
+
10
+ def normalize_render(payload, type)
11
+ [path_for(payload[:identifier]), type, nil]
12
+ end
13
+
14
+ def path_for(path)
15
+ return 'Unknown template' unless path
16
+ return path unless path.start_with?('/')
17
+
18
+ view_path(path) || gem_path(path) || 'Absolute path'
19
+ end
20
+
21
+ def view_path(path)
22
+ root = @config.view_paths.find { |vp| path.start_with?(vp) }
23
+ return unless root
24
+
25
+ strip_root(root, path)
26
+ end
27
+
28
+ def gem_path(path)
29
+ root = Gem.path.find { |gp| path.start_with? gp }
30
+ return unless root
31
+
32
+ format '$GEM_PATH/%s', strip_root(root, path)
33
+ end
34
+
35
+ def strip_root(root, path)
36
+ start = root.length + 1
37
+ path[start, path.length]
38
+ end
39
+ end
40
+
41
+ # @api private
42
+ class RenderTemplateNormalizer < RenderNormalizer
43
+ register 'render_template.action_view'
44
+ TYPE = 'template.view'.freeze
45
+
46
+ def normalize(_transaction, _name, payload)
47
+ normalize_render(payload, TYPE)
48
+ end
49
+ end
50
+
51
+ # @api private
52
+ class RenderPartialNormalizer < RenderNormalizer
53
+ register 'render_partial.action_view'
54
+ TYPE = 'template.view.partial'.freeze
55
+
56
+ def normalize(_transaction, _name, payload)
57
+ normalize_render(payload, TYPE)
58
+ end
59
+ end
60
+
61
+ # @api private
62
+ class RenderCollectionNormalizer < RenderNormalizer
63
+ register 'render_collection.action_view'
64
+ TYPE = 'template.view.collection'.freeze
65
+
66
+ def normalize(_transaction, _name, payload)
67
+ normalize_render(payload, TYPE)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ module Normalizers
5
+ module ActiveRecord
6
+ # @api private
7
+ class SqlNormalizer < Normalizer
8
+ include Log
9
+ register 'sql.active_record'
10
+
11
+ def initialize(*args)
12
+ super(*args)
13
+
14
+ @type = format('db.%s.sql', lookup_adapter || 'unknown').freeze
15
+ end
16
+
17
+ def normalize(_transaction, _name, payload)
18
+ return :skip if %w[SCHEMA CACHE].include?(payload[:name])
19
+
20
+ statement = query_variables(payload)
21
+ name = payload[:sql] || payload[:name] || 'Default'
22
+ context = Span::Context.new(statement)
23
+ [name, @type, context]
24
+ end
25
+
26
+ private
27
+
28
+ # return back valid PROVIDER based on driver name passed in
29
+ def get_profiler(driver)
30
+ if driver.to_s.empty?
31
+ "generic"
32
+ elsif driver.include? "mysql"
33
+ "mysql"
34
+ elsif driver.include? "pg"
35
+ "postgresql"
36
+ elsif driver.include? "oci8"
37
+ "oracle"
38
+ elsif driver.include? "db2"
39
+ "db2"
40
+ end
41
+ end
42
+
43
+ def query_variables(payload)
44
+ debug "@stackify_ruby [SqlNormalizer] [normalizers/active_record.rb] query_variables payload:"
45
+ debug payload.inspect
46
+
47
+ if payload[:type_casted_binds]
48
+ row_id = payload[:type_casted_binds][0]
49
+ else
50
+ row_id = []
51
+ end
52
+
53
+ {
54
+ CATEGORY: 'Database',
55
+ SUBCATEGORY: 'Execute',
56
+ COMPONENT_CATEGORY: 'DB Query',
57
+ COMPONENT_DETAIL: 'Execute SQL Query',
58
+ SQL: payload[:sql],
59
+ PROVIDER: get_profiler(lookup_adapter)
60
+ }
61
+ end
62
+
63
+ def lookup_adapter
64
+ ::ActiveRecord::Base.connection.adapter_name.downcase
65
+ rescue StandardError
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end