contrast-agent 4.3.2 → 4.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|