stackify-ruby-apm 1.11.1 → 1.12.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stackify_apm/config.rb +32 -5
  3. data/lib/stackify_apm/context.rb +4 -1
  4. data/lib/stackify_apm/context/prefix.rb +30 -0
  5. data/lib/stackify_apm/context/request/headers.rb +30 -0
  6. data/lib/stackify_apm/context_builder.rb +1 -0
  7. data/lib/stackify_apm/helper/database_helper.rb +38 -0
  8. data/lib/stackify_apm/instrumenter_helper.rb +87 -0
  9. data/lib/stackify_apm/middleware.rb +21 -3
  10. data/lib/stackify_apm/normalizers/active_record.rb +16 -8
  11. data/lib/stackify_apm/root_info.rb +6 -0
  12. data/lib/stackify_apm/serializers/transactions.rb +5 -0
  13. data/lib/stackify_apm/span/context.rb +39 -1
  14. data/lib/stackify_apm/spies.rb +4 -2
  15. data/lib/stackify_apm/spies/action_dispatch.rb +6 -1
  16. data/lib/stackify_apm/spies/curb.rb +41 -20
  17. data/lib/stackify_apm/spies/curb/easy.rb +220 -92
  18. data/lib/stackify_apm/spies/curb/multi.rb +26 -12
  19. data/lib/stackify_apm/spies/custom_instrumenter.rb +25 -4
  20. data/lib/stackify_apm/spies/httparty.rb +45 -24
  21. data/lib/stackify_apm/spies/httpclient.rb +41 -20
  22. data/lib/stackify_apm/spies/httprb.rb +39 -18
  23. data/lib/stackify_apm/spies/log4r.rb +59 -0
  24. data/lib/stackify_apm/spies/logger.rb +116 -0
  25. data/lib/stackify_apm/spies/logging.rb +65 -0
  26. data/lib/stackify_apm/spies/net_http.rb +38 -20
  27. data/lib/stackify_apm/spies/redis.rb +36 -30
  28. data/lib/stackify_apm/spies/sequel.rb +28 -11
  29. data/lib/stackify_apm/spies/sinatra_activerecord/mysql_adapter.rb +27 -25
  30. data/lib/stackify_apm/spies/sinatra_activerecord/postgresql_adapter.rb +27 -24
  31. data/lib/stackify_apm/spies/sinatra_activerecord/sqlite_adapter.rb +18 -8
  32. data/lib/stackify_apm/spies/stackify_logger.rb +28 -16
  33. data/lib/stackify_apm/spies/yell.rb +64 -0
  34. data/lib/stackify_apm/util.rb +10 -0
  35. data/lib/stackify_apm/version.rb +1 -1
  36. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5617c10d650b0e32554d8e6ae3deb13c24275b7d2df82f0f1c6b223654397e4d
4
- data.tar.gz: 55a0d2aa8b75165c5f4d7c4cdc0fad19411ae6b3a6e3c57e3d47e13b4f9b37a7
3
+ metadata.gz: ec496ca2f97d6531e0e5059845fb078d60e194f0b2896a5148cc570f8de66c13
4
+ data.tar.gz: 5dbc4a02c01e54b3d1276932f5d7528da2751d734a15926517595528b00dd0a8
5
5
  SHA512:
6
- metadata.gz: d9cd97e0abb2265239113f5afcfa73bb7d9522de34e37137e3da8ab5b49705d96864d8f03c3e55908623e695bfe5a5b981319463e8a0ebf4e085d71c2b020e0d
7
- data.tar.gz: 842839b3d394985cbf583b3b200515272e3c510b377406f0ff0054d2ca85d8145a7f1ae81c3edfec4362f35cf30d5d4f5b2ab17da739a8fba6e8a8b720953866
6
+ metadata.gz: 76c13b39665912b23fa8bd42cb75af0cfa455e83735814f167c768f380b96f411f4267670b8708f15d0aa6de9698d58055fba04ba6f8286dfb1efe51fb724361
7
+ data.tar.gz: 2aa04cf139250db11e987a38ad213863a0d4dab9427561a1a2f63240a42005b37ab1dd9e8ae62df54159d6a05f9d7039df116fa41eea71f00ed69adfc173ebc3
@@ -26,7 +26,7 @@ module StackifyRubyAPM
26
26
  environment_name: ENV['RAILS_ENV'] || ENV['RACK_ENV'],
27
27
  already_instrumented_flag: false,
28
28
  rum_auto_injection: false,
29
- rum_enabled: false,
29
+ rum_enabled: true,
30
30
  rum_cookie_path: '/',
31
31
  rum_cookie_name: '.Stackify.Rum',
32
32
  transport: StackifyRubyAPM::TRACE_LOG,
@@ -70,7 +70,8 @@ module StackifyRubyAPM
70
70
  transport_http_endpoint: 'https://localhost:10601',
71
71
 
72
72
  queue: true,
73
- lambda_handler: ''
73
+ lambda_handler: '',
74
+ prefix_enabled: false
74
75
  }.freeze
75
76
 
76
77
  ENV_TO_KEY = {
@@ -97,7 +98,8 @@ module StackifyRubyAPM
97
98
  'STACKIFY_FLUSH_INTERVAL' => 'flush_interval_seconds',
98
99
  'STACKIFY_DISABLED_SPIES' => [:list, 'disabled_spies'],
99
100
  'STACKIFY_QUEUE' => [:bool, 'queue'],
100
- 'STACKIFY_LAMBDA_HANDLER' => 'lambda_handler'
101
+ 'STACKIFY_LAMBDA_HANDLER' => 'lambda_handler',
102
+ 'STACKIFY_PREFIX_ENABLED' => [:bool, 'prefix_enabled']
101
103
  }.freeze
102
104
 
103
105
  def initialize(options = {})
@@ -105,6 +107,7 @@ module StackifyRubyAPM
105
107
  set_from_args(options)
106
108
  set_from_config_file
107
109
  set_from_env
110
+ set_prefix_paths if @prefix_enabled
108
111
  yield self if block_given?
109
112
  debug_logger
110
113
  StackifyRubyAPM::Util.host_os == 'WINDOWS' ? load_stackify_props_windows : load_stackify_props_linux
@@ -170,6 +173,7 @@ module StackifyRubyAPM
170
173
 
171
174
  attr_accessor :queue
172
175
  attr_accessor :lambda_handler
176
+ attr_accessor :prefix_enabled
173
177
 
174
178
  attr_reader :client_id
175
179
  attr_reader :device_id
@@ -214,6 +218,16 @@ module StackifyRubyAPM
214
218
  ]
215
219
  end
216
220
 
221
+ def prefix_spies
222
+ return [] unless @prefix_enabled
223
+ %w[
224
+ logger
225
+ logging
226
+ log4r
227
+ yell
228
+ ]
229
+ end
230
+
217
231
  def enabled_spies
218
232
  # check if the framework is rails or sinatra
219
233
  sinatra_activerecord_spies = %w[
@@ -233,7 +247,7 @@ module StackifyRubyAPM
233
247
  available_spies
234
248
  end
235
249
 
236
- new_available_spies - disabled_spies
250
+ new_available_spies + prefix_spies - disabled_spies
237
251
  end
238
252
 
239
253
  # Default Transport
@@ -254,7 +268,11 @@ module StackifyRubyAPM
254
268
 
255
269
  def assign(options)
256
270
  options.each do |key, value|
257
- send("#{key}=", value)
271
+ begin
272
+ send("#{key}=", value)
273
+ rescue Exception => e
274
+ info "[Config] Key: '#{key}' doesn't exist."
275
+ end
258
276
  end
259
277
  end
260
278
 
@@ -295,6 +313,14 @@ module StackifyRubyAPM
295
313
  assign(YAML.load_file(config_file) || {})
296
314
  end
297
315
 
316
+ # set log paths to prefix path if prefix_enabled is true
317
+ def set_prefix_paths
318
+ all_user_profile = ENV['ALLUSERSPROFILE'] || "C:\\ProgramData"
319
+
320
+ @log_path = StackifyRubyAPM::Util.host_os == 'WINDOWS' ? "#{all_user_profile}\\Stackify\\Agent\\debug\\stackify-ruby-apm-1.log" : "/usr/local/prefix/debug/stackify-ruby-apm-1.log"
321
+ @log_trace_path = StackifyRubyAPM::Util.host_os == 'WINDOWS' ? "#{all_user_profile}\\Stackify\\Agent\\log\\" : '/usr/local/prefix/log/'
322
+ end
323
+
298
324
  # rubocop:disable Naming/AccessorMethodName
299
325
  def set_rails(app)
300
326
  self.application_name ||= format_name(application_name || app.class.parent_name).strip
@@ -382,6 +408,7 @@ module StackifyRubyAPM
382
408
  info '[Config] environment_name must be String type.' unless @environment_name.is_a?(String) && defined?(@environment_name)
383
409
  info '[Config] transport must be String type.' unless @transport.is_a?(String) && defined?(@transport)
384
410
  info '[Config] Transport should be one of these values: [agent_socket, default, agent_http]. Should be a String.' if defined?(@transport) && !%w[agent_socket default agent_http].include?(@transport.downcase)
411
+ info '[Config] prefix_enabled must be Boolean type: true/false.' unless [TrueClass, FalseClass].include?(@prefix_enabled.class) && defined?(@prefix_enabled)
385
412
  end
386
413
  # rubocop:enable Metrics/CyclomaticComplexity
387
414
  # rubocop:enable Metrics/PerceivedComplexity
@@ -2,7 +2,9 @@
2
2
 
3
3
  # This class initializes the parameters and variables for the context of Transaction/Span
4
4
 
5
+ require 'stackify_apm/context/prefix'
5
6
  require 'stackify_apm/context/request'
7
+ require 'stackify_apm/context/request/headers'
6
8
  require 'stackify_apm/context/request/socket'
7
9
  require 'stackify_apm/context/request/url'
8
10
  require 'stackify_apm/context/response'
@@ -12,12 +14,13 @@ module StackifyRubyAPM
12
14
  class Context
13
15
  include NaivelyHashable
14
16
 
15
- attr_accessor :request, :response, :aws, :category
17
+ attr_accessor :request, :response, :aws, :category, :prefix
16
18
  attr_reader :custom, :tags
17
19
 
18
20
  def initialize
19
21
  @custom = {}
20
22
  @tags = {}
23
+ @prefix = Context::Prefix.new
21
24
  end
22
25
 
23
26
  # add aws context to context instance
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ class Context
5
+ # @api private
6
+ class Prefix
7
+ include NaivelyHashable
8
+
9
+ attr_accessor :response_body, :request_body
10
+ attr_reader :response_headers, :request_headers
11
+
12
+ def request_headers=(headers)
13
+ @request_headers = to_json_list(headers)
14
+ end
15
+
16
+ def response_headers=(headers)
17
+ @response_headers = to_json_list(headers)
18
+ end
19
+
20
+ private
21
+ def to_json_list(header)
22
+ list_header = []
23
+ header.each do |key, value|
24
+ list_header << {key => value}
25
+ end
26
+ list_header.to_json
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StackifyRubyAPM
4
+ # @api private
5
+ class Context
6
+ # @api private
7
+ class Request
8
+ # @api private
9
+ class Headers
10
+ include NaivelyHashable
11
+
12
+ def initialize(req)
13
+ @values = build_headers req
14
+ end
15
+
16
+ attr_reader :values
17
+
18
+ def build_headers req
19
+ env = req.env
20
+ headers = Hash[*env.select {|k,v| k.start_with? 'HTTP_'}
21
+ .collect {|k,v| [k.sub(/^HTTP_/, ''), v]}
22
+ .collect {|k,v| [k.split('_').collect(&:capitalize).join('-'), v]}
23
+ .sort
24
+ .flatten]
25
+ return headers
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -27,6 +27,7 @@ module StackifyRubyAPM
27
27
  request.method = req.request_method
28
28
  request.url = Context::Request::Url.new(req).to_h
29
29
  request.body = get_body(req)
30
+ request.headers = Context::Request::Headers.new(req).values
30
31
 
31
32
  context
32
33
  end
@@ -23,5 +23,43 @@ module DatabaseHelper
23
23
  end
24
24
  # rubocop:enable Metrics/CyclomaticComplexity
25
25
  # rubocop:enable Metrics/PerceivedComplexity
26
+
27
+ # Check the prepared statement by placeholder if its valid
28
+ # placeholder - contains the prepared statement pattern.
29
+ # Placeholder for mysql, sqlite, jdbc oracle, db2 is ?, example "SELECT * FROM posts WHERE author = ? and id = ?"
30
+ # Placeholder for postgres is $1, $2,...$n, example "SELECT * FROM posts WHERE author = $1 and id = $2"
31
+ # statement - contains the db properties such as SQL, PROVIDER, etc.
32
+ # So if there's payload value we append SQL_PARAMETERS to the existing object properties.
33
+ # payload - contains the payload[:binds] which is the payload value/data of the placeholder.
34
+ # Example payload: {:sql=>"SELECT * FROM posts WHERE author = ? and id = ?", :db_adapter=>"mysql2::client", :binds=>["J.K. Rowling", 1]}
35
+ def check_prepared_stmt_by_placeholder(placeholder, statement, payload)
36
+ sqlParam = []
37
+ unless payload[:binds].nil?
38
+ payload[:binds].each_with_index do |record, idx|
39
+ if record && defined?(record.value)
40
+ StackifyRubyAPM::Util.pushToAryIndex(sqlParam, idx, record.value)
41
+ elsif ((record && record.instance_of?(Array)) && defined?(record[1]))
42
+ StackifyRubyAPM::Util.pushToAryIndex(sqlParam, idx, record[1])
43
+ else
44
+ StackifyRubyAPM::Util.pushToAryIndex(sqlParam, idx, record)
45
+ end
46
+ end
47
+ statement[:SQL_PARAMETERS] = sqlParam.to_json if sqlParam.count > 0
48
+ true
49
+ else
50
+ false
51
+ end
52
+ end
53
+
54
+ # Common DB span properties shared to sequel, activerecord, etc.
55
+ def get_common_db_properties
56
+ {
57
+ CATEGORY: 'Database',
58
+ SUBCATEGORY: 'Execute',
59
+ COMPONENT_CATEGORY: 'DB Query',
60
+ COMPONENT_DETAIL: 'Execute SQL Query',
61
+ PROVIDER: 'generic'
62
+ }
63
+ end
26
64
  end
27
65
  include DatabaseHelper
@@ -189,6 +189,93 @@ module StackifyRubyAPM
189
189
  @custom_class_info[current_class.to_s]['controller'] = true if class_path && class_path.include?('controllers')
190
190
  @custom_class_info[current_class.to_s]['model'] = true if class_path && class_path.include?('models')
191
191
  end
192
+
193
+ # Monkey patch the single ruby module file.
194
+ #
195
+ # tracked_func - trackedFunction variable in stackify.json
196
+ # current_module - module variable in stackify.json
197
+ # class_path - file_path variable in stackify.json
198
+ # options - other options such as trackedFunctionName
199
+ def self.patched_module(tracked_func, current_module, class_path, **options)
200
+ current_method = options[:current_method] || nil
201
+ tracked_function_name = options[:tracked_function_name] || nil
202
+ transaction = options[:is_transaction] || nil
203
+ mod_spy = "#{current_module}Spy"
204
+
205
+ unless @custom_instrumented[current_module.to_s]
206
+ @custom_instrumented[current_module.to_s] = {}
207
+ end
208
+
209
+ unless @custom_instrumented[current_module.to_s][current_method.to_s]
210
+ @custom_instrumented[current_module.to_s][current_method.to_s] = false
211
+ end
212
+
213
+ unless @custom_class_info[current_module.to_s]
214
+ @custom_class_info[current_module.to_s] = {}
215
+ end
216
+
217
+ unless @custom_class_info[current_module.to_s]['controller']
218
+ @custom_class_info[current_module.to_s]['controller'] = false
219
+ end
220
+
221
+ unless @custom_class_info[current_module.to_s]['model']
222
+ @custom_class_info[current_module.to_s]['model'] = false
223
+ end
224
+
225
+ return unless @custom_instrumented[current_module.to_s][current_method.to_s] == false
226
+
227
+ eval <<-RUBY
228
+ module #{mod_spy}
229
+ def self.install
230
+ #{current_module}.class_eval do
231
+ class<< self
232
+ alias_method "#{current_method}_without_apm", "#{current_method}"
233
+
234
+ def #{current_method}(*args, &block)
235
+ if StackifyRubyAPM.current_transaction.nil? && #{!transaction.nil?}
236
+ t = StackifyRubyAPM.transaction("custom.#{current_module}.#{current_method}", TRACETYPE)
237
+ begin
238
+ req = #{current_method}_without_apm(*args, &block)
239
+ rescue Exception => e
240
+ StackifyRubyAPM.report(e)
241
+ raise e
242
+ ensure
243
+ t.submit
244
+ end
245
+ return req
246
+ elsif StackifyRubyAPM.current_transaction
247
+ name = "Custom Instrument"
248
+ type = "#{current_module}##{current_method}"
249
+ ctx = if "#{tracked_func}" == 'true'
250
+ Span::Context.new(
251
+ CATEGORY: 'Ruby',
252
+ TRACKED_FUNC: "#{tracked_function_name}"
253
+ )
254
+ else
255
+ Span::Context.new(
256
+ CATEGORY: 'Ruby'
257
+ )
258
+ end
259
+
260
+ StackifyRubyAPM.span name, type, context: ctx do
261
+ #{current_method}_without_apm(*args, &block)
262
+ end
263
+ else
264
+ return #{current_method}_without_apm(*args, &block)
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ StackifyRubyAPM::Spies.register current_module.to_s, class_path.to_s, #{mod_spy}, true, "#{current_method}", "#{tracked_func}", "#{tracked_function_name}"
273
+ RUBY
274
+ @custom_instrumented[current_module.to_s][current_method.to_s] = true
275
+ @custom_class_info[current_module.to_s]['controller'] = true if class_path && class_path.include?('controllers')
276
+ @custom_class_info[current_module.to_s]['model'] = true if class_path && class_path.include?('models')
277
+ end
278
+
192
279
  # rubocop:enable Metrics/CyclomaticComplexity
193
280
  # rubocop:enable Metrics/PerceivedComplexity
194
281
  # rubocop:enable Lint/UselessAssignment
@@ -39,7 +39,8 @@ module StackifyRubyAPM
39
39
  # rubocop:disable Metrics/PerceivedComplexity
40
40
  def call(env)
41
41
  begin
42
- transaction = build_transaction(env) if running?
42
+ context = StackifyRubyAPM.build_context(env)
43
+ transaction = build_transaction(env, context) if running?
43
44
  resp = @app.call env
44
45
 
45
46
  @rack_status = resp[0].to_i
@@ -47,6 +48,8 @@ module StackifyRubyAPM
47
48
  @rack_body = resp[2]
48
49
  @configuration = config
49
50
 
51
+ build_prefix_context(transaction, context)
52
+
50
53
  if okay_to_modify?
51
54
  @configuration.already_instrumented_flag = true
52
55
  if @configuration.rum_enabled.is_a?(TrueClass)
@@ -92,8 +95,9 @@ module StackifyRubyAPM
92
95
 
93
96
  # Start of transaction building with params: name, type, context
94
97
  #
95
- def build_transaction(env)
96
- StackifyRubyAPM.transaction 'Rack', 'WEBAPP', context: StackifyRubyAPM.build_context(env)
98
+ def build_transaction(env, context=nil)
99
+ context = context || StackifyRubyAPM.build_context(env)
100
+ StackifyRubyAPM.transaction 'Rack', 'WEBAPP', context: context
97
101
  end
98
102
 
99
103
  def running?
@@ -129,5 +133,19 @@ module StackifyRubyAPM
129
133
  def xhr?
130
134
  @rack_headers['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
131
135
  end
136
+
137
+ # Add prefix instrument data to transaction context if enabled
138
+ def build_prefix_context(transaction, context)
139
+ return nil unless @configuration.prefix_enabled.is_a?(TrueClass)
140
+
141
+ source = nil
142
+ body = @rack_body
143
+ body.each { |fragment| source ? (source << fragment.to_s) : (source = fragment.to_s) }
144
+
145
+ transaction.context.prefix.request_body = context && context.request && context.request.body || ""
146
+ transaction.context.prefix.request_headers = context && context.request && context.request.headers || Hash.new
147
+ transaction.context.prefix.response_body = source || ""
148
+ transaction.context.prefix.response_headers = @rack_headers || Hash.new
149
+ end
132
150
  end
133
151
  end
@@ -20,6 +20,7 @@ module StackifyRubyAPM
20
20
  return :skip if %w[SCHEMA CACHE].include?(payload[:name])
21
21
 
22
22
  statement = query_variables(payload)
23
+ check_prepared_stmt(statement, payload)
23
24
  name = payload[:sql] || payload[:name] || 'Default'
24
25
  context = Span::Context.new(statement)
25
26
  [name, @type, context]
@@ -28,14 +29,10 @@ module StackifyRubyAPM
28
29
  private
29
30
 
30
31
  def query_variables(payload)
31
- {
32
- CATEGORY: 'Database',
33
- SUBCATEGORY: 'Execute',
34
- COMPONENT_CATEGORY: 'DB Query',
35
- COMPONENT_DETAIL: 'Execute SQL Query',
36
- SQL: payload[:sql],
37
- PROVIDER: get_profiler(lookup_adapter)
38
- }
32
+ props = get_common_db_properties
33
+ props[:PROVIDER] = get_profiler(lookup_adapter)
34
+ props[:SQL] = payload[:sql]
35
+ props
39
36
  end
40
37
 
41
38
  def lookup_adapter
@@ -44,6 +41,17 @@ module StackifyRubyAPM
44
41
  debug '[SqlNormalizer] lookup_adapter err: ' + error.inspect.to_s
45
42
  nil
46
43
  end
44
+
45
+ def check_prepared_stmt(statement, payload)
46
+ if StackifyRubyAPM.agent.config.prefix_enabled
47
+ case get_profiler(lookup_adapter)
48
+ when 'generic', 'mysql', 'sqlite', 'oracle', 'db2'
49
+ check_prepared_stmt_by_placeholder(payload[:sql].include?('?'), statement, payload)
50
+ when 'postgresql'
51
+ check_prepared_stmt_by_placeholder(!!payload[:sql].match(/\$\d/), statement, payload)
52
+ end
53
+ end
54
+ end
47
55
  end
48
56
  end
49
57
  end
@@ -47,6 +47,12 @@ module StackifyRubyAPM
47
47
  hash[:URL] = @transaction.context.request.url[:full] if @transaction.context && @transaction.context.request && @transaction.context.request.url[:full]
48
48
  hash[:RUM] = true if @config.rum_enabled.is_a?(TrueClass)
49
49
  hash[:AWS_LAMBDA_ARN] = @transaction.context.aws[:arn] if @transaction.context && @transaction.context.aws && @transaction.context.aws[:arn]
50
+ hash[:RESPONSE_BODY] = @transaction.context.prefix.response_body.to_s if @transaction.context.prefix && @transaction.context.prefix.response_body
51
+ hash[:RESPONSE_SIZE_BYTES] = @transaction.context.prefix.response_body.length.to_s if @transaction.context.prefix && @transaction.context.prefix.response_body
52
+ hash[:RESPONSE_HEADERS] = @transaction.context.prefix.response_headers.to_s if @transaction.context.prefix && @transaction.context.prefix.response_headers
53
+ hash[:REQUEST_BODY] = @transaction.context.prefix.request_body.to_s if @transaction.context.prefix && @transaction.context.prefix.request_body
54
+ hash[:REQUEST_SIZE_BYTES] = @transaction.context.prefix.request_body.length.to_s if @transaction.context.prefix && @transaction.context.prefix.request_body
55
+ hash[:REQUEST_HEADERS] = @transaction.context.prefix.request_headers.to_s if @transaction.context.prefix && @transaction.context.prefix.request_headers
50
56
  hash
51
57
  end
52
58
  # rubocop:enable Metrics/CyclomaticComplexity