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