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