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,50 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This class loads the specified config of the Rails app and starts the Agent.
4
+ #
5
+ # APM Config setting is done at stackify/config.rb
6
+ #
7
+ # After the Agent has been started:
8
+ # -> the enabled Spies (libraries) are registered
9
+ # -> the Subscriber is called to register the events
10
+ # -> the Middleware is then inserted
11
+ #
12
+
13
+ require 'stackify/subscriber'
14
+
15
+ module StackifyRubyAPM
16
+ # @api private
17
+ # Railtie - core of the Rails framework and provides several hooks to modify the initialization process.
18
+ class Railtie < Rails::Railtie
19
+
20
+ config.stackify = ActiveSupport::OrderedOptions.new
21
+
22
+ Config::DEFAULTS.each { |option, value| config.stackify[option] = value }
23
+
24
+ # This will overwrite the default values of the config based from stackify_apm.yml initialized
25
+
26
+ initializer 'stackify_apm.initialize' do |app|
27
+ # puts '@stackify_ruby [Railtie] [lib/railtie.rb] initializer'
28
+ config = app.config.stackify.merge(app: app).tap do |c|
29
+ # Prepend Rails.root to log_path if present
30
+ if c.log_path && !c.log_path.start_with?('/')
31
+ c.log_path = Rails.root.join(c.log_path)
32
+ end
33
+ end
34
+
35
+ begin
36
+ agent = StackifyRubyAPM.start config
37
+ if agent
38
+ agent.instrumenter.subscriber = StackifyRubyAPM::Subscriber.new(agent)
39
+ app.middleware.insert 0, Middleware # add a middleware to the application
40
+ end
41
+ rescue StandardError => e
42
+ Rails.logger.error "[Railtie] Failed to start: #{e.message}"
43
+ end
44
+ end
45
+
46
+ config.after_initialize do
47
+ require 'stackify/spies/action_dispatch'
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This will format the root information of the Transaction.
4
+ #
5
+
6
+ module StackifyRubyAPM
7
+ # @api private
8
+ class RootInfo
9
+ def initialize(config, transaction)
10
+ @transaction = transaction
11
+ @config = config
12
+ end
13
+
14
+ # rubocop:disable Metrics/MethodLength
15
+ # rubocop:disable Style/StringLiterals
16
+ def build
17
+ # get process id
18
+ pid = $PID || Process.pid
19
+
20
+ base = {
21
+ CATEGORY: 'Ruby',
22
+ APPLICATION_PATH: '/',
23
+ APPLICATION_FILESYSTEM_PATH: @config.root_path,
24
+ APPLICATION_NAME: @config.application_name,
25
+ APPLICATION_ENV: @config.environment_name,
26
+ REPORTING_URL: @transaction.name,
27
+ METHOD: @transaction.context.request.method,
28
+ STATUS: @transaction.context.response.status_code,
29
+ URL: @transaction.context.request.url[:full],
30
+ TRACETYPE: 'WEBAPP',
31
+ TRACE_ID: @transaction.id,
32
+ THREAD_ID: Thread.current.object_id,
33
+ TRACE_SOURCE: 'Ruby',
34
+ TRACE_TARGET: 'RETRACE',
35
+ HOST_NAME: @config.hostname || `hostname`,
36
+ OS_TYPE: 'LINUX',
37
+ PROCESS_ID: pid,
38
+ TRACE_VERSION: '2.0'
39
+ }
40
+ end
41
+ # rubocop:enable Metrics/MethodLength
42
+
43
+ def self.build(config, transaction)
44
+ new(config, transaction).build
45
+ end
46
+
47
+ private
48
+
49
+ def runtime
50
+ case RUBY_ENGINE
51
+ when 'ruby'
52
+ { name: RUBY_ENGINE, version: RUBY_VERSION }
53
+ when 'jruby'
54
+ { name: RUBY_ENGINE, version: ENV['JRUBY_VERSION'] }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+ # After the spans are being build it will serialize or rebuild or reformat the spans created into json format
4
+ module StackifyRubyAPM
5
+ # @api private
6
+ module Serializers
7
+ # @api private
8
+ class Serializer
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ private
14
+
15
+ def micros_to_time(micros)
16
+ Time.at(ms(micros) / 1_000)
17
+ end
18
+
19
+ def ms(micros)
20
+ micros.to_f / 1_000
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ require 'stackify/serializers/transactions'
27
+ require 'stackify/serializers/errors'
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This module gathers the built Errors and Exceptions into hash formats
4
+ #
5
+ module StackifyRubyAPM
6
+ module Serializers
7
+ # @api private
8
+ class Errors < Serializer
9
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
10
+ def build(error)
11
+
12
+ if (exception = error.exception)
13
+
14
+ base = {
15
+ CaughtBy: exception.module,
16
+ Exception: exception.type,
17
+ Message: exception.message,
18
+ Timestamp: micros_to_time(error.timestamp).utc.iso8601(3),
19
+ Frames: exception.stacktrace.to_a,
20
+ }
21
+
22
+ end
23
+
24
+ base
25
+ end
26
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
27
+
28
+ def build_all(errors)
29
+ { exceptions: Array(errors).map(&method(:build)) }
30
+ end
31
+
32
+ private
33
+
34
+ def build_log(log)
35
+ {
36
+ message: log.message,
37
+ level: log.level,
38
+ logger_name: log.logger_name,
39
+ param_message: log.param_message,
40
+ stacktrace: log.stacktrace.to_a
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This class builds and reformats all the gathered spans within the transaction.
4
+ #
5
+
6
+ module StackifyRubyAPM
7
+ module Serializers
8
+ # @api private
9
+
10
+ class Transactions < Serializer
11
+ include Log
12
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
13
+ def build(config, transaction)
14
+
15
+ root_span_json = {
16
+ id: -1,
17
+ call: transaction.name,
18
+ reqBegin: transaction.timestamp,
19
+ reqEnd: transaction.duration,
20
+ props: RootInfo.build(config, transaction)
21
+ }
22
+
23
+ # serialize all the spans
24
+ children_spans_json = []
25
+ transaction.spans.each do |span|
26
+ children_spans_json.push(build_span(span))
27
+ end
28
+
29
+ add_children_spans(root_span_json, children_spans_json)
30
+
31
+ root_span_json
32
+ end
33
+
34
+ private
35
+
36
+ # Add Children Spans to span_json
37
+ def add_children_spans(span_json, children_spans_json)
38
+ id = span_json[:id]
39
+ children_spans_json.each do |child_span_json|
40
+ parent_id = child_span_json[:parent_id]
41
+ if parent_id == id
42
+ child_span_copy = child_span_json.dup
43
+ add_children_spans(child_span_copy, children_spans_json)
44
+ if span_json[:stacks].nil?
45
+ span_json[:stacks] = []
46
+ end
47
+ span_json[:stacks].push(child_span_copy)
48
+ end
49
+ end
50
+ end
51
+
52
+ # rubocop:disable Metrics/AbcSize
53
+ def build_span(span)
54
+ span_type = span.type
55
+ span_context = span.context && span.context.to_h
56
+
57
+ {
58
+ id: span.id,
59
+ parent_id: span.parent_id,
60
+ call: span_type,
61
+ reqBegin: span.relative_start,
62
+ reqEnd: span.relative_end,
63
+ props: span_context.nil? ? { CATEGORY: 'Ruby' } : span_context,
64
+ stacks: []
65
+ }
66
+ end
67
+
68
+ # rubocop:enable Metrics/AbcSize
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This modules creates and initializes new Span
4
+ #
5
+
6
+ require 'securerandom'
7
+ require 'stackify/span/context'
8
+ module StackifyRubyAPM
9
+ # @api private
10
+ class Span
11
+ DEFAULT_TYPE = 'custom'.freeze
12
+
13
+ # rubocop:disable Metrics/ParameterLists
14
+ def initialize(
15
+ transaction,
16
+ id,
17
+ name,
18
+ type = nil,
19
+ parent_id: nil,
20
+ context: nil,
21
+ http_status: nil
22
+ )
23
+ @transaction = transaction
24
+ @id = id
25
+ @name = name
26
+ @type = type || DEFAULT_TYPE
27
+ @parent_id = parent_id
28
+ @context = context
29
+ @http_status = http_status
30
+
31
+ @stacktrace = nil
32
+ @original_backtrace = nil
33
+ end
34
+ # rubocop:enable Metrics/ParameterLists
35
+
36
+ attr_accessor :name, :type, :original_backtrace, :http_status
37
+ attr_reader :id, :context, :stacktrace, :duration, :parent_id, :relative_start, :relative_end
38
+
39
+ def start
40
+ @relative_start = (Time.now).to_f * 1000
41
+
42
+ self
43
+ end
44
+
45
+ def done
46
+ @relative_end = (Time.now).to_f * 1000
47
+ @duration = (Time.now).to_f * 1000
48
+ self.original_backtrace = nil # release it
49
+
50
+ self
51
+ end
52
+
53
+ def done?
54
+ !!duration
55
+ end
56
+
57
+ def running?
58
+ relative_start && !done?
59
+ end
60
+
61
+ def inspect
62
+ "<StackifyRubyAPM::Span id:#{id}" \
63
+ " name:#{name.inspect}" \
64
+ " type:#{type.inspect}" \
65
+ '>'
66
+ end
67
+
68
+ private
69
+
70
+ end
71
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ module StackifyRubyAPM
3
+ class Span
4
+ # @api private
5
+ class Context
6
+ include NaivelyHashable
7
+
8
+ def initialize(**args)
9
+ args.each do |key, val|
10
+ send(:"#{key}=", val)
11
+ end
12
+ end
13
+
14
+ attr_accessor :CATEGORY,
15
+ :SUBCATEGORY,
16
+ :COMPONENT_CATEGORY,
17
+ :COMPONENT_DETAIL,
18
+ :SQL,
19
+ :MONGODB_COLLECTION,
20
+ :METHOD,
21
+ :URL,
22
+ :STATUS,
23
+ :PROVIDER
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This module communicates and registers the additional or enabled outside library method
4
+ #
5
+
6
+ require 'forwardable'
7
+ require 'stackify/util/inflector'
8
+
9
+ module StackifyRubyAPM
10
+ # @api private
11
+ module Spies
12
+ # @api private
13
+ class Registration
14
+ extend Forwardable
15
+
16
+ def initialize(const_name, require_paths, spy)
17
+ @const_name = const_name
18
+ @require_paths = Array(require_paths)
19
+ @spy = spy
20
+ end
21
+
22
+ attr_reader :const_name, :require_paths
23
+
24
+ def_delegator :@spy, :install
25
+ end
26
+
27
+ def self.require_hooks
28
+ @require_hooks ||= {}
29
+ end
30
+
31
+ def self.installed
32
+ @installed ||= {}
33
+ end
34
+
35
+ def self.register(*args)
36
+ registration = Registration.new(*args)
37
+
38
+ if safe_defined?(registration.const_name)
39
+ registration.install
40
+ installed[registration.const_name] = registration
41
+ else
42
+ register_require_hook registration
43
+ end
44
+ end
45
+
46
+ def self.register_require_hook(registration)
47
+ registration.require_paths.each do |path|
48
+ require_hooks[path] = registration
49
+ end
50
+ end
51
+
52
+ def self.hook_into(name)
53
+ return unless (registration = require_hooks[name])
54
+ return unless safe_defined?(registration.const_name)
55
+
56
+ installed[registration.const_name] = registration
57
+ registration.install
58
+
59
+ registration.require_paths.each do |path|
60
+ require_hooks.delete path
61
+ end
62
+ end
63
+
64
+ def self.safe_defined?(const_name)
65
+ Util::Inflector.safe_constantize(const_name)
66
+ end
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ module Kernel
72
+ private
73
+
74
+ alias require_without_apm require
75
+
76
+ def require(path)
77
+ res = require_without_apm(path)
78
+
79
+ begin
80
+ StackifyRubyAPM::Spies.hook_into(path)
81
+ rescue ::Exception => e
82
+ puts "Failed hooking into '#{path}'. Please report this at " \
83
+ 'git@bitbucket.org:stackify/stackify-ruby-apm.git'
84
+ puts e.backtrace.join("\n")
85
+ end
86
+
87
+ res
88
+ end
89
+ end