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