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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4f5b0114ca432b384986bef0196ff170ddc28d29
4
+ data.tar.gz: 8c5850539e077d39f1b0542a9bc4c80a9d5c21cf
5
+ SHA512:
6
+ metadata.gz: 51f466a4778791cb024cb072a38e590cf8f07e09a7e202be694d6bb6a97be1ea501ccb79a057c68c538e1a18186b8a375b894d8133845eb6c44608d6ec3a6681
7
+ data.tar.gz: a858da77af22d0f32415377fdd454effb880fccb7ebf2e82d2045c1da1d9d451634a5405539cae2ebcb040db701b659296828428e06c6b44f72129a3836dd29c
data/.gitignore ADDED
@@ -0,0 +1,76 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ # Ignore bundler config.
14
+ /.bundle
15
+
16
+ # Ignore all logfiles and tempfiles.
17
+ /log/*
18
+ /tmp/*
19
+ !/log/.keep
20
+ !/tmp/.keep
21
+
22
+ # Ignore bundler config.
23
+ /.bundle
24
+
25
+ # Deps
26
+ node_modules
27
+
28
+ # IDE
29
+ .idea
30
+
31
+ # VS Code
32
+ .vscode
33
+ .vs
34
+
35
+ # Unnecessary since this will be a plugin
36
+ package-lock.json
37
+
38
+ # Folder config file
39
+ Desktop.ini
40
+
41
+ # Windows shortcuts
42
+ *.lnk
43
+
44
+ ### OSX ###
45
+ /**/.DS_Store
46
+ /.DS_Store
47
+ .AppleDouble
48
+ .LSOverride
49
+
50
+ # Thumbnails
51
+ ._*
52
+
53
+ # Files that might appear on external disk
54
+ .Spotlight-V100
55
+ .Trashes
56
+
57
+ ### Linux ###
58
+ *~
59
+
60
+ # Windows image file caches
61
+ Thumbs.db
62
+ ehthumbs.db
63
+
64
+ # Ignore .byebug_history, .rspec, brakeman.html
65
+ .byebug_history
66
+ .rspec
67
+ brakeman.html
68
+
69
+
70
+ # Gem
71
+ *.gem
72
+
73
+ #bin
74
+ bin
75
+ bin/*
76
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in stackify-ruby-apm.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,68 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ stackify-ruby-apm (0.9.0)
5
+ concurrent-ruby (~> 1.0)
6
+ delegate_matcher (~> 0.4)
7
+ webmock (~> 3.4)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ addressable (2.5.2)
13
+ public_suffix (>= 2.0.2, < 4.0)
14
+ concurrent-ruby (1.1.3)
15
+ crack (0.4.3)
16
+ safe_yaml (~> 1.0.0)
17
+ delegate_matcher (0.4.3)
18
+ proc_extensions (~> 0.2)
19
+ diff-lcs (1.3)
20
+ file-tail (1.2.0)
21
+ tins (~> 1.0)
22
+ hashdiff (0.3.7)
23
+ proc_extensions (0.2)
24
+ sourcify (~> 0.5)
25
+ public_suffix (3.0.3)
26
+ rake (10.5.0)
27
+ rspec (3.8.0)
28
+ rspec-core (~> 3.8.0)
29
+ rspec-expectations (~> 3.8.0)
30
+ rspec-mocks (~> 3.8.0)
31
+ rspec-core (3.8.0)
32
+ rspec-support (~> 3.8.0)
33
+ rspec-expectations (3.8.2)
34
+ diff-lcs (>= 1.2.0, < 2.0)
35
+ rspec-support (~> 3.8.0)
36
+ rspec-mocks (3.8.0)
37
+ diff-lcs (>= 1.2.0, < 2.0)
38
+ rspec-support (~> 3.8.0)
39
+ rspec-support (3.8.0)
40
+ ruby2ruby (2.4.1)
41
+ ruby_parser (~> 3.1)
42
+ sexp_processor (~> 4.6)
43
+ ruby_parser (3.11.0)
44
+ sexp_processor (~> 4.9)
45
+ safe_yaml (1.0.4)
46
+ sexp_processor (4.11.0)
47
+ sourcify (0.5.0)
48
+ file-tail (>= 1.0.5)
49
+ ruby2ruby (>= 1.2.5)
50
+ ruby_parser (>= 2.0.5)
51
+ sexp_processor (>= 3.0.5)
52
+ tins (1.19.0)
53
+ webmock (3.4.2)
54
+ addressable (>= 2.3.6)
55
+ crack (>= 0.3.2)
56
+ hashdiff
57
+
58
+ PLATFORMS
59
+ ruby
60
+
61
+ DEPENDENCIES
62
+ bundler (~> 1.16)
63
+ rake (~> 10.0)
64
+ rspec (~> 3.0)
65
+ stackify-ruby-apm!
66
+
67
+ BUNDLED WITH
68
+ 1.16.4
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+
2
+ # Stackify Ruby APM
3
+
4
+ ## Installation Guide
5
+
6
+ 1. Install **Stackify Linux Agent**.
7
+
8
+ 2. Check that your setup meets our system requirements.
9
+ * Ruby version 2.5+
10
+ * Framework
11
+ * Ruby on Rails 5.2+
12
+ * Operating System
13
+ * Linux
14
+
15
+ 3. Add `gem 'stackify-ruby-apm'` to your `Gemfile`.
16
+
17
+ 4. In the root folder of your Rails app create a file `config/stackify_apm.yml` and then add this configuration
18
+ ```yaml
19
+ application_name: 'Ruby Application'
20
+ environment_name: 'Production'
21
+ ```
22
+ 5. Build/Deploy your application:
23
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Make bundler auto-require work
4
+ require 'stackify_ruby_apm'
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # The Agent interacts with the Web Application and Profiler.
4
+ # It is also responsible in requesting the instrumenter to build the transactions and spans.
5
+ #
6
+
7
+ require 'stackify/naively_hashable'
8
+ require 'stackify/context_builder'
9
+ require 'stackify/error_builder'
10
+ require 'stackify/stacktrace_builder'
11
+ require 'stackify/error'
12
+ require 'stackify/trace_logger'
13
+ require 'stackify/spies'
14
+ require 'stackify/serializers'
15
+ require 'stackify/worker'
16
+
17
+ module StackifyRubyAPM
18
+ class Agent
19
+ include Log
20
+
21
+ LOCK = Mutex.new
22
+
23
+ def self.instance # rubocop:disable Style/TrivialAccessors
24
+ @instance
25
+ end
26
+
27
+ def self.start(config)
28
+ return @instance if @instance
29
+ config = Config.new(config) unless config.is_a?(Config)
30
+ LOCK.synchronize do
31
+ return @instance if @instance
32
+ @instance = new(config).start
33
+ end
34
+ end
35
+
36
+ def self.stop
37
+ LOCK.synchronize do
38
+ return unless @instance
39
+
40
+ @instance.stop
41
+ @instance = nil
42
+ end
43
+ end
44
+
45
+ def stop
46
+ @instrumenter.stop
47
+ kill_worker
48
+ self
49
+ end
50
+
51
+ def self.running?
52
+ !!@instance
53
+ end
54
+
55
+ def initialize(config)
56
+ # puts '@stackify_ruby [Agent] [lib/agent.rb] initialize()'
57
+ @config = config
58
+ @trace_logger = TraceLogger.new(config)
59
+ @messages = Queue.new
60
+ @pending_transactions = Queue.new
61
+ @instrumenter = Instrumenter.new(self)
62
+ @context_builder = ContextBuilder.new(self)
63
+ @error_builder = ErrorBuilder.new(self)
64
+ @stacktrace_builder = StacktraceBuilder.new(self)
65
+ end
66
+
67
+ attr_reader :config,
68
+ :messages,
69
+ :pending_transactions,
70
+ :instrumenter,
71
+ :context_builder,
72
+ :stacktrace_builder,
73
+ :trace_logger,
74
+ :error_builder
75
+
76
+ def start
77
+ # puts '@stackify_ruby [Agent] [lib/agent.rb] start()'
78
+ # puts '@stackify_ruby [Agent] [lib/agent.rb] Environment: ' + @config.environment_name
79
+ # puts '@stackify_ruby [Agent] [lib/agent.rb] Application Name: ' + @config.application_name
80
+ # @instrumenter.start
81
+ debug 'Loaded spies:'
82
+ config.enabled_spies.each do |lib|
83
+ debug lib.inspect
84
+ require "stackify/spies/#{lib}"
85
+ end
86
+ self
87
+ end
88
+
89
+ # queues
90
+
91
+ # Stores transaction in queue
92
+ #
93
+ def enqueue_transaction(transaction)
94
+ boot_worker unless worker_running?
95
+ pending_transactions.push(transaction)
96
+ return unless should_flush_transactions?
97
+
98
+ messages.push(Worker::FlushMsg.new)
99
+ end
100
+
101
+ def should_flush_transactions?
102
+ return true unless config.flush_interval
103
+ return true if pending_transactions.length >= config.max_queue_size
104
+
105
+ false
106
+ end
107
+
108
+ def enqueue_error(error)
109
+ boot_worker unless worker_running?
110
+
111
+ messages.push(Worker::ErrorMsg.new(error))
112
+ end
113
+
114
+ # Instrumentation
115
+ #
116
+ def current_transaction
117
+ instrumenter.current_transaction
118
+ end
119
+
120
+ # Loads transaction
121
+ #
122
+ def transaction(*args, &block)
123
+ instrumenter.transaction(*args, &block)
124
+ end
125
+
126
+ def span(*args, &block)
127
+ instrumenter.span(*args, &block)
128
+ end
129
+
130
+ # Responsible for building the transaction's context
131
+ #
132
+ def build_context(rack_env)
133
+ @context_builder.build(rack_env)
134
+ end
135
+
136
+ # errors
137
+
138
+ def report(exception, handled: true)
139
+ return if config.filter_exception_types.include?(exception.class.to_s)
140
+
141
+ error = @error_builder.build_exception(
142
+ exception,
143
+ handled: handled
144
+ )
145
+ enqueue_error error
146
+ end
147
+
148
+ def report_message(message, backtrace: nil, **attrs)
149
+ error = @error_builder.build_log(
150
+ message,
151
+ backtrace: backtrace,
152
+ **attrs
153
+ )
154
+ enqueue_error error
155
+ end
156
+
157
+ private
158
+
159
+ def boot_worker
160
+ debug 'Booting worker'
161
+
162
+ @worker_thread = Thread.new do
163
+ Worker.new(
164
+ config,
165
+ messages,
166
+ pending_transactions,
167
+ trace_logger
168
+ ).run_forever
169
+ end
170
+ end
171
+
172
+ def kill_worker
173
+ messages << Worker::StopMsg.new
174
+
175
+ if @worker_thread && !@worker_thread.join(5) # 5 secs
176
+ raise 'Failed to wait for worker, not all messages sent'
177
+ end
178
+
179
+ @worker_thread = nil
180
+ end
181
+
182
+ def worker_running?
183
+ @worker_thread && @worker_thread.alive?
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # The Config class sets the APM's default values.
4
+ #
5
+
6
+ require 'logger'
7
+ require 'yaml'
8
+ require 'socket'
9
+ module StackifyRubyAPM
10
+ # rubocop:disable Metrics/ClassLength
11
+ # @api private
12
+ class Config
13
+ DEFAULTS = {
14
+ config_file: 'config/stackify_apm.yml',
15
+ environment_name: ENV['RAILS_ENV'] || ENV['RACK_ENV'],
16
+ instrument: true,
17
+
18
+ log_path: '/usr/local/stackify/stackify-ruby-apm/log/stackify-ruby-apm.log',
19
+ log_level: Logger::DEBUG,
20
+
21
+ log_trace_path: '/usr/local/stackify/stackify-ruby-apm/log/',
22
+
23
+ max_queue_size: 500, # Maximum queue length of transactions before sending transactions to the APM.
24
+ flush_interval: 1, # interval with which transactions should be sent to the APM. Default value: 10 seconds
25
+
26
+ filter_exception_types: [],
27
+
28
+ http_status: nil,
29
+ debug_transactions: false,
30
+
31
+ source_lines_error_app_frames: 5,
32
+ source_lines_span_app_frames: 5,
33
+ source_lines_error_library_frames: 0,
34
+ source_lines_span_library_frames: 0,
35
+ span_frames_min_duration: -1, # it will collect stack traces for all spans
36
+
37
+ disabled_spies: %w[json],
38
+
39
+ view_paths: [],
40
+ root_path: Dir.pwd
41
+ }.freeze
42
+
43
+ ENV_TO_KEY = {
44
+ 'STACKIFY_APM_ENVIRONMENT_NAME' => 'environment_name',
45
+ 'STACKIFY_APM_INSTRUMENT' => [:bool, 'instrument'],
46
+ 'STACKIFY_APM_HOSTNAME' => 'hostname',
47
+ 'STACKIFY_APM_LOG_PATH' => 'log_path',
48
+ 'STACKIFY_APM_LOG_LEVEL' => [:int, 'log_level'],
49
+ 'STACKIFY_APM_APPLICATION_NAME' => 'application_name',
50
+ 'STACKIFY_APM_SOURCE_LINES_ERROR_APP_FRAMES' => [:int, 'source_lines_error_app_frames'],
51
+ 'STACKIFY_APM_SOURCE_LINES_SPAN_APP_FRAMES' => [:int, 'source_lines_span_app_frames'],
52
+ 'STACKIFY_APM_SOURCE_LINES_ERROR_LIBRARY_FRAMES' => [:int, 'source_lines_error_library_frames'],
53
+ 'STACKIFY_APM_SOURCE_LINES_SPAN_LIBRARY_FRAMES' => [:int, 'source_lines_span_library_frames'],
54
+ 'STACKIFY_APM_SPAN_FRAMES_MIN_DURATION' => [:int, 'span_frames_min_duration'],
55
+ 'STACKIFY_APM_MAX_QUEUE_SIZE' => [:int, 'max_queue_size'],
56
+ 'STACKIFY_APM_FLUSH_INTERVAL' => 'flush_interval',
57
+ 'STACKIFY_APM_DISABLED_SPIES' => [:list, 'disabled_spies']
58
+ }.freeze
59
+
60
+ def initialize(options = {})
61
+ set_defaults
62
+
63
+ set_from_args(options)
64
+ set_from_config_file
65
+ set_from_env
66
+
67
+ yield self if block_given?
68
+
69
+ build_logger if logger.nil? || log_path
70
+ end
71
+
72
+ attr_accessor :config_file
73
+ attr_accessor :environment_name
74
+ attr_accessor :instrument
75
+ attr_accessor :enabled_environments
76
+
77
+ attr_accessor :application_name
78
+ attr_accessor :hostname
79
+
80
+ attr_accessor :log_path
81
+ attr_accessor :log_level
82
+ attr_accessor :logger
83
+
84
+ attr_accessor :log_trace_path
85
+ attr_accessor :source_lines_error_app_frames
86
+ attr_accessor :source_lines_span_app_frames
87
+ attr_accessor :source_lines_error_library_frames
88
+ attr_accessor :source_lines_span_library_frames
89
+ attr_accessor :span_frames_min_duration
90
+
91
+ attr_accessor :max_queue_size
92
+ attr_accessor :flush_interval
93
+
94
+ attr_accessor :filter_exception_types
95
+
96
+ attr_accessor :debug_transactions
97
+
98
+ attr_accessor :disabled_spies
99
+
100
+ attr_accessor :view_paths
101
+ attr_accessor :root_path
102
+ attr_accessor :http_status
103
+
104
+ def app=(app)
105
+ case app_type?(app)
106
+ when :rails
107
+ set_rails(app)
108
+ else
109
+ # TODO: define custom?
110
+ self.application_name = 'ruby'
111
+ end
112
+ end
113
+
114
+ def app_type?(app)
115
+ if defined?(::Rails) && app.is_a?(Rails::Application)
116
+ return :rails
117
+ end
118
+ nil
119
+ end
120
+
121
+ # rubocop:disable Metrics/MethodLength
122
+ def available_spies
123
+ %w[
124
+ action_dispatch
125
+ mongo
126
+ net_http
127
+ httpclient
128
+ sinatra
129
+ tilt
130
+ ]
131
+ end
132
+ # rubocop:enable Metrics/MethodLength
133
+
134
+ def enabled_spies
135
+ available_spies - disabled_spies
136
+ end
137
+
138
+ def check_lastlog_needs_new(path)
139
+ latest_file = Dir.glob("#{path}*").grep(/([#])\w+/).max_by {|f| File.mtime(f)}
140
+ new_flagger = false
141
+
142
+ if latest_file.nil?
143
+ new_flagger = true
144
+ else
145
+ current_kb_size = ((File.size(latest_file)) / 1000).to_f
146
+
147
+ #50000KB = 50MB
148
+ if current_kb_size > 50000.0
149
+ new_flagger = true
150
+ end
151
+ end
152
+ result = {
153
+ 'latest_file' => latest_file,
154
+ 'new_flagger' => new_flagger
155
+ }
156
+
157
+ end
158
+
159
+ private
160
+
161
+ def assign(options)
162
+ options.each do |key, value|
163
+ send("#{key}=", value)
164
+ end
165
+ end
166
+
167
+ def set_defaults
168
+ assign(DEFAULTS)
169
+ end
170
+
171
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
172
+ def set_from_env
173
+ ENV_TO_KEY.each do |env_key, key|
174
+ next unless (value = ENV[env_key])
175
+
176
+ type, key = key if key.is_a? Array
177
+
178
+ value =
179
+ case type
180
+ when :int then value.to_i
181
+ when :float then value.to_f
182
+ when :bool then !%w[0 false].include?(value.strip.downcase)
183
+ when :list then value.split(/[ ,]/)
184
+ else value
185
+ end
186
+
187
+ send("#{key}=", value)
188
+ end
189
+ end
190
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
191
+
192
+ def set_from_args(options)
193
+ assign(options)
194
+ end
195
+
196
+ def set_from_config_file
197
+ return unless File.exist?(config_file)
198
+ assign(YAML.load_file(config_file) || {})
199
+ end
200
+
201
+ def set_rails(app) # rubocop:disable Metrics/AbcSize
202
+ self.application_name ||= format_name(application_name || app.class.parent_name)
203
+ self.logger ||= Rails.logger
204
+
205
+ self.root_path = Rails.root.to_s
206
+ self.view_paths = app.config.paths['app/views'].existent
207
+ end
208
+
209
+ def build_logger
210
+ logger = Logger.new(log_path == '-' ? $stdout : log_path)
211
+ logger.level = log_level
212
+
213
+ self.logger = logger
214
+ end
215
+
216
+ def format_name(str)
217
+ str.gsub('::', '_')
218
+ end
219
+ end
220
+ # rubocop:enable Metrics/ClassLength
221
+ end