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,59 @@
1
+ module StackifyRubyAPM
2
+ module Util
3
+ # @api private
4
+ class Inspector
5
+ def initialize(width = 80)
6
+ @width = width
7
+ end
8
+
9
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
10
+ def transaction(transaction)
11
+ unless transaction.done?
12
+ raise ArgumentError, 'Transaction still running'
13
+ end
14
+
15
+ width_factor = @width.to_f / ms(transaction.duration)
16
+
17
+ lines = ['=' * @width]
18
+ lines << "[T] #{transaction.name} " \
19
+ "- #{transaction.type} (#{ms transaction.duration} ms)"
20
+ lines << "+#{'-' * (@width - 2)}+"
21
+
22
+ transaction.spans.each do |span|
23
+ indent = (ms(span.relative_start) * width_factor).to_i
24
+
25
+ if span.duration
26
+ span_width = ms(span.duration) * width_factor
27
+ duration_desc = ms(span.duration)
28
+ else
29
+ span_width = @width - indent
30
+ duration_desc = 'RUNNING'
31
+ end
32
+
33
+ description = "[#{span.id}] " \
34
+ "#{span.name} - #{span.type} (#{duration_desc} ms)"
35
+ description_indent = [
36
+ 0,
37
+ [indent, @width - description.length].min
38
+ ].max
39
+
40
+ lines << "#{' ' * description_indent}#{description}"
41
+ lines << "#{' ' * indent}+#{'-' * [(span_width - 2), 0].max}+"
42
+ end
43
+
44
+ lines.map { |s| s[0..@width] }.join("\n")
45
+ rescue StandardError => e
46
+ puts e
47
+ puts e.backtrace.join("\n")
48
+ nil
49
+ end
50
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
51
+
52
+ private
53
+
54
+ def ms(micros)
55
+ micros.to_f / 1_000
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ module Util
5
+ # @api private
6
+ class LruCache
7
+ def initialize(max_size = 512, &block)
8
+ @max_size = max_size
9
+ @data = Hash.new(&block)
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def [](key)
14
+ @mutex.synchronize do
15
+ val = @data[key]
16
+ return unless val
17
+ add(key, val)
18
+ val
19
+ end
20
+ end
21
+
22
+ def []=(key, val)
23
+ @mutex.synchronize do
24
+ add(key, val)
25
+ end
26
+ end
27
+
28
+ def length
29
+ @data.length
30
+ end
31
+
32
+ def to_a
33
+ @data.to_a
34
+ end
35
+
36
+ private
37
+
38
+ def add(key, val)
39
+ @data.delete(key)
40
+ @data[key] = val
41
+
42
+ return unless @data.length > @max_size
43
+
44
+ @data.delete(@data.first[0])
45
+ end
46
+ end
47
+ end
48
+ end
49
+
@@ -0,0 +1,4 @@
1
+ # Sets the version of the APM
2
+ module StackifyRubyAPM
3
+ VERSION = '0.9.0'.freeze
4
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This class will receive the transactions and send it to HTTP/TraceLogger.
4
+ # Responsible for sending transactions to APM for every 10 secs (config.flush_interval default)
5
+ # Worker will constantly execute transaction sending to APM until the Agent is stopped.
6
+ #
7
+
8
+ require 'concurrent/timer_task'
9
+
10
+ module StackifyRubyAPM
11
+ # @api private
12
+ class Worker
13
+ include Log
14
+
15
+ # @api private
16
+ class StopMsg; end
17
+
18
+ # @api private
19
+ class FlushMsg; end
20
+
21
+ # @api private
22
+ class ErrorMsg
23
+ def initialize(error)
24
+ @error = error
25
+ end
26
+
27
+ attr_reader :error
28
+ end
29
+
30
+ def initialize(config, messages, pending_transactions, trace_logger)
31
+ @config = config
32
+ @messages = messages
33
+ @pending_transactions = pending_transactions
34
+ @trace_logger = trace_logger
35
+
36
+ @serializers = Struct.new(:transactions, :errors).new(
37
+ Serializers::Transactions.new(config),
38
+ Serializers::Errors.new(config)
39
+ )
40
+ end
41
+
42
+ attr_reader :config, :messages, :pending_transactions
43
+
44
+ # rubocop:disable Metrics/MethodLength
45
+ # Collects and sends the transactions
46
+ #
47
+ def run_forever
48
+ @timer_task = build_timer_task.execute
49
+
50
+ while (msg = messages.pop)
51
+ case msg
52
+ when ErrorMsg
53
+ post_error msg
54
+ when FlushMsg
55
+ collect_and_send_transactions
56
+ when StopMsg
57
+ # empty collected transactions before exiting
58
+ collect_and_send_transactions
59
+ stop!
60
+ end
61
+ end
62
+ end
63
+ # rubocop:enable Metrics/MethodLength
64
+
65
+ private
66
+
67
+ def stop!
68
+ @timer_task && @timer_task.shutdown
69
+ Thread.exit
70
+ end
71
+
72
+ # flush_interval - interval with which transactions should be sent to the APM. Default value: 10 seconds
73
+ # The running task responsible for initiating the transactions sending
74
+ #
75
+ def build_timer_task
76
+ Concurrent::TimerTask.new(execution_interval: config.flush_interval) do
77
+ messages.push(FlushMsg.new)
78
+ end
79
+ end
80
+
81
+ # Collects, builds, and sends transactions via HTTP/TraceLogger
82
+ #
83
+ def collect_and_send_transactions
84
+ return if pending_transactions.empty?
85
+ transactions = collect_batched_transactions
86
+ debug '@stackify_ruby [lib/worker.rb] Successfully collect and send transaction to the to trace logger.'
87
+ begin
88
+ @trace_logger.post(transactions)
89
+ rescue ::Exception => e
90
+ debug 'Failed posting: '
91
+ debug e.inspect
92
+ debug e.backtrace.join("\n")
93
+ nil
94
+ end
95
+ end
96
+
97
+ def post_error(msg)
98
+ return if pending_transactions.empty?
99
+ transactions = collect_batched_transactions
100
+ payload = @serializers.errors.build_all([msg.error])
101
+ debug '@stackify_ruby [lib/worker.rb] error'
102
+ # @trace_logger.post(payload, transactions[0])
103
+ end
104
+
105
+ def collect_batched_transactions
106
+ batch = []
107
+
108
+ begin
109
+ while (transaction = pending_transactions.pop(true)) &&
110
+ batch.length <= config.max_queue_size
111
+ batch << transaction
112
+ end
113
+ rescue ThreadError # queue empty
114
+ end
115
+
116
+ batch
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # The entry point once the server starts after the Stackify APM has been integrated.
4
+ #
5
+ # Checks if Web App's framework is Rails or Sinatra:
6
+ # if Rails
7
+ # -> call lib/stackify/railtie.rb to start Agent
8
+ # elsif Sinatra
9
+ # -> access the Web App's /config.ru file to start Agent
10
+ # end
11
+ #
12
+ # Incoming requests are then handled by the Middleware (middleware.rb)
13
+ #
14
+
15
+ require 'stackify/log'
16
+ require 'stackify/version'
17
+ require 'stackify/util/dig'
18
+
19
+ require 'stackify/agent'
20
+ require 'stackify/config'
21
+ require 'stackify/logger'
22
+ require 'stackify/context'
23
+ require 'stackify/instrumenter'
24
+ require 'stackify/internal_error'
25
+ require 'stackify/util'
26
+
27
+ require 'stackify/middleware'
28
+
29
+ # Checks if the framework using is Rails
30
+ require 'stackify/railtie' if defined?(::Rails::Railtie)
31
+
32
+ module StackifyRubyAPM
33
+ # Starts the StackifyRubyAPM Agent
34
+ #
35
+ # @param config [Config] An instance of Config
36
+ # @return [Agent] The resulting [Agent]
37
+ def self.start(config = {})
38
+ # debug "Starts the StackifyRubyAPM Agent self.start()"
39
+
40
+ Agent.start config
41
+ end
42
+
43
+ # Stops the StackifyRubyAPM Agent
44
+ def self.stop
45
+ Agent.stop
46
+ end
47
+
48
+ # @return [Boolean] Whether there's an [Agent] running
49
+ def self.running?
50
+ Agent.running?
51
+ end
52
+
53
+ # @return [Agent] Currently running [Agent] if any
54
+ def self.agent
55
+ Agent.instance
56
+ end
57
+ ### Metrics
58
+
59
+ # Returns the currently active transaction (if any)
60
+ #
61
+ # @return [Transaction] if any
62
+ def self.current_transaction
63
+ agent && agent.current_transaction
64
+ end
65
+
66
+ # Start a new transaction or return the currently running
67
+ #
68
+ # @param name [String] A description of the transaction, eg
69
+ # `ExamplesController#index`
70
+ # @param type [String] The kind of the transaction, eg `app.request.get` or
71
+ # `db.mysql2.query`
72
+ # @param context [Context] An optional [Context]
73
+ # @yield [Transaction] Optional block encapsulating transaction
74
+ # @return [Transaction] Unless block given
75
+ def self.transaction(name = nil, type = nil, context: nil, &block)
76
+ # debug "@stackify_ruby [lib/stackify_ruby_apm.rb] loads self.transaction(name = nil, type = nil, context: nil, &block)"
77
+ return (block_given? ? yield : nil) unless agent
78
+
79
+ agent.transaction(name, type, context: context, &block)
80
+ end
81
+
82
+ # Starts a new span under the current transaction
83
+ #
84
+ # @param name [String] A description of the span, eq `SELECT FROM "users"`
85
+ # @param type [String] The kind of span, eq `db.mysql2.query`
86
+ # @param context [Span::Context] Context information about the span
87
+ # @yield [Span] Optional block encapsulating span
88
+ # @return [Span] Unless block given
89
+ def self.span(name, type = nil, context: nil, include_stacktrace: true,
90
+ &block)
91
+ return (block_given? ? yield : nil) unless agent
92
+
93
+ agent.span(
94
+ name,
95
+ type,
96
+ context: context,
97
+ backtrace: include_stacktrace ? caller : nil,
98
+ &block
99
+ )
100
+ end
101
+
102
+ # Build a [Context] from a Rack `env`. The context may include information
103
+ # about the request, response, current user and more
104
+ #
105
+ # @param rack_env [Rack::Env] A Rack env
106
+ # @return [Context] The built context
107
+ def self.build_context(rack_env)
108
+ # debug "@stackify_ruby [lib/stackify_ruby_apm.rb] self.build_context(rack_env)"
109
+ agent && agent.build_context(rack_env)
110
+ end
111
+
112
+ ### Errors
113
+
114
+ # Report and exception to APM
115
+ #
116
+ # @param exception [Exception] The exception
117
+ # @param handled [Boolean] Whether the exception was rescued
118
+ # @return [Error] The generated [Error]
119
+ def self.report(exception, handled: true)
120
+ agent && agent.report(exception, handled: handled)
121
+ end
122
+
123
+ # Report a custom string error message to APM
124
+ #
125
+ # @param message [String] The message
126
+ # @return [Error] The generated [Error]
127
+ def self.report_message(message, **attrs)
128
+ agent && agent.report_message(message, backtrace: caller, **attrs)
129
+ end
130
+ end
@@ -0,0 +1,30 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "stackify/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "stackify-ruby-apm"
8
+ spec.version = StackifyRubyAPM::VERSION
9
+ spec.authors = ["Stackify"]
10
+ spec.email = ["support@stackify.com"]
11
+ spec.summary = 'Stackify APM for Ruby'
12
+ spec.homepage = 'http://www.stackify.com'
13
+ spec.license = 'Stackify'
14
+
15
+ # Specify which files should be added to the gem when it is released.
16
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|docs)/}) }
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.16"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_dependency('concurrent-ruby', '~> 1.0')
28
+ spec.add_dependency('delegate_matcher', '~> 0.4')
29
+ spec.add_dependency('webmock', '~> 3.4')
30
+ end
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stackify-ruby-apm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Stackify
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-11-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: concurrent-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: delegate_matcher
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.4'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.4'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.4'
97
+ description:
98
+ email:
99
+ - support@stackify.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - Gemfile
107
+ - Gemfile.lock
108
+ - README.md
109
+ - Rakefile
110
+ - lib/stackify-ruby-apm.rb
111
+ - lib/stackify/agent.rb
112
+ - lib/stackify/config.rb
113
+ - lib/stackify/context.rb
114
+ - lib/stackify/context/request.rb
115
+ - lib/stackify/context/request/socket.rb
116
+ - lib/stackify/context/request/url.rb
117
+ - lib/stackify/context/response.rb
118
+ - lib/stackify/context_builder.rb
119
+ - lib/stackify/error.rb
120
+ - lib/stackify/error/exception.rb
121
+ - lib/stackify/error/log.rb
122
+ - lib/stackify/error_builder.rb
123
+ - lib/stackify/instrumenter.rb
124
+ - lib/stackify/internal_error.rb
125
+ - lib/stackify/log.rb
126
+ - lib/stackify/logger.rb
127
+ - lib/stackify/middleware.rb
128
+ - lib/stackify/naively_hashable.rb
129
+ - lib/stackify/normalizers.rb
130
+ - lib/stackify/normalizers/action_controller.rb
131
+ - lib/stackify/normalizers/action_mailer.rb
132
+ - lib/stackify/normalizers/action_view.rb
133
+ - lib/stackify/normalizers/active_record.rb
134
+ - lib/stackify/railtie.rb
135
+ - lib/stackify/root_info.rb
136
+ - lib/stackify/serializers.rb
137
+ - lib/stackify/serializers/errors.rb
138
+ - lib/stackify/serializers/transactions.rb
139
+ - lib/stackify/span.rb
140
+ - lib/stackify/span/context.rb
141
+ - lib/stackify/spies.rb
142
+ - lib/stackify/spies/action_dispatch.rb
143
+ - lib/stackify/spies/httpclient.rb
144
+ - lib/stackify/spies/mongo.rb
145
+ - lib/stackify/spies/net_http.rb
146
+ - lib/stackify/spies/sinatra.rb
147
+ - lib/stackify/spies/tilt.rb
148
+ - lib/stackify/stacktrace.rb
149
+ - lib/stackify/stacktrace/frame.rb
150
+ - lib/stackify/stacktrace_builder.rb
151
+ - lib/stackify/subscriber.rb
152
+ - lib/stackify/trace_logger.rb
153
+ - lib/stackify/transaction.rb
154
+ - lib/stackify/util.rb
155
+ - lib/stackify/util/dig.rb
156
+ - lib/stackify/util/inflector.rb
157
+ - lib/stackify/util/inspector.rb
158
+ - lib/stackify/util/lru_cache.rb
159
+ - lib/stackify/version.rb
160
+ - lib/stackify/worker.rb
161
+ - lib/stackify_ruby_apm.rb
162
+ - stackify-ruby-apm.gemspec
163
+ homepage: http://www.stackify.com
164
+ licenses:
165
+ - Stackify
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubyforge_project:
183
+ rubygems_version: 2.5.2.3
184
+ signing_key:
185
+ specification_version: 4
186
+ summary: Stackify APM for Ruby
187
+ test_files: []