contrast-agent 4.3.2 → 4.4.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent.rb +5 -1
  3. data/lib/contrast/agent/assess.rb +0 -9
  4. data/lib/contrast/agent/assess/contrast_event.rb +0 -2
  5. data/lib/contrast/agent/assess/contrast_object.rb +5 -2
  6. data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
  7. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
  8. data/lib/contrast/agent/assess/policy/propagation_method.rb +28 -13
  9. data/lib/contrast/agent/assess/policy/propagator/append.rb +28 -13
  10. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -16
  11. data/lib/contrast/agent/assess/policy/propagator/splat.rb +23 -13
  12. data/lib/contrast/agent/assess/policy/propagator/split.rb +14 -7
  13. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +30 -14
  14. data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
  15. data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
  16. data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
  17. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -2
  18. data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
  19. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
  20. data/lib/contrast/agent/assess/properties.rb +0 -2
  21. data/lib/contrast/agent/assess/property/tagged.rb +37 -19
  22. data/lib/contrast/agent/assess/tracker.rb +1 -1
  23. data/lib/contrast/agent/middleware.rb +85 -55
  24. data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
  25. data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
  26. data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
  27. data/lib/contrast/agent/protect/rule/sqli.rb +17 -11
  28. data/lib/contrast/agent/request_context.rb +12 -0
  29. data/lib/contrast/agent/thread.rb +1 -1
  30. data/lib/contrast/agent/thread_watcher.rb +20 -5
  31. data/lib/contrast/agent/version.rb +1 -1
  32. data/lib/contrast/api/communication/messaging_queue.rb +18 -21
  33. data/lib/contrast/api/communication/response_processor.rb +8 -1
  34. data/lib/contrast/api/communication/socket_client.rb +22 -14
  35. data/lib/contrast/api/decorators.rb +2 -0
  36. data/lib/contrast/api/decorators/agent_startup.rb +58 -0
  37. data/lib/contrast/api/decorators/application_startup.rb +51 -0
  38. data/lib/contrast/api/decorators/route_coverage.rb +15 -5
  39. data/lib/contrast/api/decorators/trace_event.rb +42 -14
  40. data/lib/contrast/components/agent.rb +2 -0
  41. data/lib/contrast/components/app_context.rb +4 -22
  42. data/lib/contrast/components/sampling.rb +48 -6
  43. data/lib/contrast/components/settings.rb +5 -4
  44. data/lib/contrast/framework/manager.rb +13 -12
  45. data/lib/contrast/framework/rails/support.rb +42 -43
  46. data/lib/contrast/framework/sinatra/support.rb +100 -41
  47. data/lib/contrast/logger/log.rb +31 -15
  48. data/lib/contrast/utils/class_util.rb +3 -1
  49. data/lib/contrast/utils/heap_dump_util.rb +103 -87
  50. data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
  51. data/resources/assess/policy.json +3 -9
  52. data/resources/deadzone/policy.json +6 -0
  53. data/ruby-agent.gemspec +54 -16
  54. metadata +105 -136
  55. data/lib/contrast/agent/assess/rule.rb +0 -18
  56. data/lib/contrast/agent/assess/rule/base.rb +0 -52
  57. data/lib/contrast/agent/assess/rule/redos.rb +0 -67
  58. data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
  59. data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
  60. data/lib/contrast/utils/prevent_serialization.rb +0 -52
@@ -47,32 +47,34 @@ module Contrast
47
47
  end
48
48
 
49
49
  # Find the current route, based on the provided Request wrapper
50
+ #
50
51
  # @param request[Contrast::Agent::Request]
51
52
  # @return [Contrast::Api::Dtm::RouteCoverage]
52
53
  def current_route request
53
54
  return unless ::Rails.cs__respond_to?(:application)
54
55
 
55
- # returns array of arrays [[match_data, path_parameters, route]], sorted by
56
- # precedence
57
- # match_data: ActionDispatch::Journey::Path::Pattern::MatchData
58
- # path_parameters: hash of various things
59
- # route: ActionDispatch::Journey::Route
60
- full_routes = ::Rails.application.routes.router.send(:find_routes, request.rack_request)
61
- return if full_routes.empty?
56
+ match, _params, route, path = get_full_route(request.rack_request)
62
57
 
63
- full_route = full_routes[0]
58
+ original_url = request.rack_request.path_info
64
59
 
65
- # the route is directly implemented within the application
66
- if direct_route?(full_route)
67
- route = full_route[2] # route w/ highest precedence
68
- Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route)
69
- else
70
- engine_route(full_route, request)
60
+ # Route is either the final rails route, or a router that points to a Sinatra controller.
61
+ if Contrast::Framework::Sinatra::Support.sinatra_controller?(route.app.app)
62
+ # Create a request copied from current request, but with the base path removed from path_info.
63
+ new_req = ::ActionDispatch::Request.new(request.env)
64
+ new_req.path_info = new_req.path_info.gsub((path << match).join, '')
65
+
66
+ return Contrast::Framework::Sinatra::Support.current_route(new_req, route.app.app, original_url)
71
67
  end
68
+
69
+ Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route, original_url)
72
70
  rescue StandardError => _e
73
71
  nil
74
72
  end
75
73
 
74
+ # Copy a request for modification.
75
+ #
76
+ # @param [::ActionDispatch::Request] original env.
77
+ # @return [::ActionDispatch::Request] a copy of original env with rails env merged.
76
78
  def retrieve_request env
77
79
  rails_env = ::Rails.application.env_config.merge(env)
78
80
  ::ActionDispatch::Request.new(rails_env || env)
@@ -87,37 +89,34 @@ module Contrast
87
89
 
88
90
  private
89
91
 
90
- # route is not mounted within an engine
91
- def direct_route? full_route
92
- full_route[2]&.app&.cs__class == ActionDispatch::Routing::RouteSet::Dispatcher ||
93
- (full_route[2].cs__class == ActionDispatch::Journey::Route && full_route[2]&.app&.cs__class == ActionDispatch::Routing::Mapper::Constraints)
92
+ # Determine if route is a Rails engine route.
93
+ #
94
+ # @param [Object] app or route that points to a ::Rails::Engine
95
+ # @return [bool] whether the router is an engine or not.
96
+ def engine_route? route
97
+ route.app.is_a?(::ActionDispatch::Routing::Mapper::Constraints) && route.app.app < ::Rails::Engine
94
98
  end
95
99
 
96
- def engine_route full_route, request
97
- engine_route = full_route[2] # supposed route - but actually an Engine mount point
98
- return unless engine_route
99
-
100
- engine_mount_name = engine_route.name
101
- return unless engine_mount_name
102
-
103
- engine_path_segments = request.rack_request.path_info.split(engine_mount_name)
104
- return if engine_path_segments.empty?
105
-
106
- path_within_engine = engine_path_segments[-1]
107
- return unless path_within_engine
108
-
109
- engine_router = engine_route.app&.app&.routes&.router
110
- return unless engine_router
111
-
112
- # Get all routes regardless of http method
113
- matching_routes = engine_router.send(:filter_routes, path_within_engine)
114
- return unless matching_routes
115
-
116
- # filter for current http method
117
- reportable_routes = engine_router.send(:match_routes, matching_routes, request.rack_request)
118
- return if reportable_routes.empty?
119
-
120
- Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(reportable_routes[0])
100
+ # Recursively get final route traversing engines as required.
101
+ #
102
+ # @param request [::Rack::Request] the rack request as will be handed to rails controller.
103
+ # @param top_router [::ActionDispatch::Journer::Router] the current router relative to the previous.
104
+ # @param path [Array<String>] the chunks of path that have been seen.
105
+ # @return [Array<array>] the final set of rails route classes.
106
+ def get_full_route request, top_router = ::Rails.application.routes.router, path = []
107
+ return if (route_matches = top_router.send(:find_routes, request)).empty?
108
+
109
+ match, params, route = route_matches.first
110
+
111
+ # If the current routing node points to a sub-app (::Rais::Engine), dive deeper.
112
+ # Have sub-app route the remainder of the url.
113
+ if engine_route?(route)
114
+ new_req = retrieve_request request.env
115
+ new_req.path_info = new_req.path_info.gsub(match.to_s, '')
116
+ get_full_route(new_req, route.app.app.routes.router, path << match.to_s)
117
+ else
118
+ [match, params, route, path]
119
+ end
121
120
  end
122
121
 
123
122
  # Rails engine routes need to be detected by inspecting Engine class route set
@@ -2,7 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/framework/base_support'
5
- require 'contrast/framework/sinatra/patch/support'
6
5
 
7
6
  module Contrast
8
7
  module Framework
@@ -10,7 +9,6 @@ module Contrast
10
9
  # Used when Sinatra is present to define framework specific behavior
11
10
  class Support
12
11
  extend Contrast::Framework::BaseSupport
13
- extend Contrast::Framework::Sinatra::Patch::Support
14
12
  class << self
15
13
  def detection_class
16
14
  'Sinatra'
@@ -21,44 +19,74 @@ module Contrast
21
19
  end
22
20
 
23
21
  def application_name
24
- return unless app_class
25
-
26
- app_class.cs__class.cs__name
22
+ app_class&.cs__name
27
23
  end
28
24
 
29
25
  def application_root
30
- return unless app_class
31
-
32
- app_class.root
26
+ app_instance&.root
33
27
  end
34
28
 
35
29
  def server_type
36
30
  'sinatra'
37
31
  end
38
32
 
39
- # Iterate over every class that extends Sinatra::Base, pull out its routes
40
- # (array of arrays with Mustermann::Sinatra as [][0]) and convert them into
41
- # Contrast::Api::Dtm::RouteCoverage
33
+ # Given an object, determine if it is a Sinatra controller with routes.
34
+ #
35
+ # @param app [Object] suspected Sinatra app.
36
+ # @return [Boolean]
37
+ def sinatra_controller? app
38
+ # Sinatra is loaded?
39
+ return false unless defined?(::Sinatra) && defined?(::Sinatra::Base)
40
+ # App is a subclass of or actually is ::Sinatra::Base.
41
+ return false unless (app.cs__respond_to?(:<) && app < ::Sinatra::Base) || app == ::Sinatra::Base
42
+
43
+ # App has routes.
44
+ !app.routes.empty?
45
+ end
46
+
47
+ # Find all classes that subclass ::Sinatra::Base. Gather their routes.
48
+ #
49
+ # @return [Array<Contrast::Api::Dtm::RouteCoverage>] the routes found as Dtms.
42
50
  def collect_routes
51
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless defined?(::Sinatra) && defined?(::Sinatra::Base)
52
+
43
53
  routes = []
44
- controllers = sinatra_controllers
45
- controllers.each do |clazz|
46
- class_routes = sinatra_class_routes(clazz)
47
- next unless class_routes
48
-
49
- class_routes.each_pair do |method, list|
50
- # item: [ Mustermann::Sinatra, [], Proc]
51
- list.each do |item|
52
- routes << Contrast::Api::Dtm::RouteCoverage.from_sinatra_route(clazz, method, item[0])
54
+ sinatra_controllers.each do |controller|
55
+ controller.routes.each_pair do |method, route_triplets|
56
+ # Sinatra stores its routes as a triplet: [Mustermann::Sinatra, [], Proc]
57
+ route_triplets.map(&:first).each do |route_pattern|
58
+ routes << Contrast::Api::Dtm::RouteCoverage.from_sinatra_route(controller, method, route_pattern)
53
59
  end
54
60
  end
55
61
  end
56
62
  routes
57
63
  end
58
64
 
59
- # TODO: RUBY-763
60
- def current_route _request
61
- nil
65
+ # Given the current request return a RouteCoverage dtm.
66
+ #
67
+ # @param request [Contrast::Agent::Request] a contrast tracked request.
68
+ # @param controller [::Sinatra::Base] optionally use this controller instead of global ::Sinatra::Base.
69
+ # @return [Contrast::Api::Dtm::RouteCoverage, nil] a Dtm describing the route
70
+ # matched to the request if a match was found.
71
+ def current_route request, controller = ::Sinatra::Base, full_route = nil
72
+ return unless sinatra_controller?(controller)
73
+
74
+ method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc...
75
+
76
+ # Find route match--checking superclasses if necessary.
77
+ final_controller, route_pattern = _route_recurse(controller, method, _cleaned_route(request))
78
+ return unless !final_controller.nil? && !route_pattern.nil?
79
+
80
+ full_route ||= request.path_info
81
+
82
+ Contrast::Api::Dtm::RouteCoverage.from_sinatra_route(final_controller, method, route_pattern, full_route)
83
+ end
84
+
85
+ # Search object space for sinatra controllers--any class that subclasses ::Sinatra::Base.
86
+ #
87
+ # @return [Array<::Sinatra::Base>] sinatra controlelrs
88
+ def sinatra_controllers
89
+ [::Sinatra::Base] + ObjectSpace.each_object(Class).select { |clazz| sinatra_controller?(clazz) }
62
90
  end
63
91
 
64
92
  def retrieve_request env
@@ -67,30 +95,61 @@ module Contrast
67
95
 
68
96
  private
69
97
 
70
- def app_class
71
- return unless defined?(::Sinatra) && defined?(::Sinatra::Base)
72
-
73
- @_app_class ||= begin
74
- sinatra_layers = ObjectSpace.each_object(::Sinatra::Base).to_a
75
- result_layer = sinatra_layers.find { |layer| layer.app.nil? }
76
- result_layer
98
+ # Given a controller and a route to match against, find the route_pattern and class that will serve the
99
+ # route. This is recursive as Sinatra's routing is recursive from subclass to super.
100
+ #
101
+ # @param controller [Sinatra::Base, #routes] a Sinatra application.
102
+ # @param method [::Rack::REQUEST_METHOD] GET, POST, PUT, etc...
103
+ # @param method [String] the relative route passed from Rack.
104
+ # @return [Array[Sinatra::Base, Mustermann::Sinatra], nil] Either the controller that
105
+ # will handle the route along with the route pattern or nil if no match.
106
+ def _route_recurse controller, method, route
107
+ return if controller.nil? || controller.cs__class == NilClass
108
+
109
+ route_patterns = controller.routes.fetch(method, []).map(&:first)
110
+ route_pattern = route_patterns&.find do |matcher|
111
+ matcher.params(route) # ::Mustermann::Sinatra match.
77
112
  end
113
+
114
+ return controller, route_pattern if route_pattern
115
+
116
+ # Check routes defined in superclass if present.
117
+ return _route_recurse(controller.superclass, method, route) if controller.superclass&.instance_variable_get(:@routes)
78
118
  end
79
119
 
80
- # Iterate over every class that extends Sinatra::Base, pull out its routes
81
- # (array of arrays with Mustermann::Sinatra as [][0]) and convert them into
82
- # Contrast::Api::Dtm::RouteCoverage
83
- def sinatra_controllers
84
- return [] unless defined?(::Sinatra) && defined?(::Sinatra::Base)
120
+ # Get route and do some cleanup matching that of Sinatra::Base#process_route.
121
+ #
122
+ # @param request [Contrast::Agent::Request] a contrast tracked request.
123
+ # @return [String] the extracted and cleaned relative route.
124
+ def _cleaned_route request
125
+ settings = ::Sinatra::Base.settings
126
+ route = request.env[::Rack::PATH_INFO]
127
+ return '/' if route.empty? && !settings.empty_path_info?
128
+
129
+ !settings.strict_paths? && route.end_with?('/') ? route[0..-2] : route
130
+ end
85
131
 
86
- Contrast::Utils::ClassUtil.descendants(::Sinatra::Base)
132
+ # Almost an alias to app_instance.
133
+ #
134
+ # @return [::Sinatra::Base] the current controller class as routed by Rack.
135
+ def app_class
136
+ return unless defined?(::Sinatra) && defined?(::Sinatra::Base)
137
+
138
+ app_instance.cs__class
87
139
  end
88
140
 
89
- def sinatra_class_routes controller
90
- controller.instance_variable_get(:@routes) if controller.instance_variable_defined?(:@routes)
91
- rescue StandardError
92
- logger.trace('Sinatra controller found with no route instances', module: controller)
93
- nil
141
+ # Search the object space for the controller handling this request which will be
142
+ # the class inheriting from ::Sinatra::Base with @app=nil since it is the final servicer
143
+ # in the request/middleware chain.
144
+ #
145
+ # @return [::Sinatra::Base] the current controller as routed by Rack.
146
+ def app_instance
147
+ return unless defined?(::Sinatra) && defined?(::Sinatra::Base)
148
+
149
+ @_app_instance ||= begin
150
+ sinatra_layers = ObjectSpace.each_object(::Sinatra::Base).to_a
151
+ sinatra_layers.find { |layer| layer.app.nil? }
152
+ end
94
153
  end
95
154
  end
96
155
  end
@@ -37,28 +37,22 @@ module Contrast
37
37
  # Given new settings from TeamServer, update our logging to use the new
38
38
  # file and level, assuming they weren't set by local configuration.
39
39
  #
40
- # @param log_file [String] the file to which to log
41
- # @param log_level [String] the level at which to log
40
+ # @param log_file [String] the file to which to log, as provided by TeamServer settings
41
+ # @param log_level [String] the level at which to log, as provided by TeamServer settings
42
42
  def update log_file = nil, log_level = nil
43
- config = CONFIG.root.agent.logger
44
-
45
- config_path = config.path&.length.to_i.positive? ? config.path : nil
46
- config_level = config.level&.length&.positive? ? config.level : nil
47
-
48
- # config > settings > default
49
- path = valid_path(config_path || log_file)
50
- level_const = valid_level(config_level || log_level)
43
+ current_path = find_valid_path(log_file)
44
+ current_level_const = find_valid_level(log_level)
51
45
 
52
- path_change = path != previous_path
53
- level_change = level_const != previous_level
46
+ path_change = current_path != previous_path
47
+ level_change = current_level_const != previous_level
54
48
 
55
49
  # don't needlessly recreate logger
56
50
  return if @_logger && !(path_change || level_change)
57
51
 
58
- @previous_path = path
59
- @previous_level = level_const
52
+ @previous_path = current_path
53
+ @previous_level = current_level_const
60
54
 
61
- @_logger = build(path: path, level_const: level_const)
55
+ @_logger = build(path: current_path, level_const: current_level_const)
62
56
  # If we're logging to a new path, then let's start it w/ our helpful
63
57
  # data gathering messages
64
58
  log_update if path_change
@@ -108,6 +102,17 @@ module Contrast
108
102
  logger.extend(Contrast::Logger::Time)
109
103
  end
110
104
 
105
+ # Determine the valid path to which to log, given the precedence of config > settings > default.
106
+ #
107
+ # @param log_file [String, nil] the file to which to log as provided by the settings retrieved from the
108
+ # TeamServer.
109
+ # @return [String] the path to which to log or STDOUT / STDERR if one of those values provided.
110
+ def find_valid_path log_file
111
+ config = CONFIG.root.agent.logger
112
+ config_path = config.path&.length.to_i.positive? ? config.path : nil
113
+ valid_path(config_path || log_file)
114
+ end
115
+
111
116
  def valid_path path
112
117
  path = path.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : path
113
118
  return path if path == STDOUT_STR
@@ -129,6 +134,17 @@ module Contrast
129
134
  end
130
135
  end
131
136
 
137
+ # Determine the valid level to which to log, given the precedence of config > settings > default.
138
+ #
139
+ # @param log_level [String, nil] the level at which to log as provided by the settings retrieved from the
140
+ # TeamServer.
141
+ # @return [::Ougai::Logging::Severity] the level at which to log
142
+ def find_valid_level log_level
143
+ config = CONFIG.root.agent.logger
144
+ config_level = config.level&.length&.positive? ? config.level : nil
145
+ valid_level(config_level || log_level)
146
+ end
147
+
132
148
  def valid_level level
133
149
  level ||= DEFAULT_LEVEL
134
150
  level = level.upcase
@@ -52,7 +52,7 @@ module Contrast
52
52
  # Return a String representing the object invoking this method in the
53
53
  # form expected by our dataflow events.
54
54
  #
55
- # @param object [Object] the entity to convert to a String
55
+ # @param object [Object, nil] the entity to convert to a String
56
56
  # @return [String] the human readable form of the String, as defined by
57
57
  # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md
58
58
  def to_contrast_string object
@@ -63,6 +63,8 @@ module Contrast
63
63
  return cached if cached
64
64
 
65
65
  object.dup
66
+ elsif object.nil?
67
+ Contrast::Utils::ObjectShare::NIL_STRING
66
68
  elsif object.cs__is_a?(Symbol)
67
69
  ":#{ object }"
68
70
  elsif object.cs__is_a?(Numeric)
@@ -2,12 +2,13 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'objspace'
5
+ require 'singleton'
5
6
  require 'contrast/components/interface'
6
7
 
7
8
  module Contrast
8
9
  module Utils
9
10
  # Implementation of a heap dump util to automate generation
10
- class HeapDumpUtil
11
+ class HeapDumpUtil < Contrast::Agent::WorkerThread
11
12
  include Contrast::Components::Interface
12
13
  access_component :heap_dump, :logging
13
14
 
@@ -15,98 +16,113 @@ module Contrast
15
16
  FILE_WRITE_FLAGS = 'w'
16
17
 
17
18
  class << self
18
- def run
19
- return unless heap_dump_enabled?
20
-
21
- log_enabled_warning
22
- dir = heap_dump_control[:path]
23
- Dir.mkdir(dir) unless Dir.exist?(dir)
24
- return unless File.writable?(dir)
25
-
26
- delay = heap_dump_control[:delay]
27
- Contrast::Agent::Thread.new do
28
- logger.info("HEAP DUMP THREAD INITIALIZED. WAITING #{ delay } SECONDS TO BEGIN.")
29
- sleep(delay)
30
- capture_heap_dump
31
- end
32
- rescue StandardError => e
33
- logger.info(LOG_ERROR_DUMPS, e)
34
- nil
19
+ def enabled?
20
+ heap_dump_enabled?
21
+ end
22
+
23
+ def control
24
+ heap_dump_control
35
25
  end
26
+ end
27
+
28
+ def start_thread!
29
+ return unless Contrast::Utils::HeapDumpUtil.enabled?
36
30
 
37
- def log_enabled_warning
38
- dir = heap_dump_control[:path]
39
- window = heap_dump_control[:window]
40
- count = heap_dump_control[:count]
41
- delay = heap_dump_control[:delay]
42
- clean = heap_dump_control[:clean]
43
-
44
- logger.info <<~WARNING
45
- *****************************************************
46
- ******** HEAP DUMP HAS BEEN ENABLED ********
47
- *** APPLICATION PROCESS WILL EXIT UPON COMPLETION ***
48
- *****************************************************
49
-
50
- Heap dump is a debugging tool that snapshots the entire
51
- state of the Ruby VM. It is an exceptionally expensive
52
- process, and should only be used to debug especially
53
- pernicious errors.
54
-
55
- It will write multiple memory snaphots, which are liable
56
- to be multiple gigabytes in size.
57
- They will be named "[unix timestamp]-heap.dump",
58
- e.g.: 1020304050-heap.dump
59
-
60
- It will then call Ruby `exit()`.
61
-
62
- If this is not your specific intent, you can (and should)
63
- disable this option in your Contrast config file.
64
-
65
- HEAP DUMP PARAMETERS:
66
- \t[write files to this directory] dir: #{ dir }
67
- \t[wait this many seconds in between dumps] window: #{ window }
68
- \t[heap dump this many times] count: #{ count }
69
- \t[wait this many seconds into app lifetime] delay: #{ delay }
70
- \t[perform gc pass before dump] clean: #{ clean }
71
-
72
- *****************************************************
73
- ******** YOU HAVE BEEN WARNED ********
74
- *****************************************************
75
- WARNING
31
+ control = Contrast::Utils::HeapDumpUtil.control
32
+ log_enabled_warning
33
+ dir = control[:path]
34
+ Dir.mkdir(dir) unless Dir.exist?(dir)
35
+ return unless File.writable?(dir)
36
+
37
+ delay = control[:delay]
38
+ @_thread = Contrast::Agent::Thread.new do
39
+ logger.info("HEAP DUMP THREAD INITIALIZED. WAITING #{ delay } SECONDS TO BEGIN.")
40
+ sleep(delay)
41
+ capture_heap_dump
76
42
  end
43
+ rescue StandardError => e
44
+ logger.info(LOG_ERROR_DUMPS, e)
45
+ nil
46
+ end
47
+
48
+ def log_enabled_warning
49
+ control = Contrast::Utils::HeapDumpUtil.control
50
+ dir = control[:path]
51
+ window = control[:window]
52
+ count = control[:count]
53
+ delay = control[:delay]
54
+ clean = control[:clean]
55
+
56
+ logger.info <<~WARNING
57
+ *****************************************************
58
+ ******** HEAP DUMP HAS BEEN ENABLED ********
59
+ *** APPLICATION PROCESS WILL EXIT UPON COMPLETION ***
60
+ *****************************************************
61
+
62
+ Heap dump is a debugging tool that snapshots the entire
63
+ state of the Ruby VM. It is an exceptionally expensive
64
+ process, and should only be used to debug especially
65
+ pernicious errors.
66
+
67
+ It will write multiple memory snaphots, which are liable
68
+ to be multiple gigabytes in size.
69
+ They will be named "[unix timestamp]-heap.dump",
70
+ e.g.: 1020304050-heap.dump
71
+
72
+ It will then call Ruby `exit()`.
73
+
74
+ If this is not your specific intent, you can (and should)
75
+ disable this option in your Contrast config file.
76
+
77
+ HEAP DUMP PARAMETERS:
78
+ \t[write files to this directory] dir: #{ dir }
79
+ \t[wait this many seconds in between dumps] window: #{ window }
80
+ \t[heap dump this many times] count: #{ count }
81
+ \t[wait this many seconds into app lifetime] delay: #{ delay }
82
+ \t[perform gc pass before dump] clean: #{ clean }
83
+
84
+ *****************************************************
85
+ ******** YOU HAVE BEEN WARNED ********
86
+ *****************************************************
87
+ WARNING
88
+ end
89
+
90
+ def capture_heap_dump
91
+ control = Contrast::Utils::HeapDumpUtil.control
92
+ dir = control[:path]
93
+ window = control[:window]
94
+ count = control[:count]
95
+ clean = control[:clean]
96
+ logger.info('HEAP DUMP MAIN LOOP')
97
+ ObjectSpace.trace_object_allocations_start
98
+ count.times do |i|
99
+ logger.info('STARTING HEAP DUMP PASS', current_pass: i, max: count)
100
+ snapshot_heap(dir, clean)
101
+ logger.info('FINISHING HEAP DUMP PASS', current_pass: i, max: count)
102
+ sleep(window)
103
+ end
104
+ ensure
105
+ ObjectSpace.trace_object_allocations_stop
106
+ logger.info('*****************************************************')
107
+ logger.info('******** HEAP DUMP HAS CONCLUDED ********')
108
+ logger.info('*** APPLICATION PROCESS WILL EXIT SHORTLY ***')
109
+ logger.info('*****************************************************')
110
+ exit # rubocop:disable Rails/Exit We weren't kidding!
111
+ end
77
112
 
78
- def capture_heap_dump
79
- dir = heap_dump_control[:path]
80
- window = heap_dump_control[:window]
81
- count = heap_dump_control[:count]
82
- clean = heap_dump_control[:clean]
83
- logger.info('HEAP DUMP MAIN LOOP')
84
- ObjectSpace.trace_object_allocations_start
85
- count.times do |i|
86
- logger.info('STARTING HEAP DUMP PASS', current_pass: i + 1, max: count)
87
- output = "#{ Time.now.to_f }-heap.dump"
88
- output = File.join(dir, output)
89
- begin
90
- logger.info('OPENING HEADUMP FILE', dir: dir, file: output)
91
- file = File.new(output, FILE_WRITE_FLAGS)
92
- if clean
93
- logger.info('PERFORMING GARBAGE COLLECTION BEFORE HEAP DUMP')
94
- GC.start
95
- end
96
- ObjectSpace.dump_all(output: file)
97
- logger.info('FINISHING HEAP DUMP PASS', current_pass: i + 1, max: count)
98
- ensure
99
- file.close
100
- end
101
- sleep(window)
113
+ def snapshot_heap dir, clean
114
+ output = "#{ Time.now.to_f }-heap.dump"
115
+ output = File.join(dir, output)
116
+ begin
117
+ logger.info('OPENING HEADUMP FILE', dir: dir, file: output)
118
+ file = File.new(output, FILE_WRITE_FLAGS)
119
+ if clean
120
+ logger.info('PERFORMING GARBAGE COLLECTION BEFORE HEAP DUMP')
121
+ GC.start
102
122
  end
123
+ ObjectSpace.dump_all(output: file)
103
124
  ensure
104
- ObjectSpace.trace_object_allocations_stop
105
- logger.info('*****************************************************')
106
- logger.info('******** HEAP DUMP HAS CONCLUDED ********')
107
- logger.info('*** APPLICATION PROCESS WILL EXIT SHORTLY ***')
108
- logger.info('*****************************************************')
109
- exit # rubocop:disable Rails/Exit We weren't kidding!
125
+ file.close
110
126
  end
111
127
  end
112
128
  end