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.
- checksums.yaml +4 -4
- data/lib/contrast/agent.rb +5 -1
- data/lib/contrast/agent/assess.rb +0 -9
- data/lib/contrast/agent/assess/contrast_event.rb +0 -2
- data/lib/contrast/agent/assess/contrast_object.rb +5 -2
- data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
- data/lib/contrast/agent/assess/policy/propagation_method.rb +28 -13
- data/lib/contrast/agent/assess/policy/propagator/append.rb +28 -13
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -16
- data/lib/contrast/agent/assess/policy/propagator/splat.rb +23 -13
- data/lib/contrast/agent/assess/policy/propagator/split.rb +14 -7
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +30 -14
- data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
- data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
- data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
- data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -2
- data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
- data/lib/contrast/agent/assess/properties.rb +0 -2
- data/lib/contrast/agent/assess/property/tagged.rb +37 -19
- data/lib/contrast/agent/assess/tracker.rb +1 -1
- data/lib/contrast/agent/middleware.rb +85 -55
- data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
- data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
- data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
- data/lib/contrast/agent/protect/rule/sqli.rb +17 -11
- data/lib/contrast/agent/request_context.rb +12 -0
- data/lib/contrast/agent/thread.rb +1 -1
- data/lib/contrast/agent/thread_watcher.rb +20 -5
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/api/communication/messaging_queue.rb +18 -21
- data/lib/contrast/api/communication/response_processor.rb +8 -1
- data/lib/contrast/api/communication/socket_client.rb +22 -14
- data/lib/contrast/api/decorators.rb +2 -0
- data/lib/contrast/api/decorators/agent_startup.rb +58 -0
- data/lib/contrast/api/decorators/application_startup.rb +51 -0
- data/lib/contrast/api/decorators/route_coverage.rb +15 -5
- data/lib/contrast/api/decorators/trace_event.rb +42 -14
- data/lib/contrast/components/agent.rb +2 -0
- data/lib/contrast/components/app_context.rb +4 -22
- data/lib/contrast/components/sampling.rb +48 -6
- data/lib/contrast/components/settings.rb +5 -4
- data/lib/contrast/framework/manager.rb +13 -12
- data/lib/contrast/framework/rails/support.rb +42 -43
- data/lib/contrast/framework/sinatra/support.rb +100 -41
- data/lib/contrast/logger/log.rb +31 -15
- data/lib/contrast/utils/class_util.rb +3 -1
- data/lib/contrast/utils/heap_dump_util.rb +103 -87
- data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
- data/resources/assess/policy.json +3 -9
- data/resources/deadzone/policy.json +6 -0
- data/ruby-agent.gemspec +54 -16
- metadata +105 -136
- data/lib/contrast/agent/assess/rule.rb +0 -18
- data/lib/contrast/agent/assess/rule/base.rb +0 -52
- data/lib/contrast/agent/assess/rule/redos.rb +0 -67
- data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
- data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
- 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
|
-
|
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
|
-
|
58
|
+
original_url = request.rack_request.path_info
|
64
59
|
|
65
|
-
# the route
|
66
|
-
if
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
25
|
-
|
26
|
-
app_class.cs__class.cs__name
|
22
|
+
app_class&.cs__name
|
27
23
|
end
|
28
24
|
|
29
25
|
def application_root
|
30
|
-
|
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
|
-
#
|
40
|
-
#
|
41
|
-
#
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
#
|
60
|
-
|
61
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
#
|
81
|
-
#
|
82
|
-
# Contrast::
|
83
|
-
|
84
|
-
|
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
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
data/lib/contrast/logger/log.rb
CHANGED
@@ -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
|
-
|
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 =
|
53
|
-
level_change =
|
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 =
|
59
|
-
@previous_level =
|
52
|
+
@previous_path = current_path
|
53
|
+
@previous_level = current_level_const
|
60
54
|
|
61
|
-
@_logger = build(path:
|
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
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
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
|