google-cloud-debugger 0.26.1 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,96 @@
1
+ # Copyright 2017 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "forwardable"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Debugger
21
+ class Breakpoint
22
+ ##
23
+ # # VariableTable
24
+ #
25
+ # The variable_table exists to aid with computation, memory and network
26
+ # traffic optimization. It enables storing a variable once and reference
27
+ # it from multiple variables, including variables stored in the
28
+ # variable_table itself. For example, the same this object, which may
29
+ # appear at many levels of the stack, can have all of its data stored
30
+ # once in this table. The stack frame variables then would hold only a
31
+ # reference to it.
32
+ #
33
+ # The variable var_table_index field is an index into this repeated
34
+ # field. The stored objects are nameless and get their name from the
35
+ # referencing variable. The effective variable is a merge of the
36
+ # referencing variable and the referenced variable.
37
+ #
38
+ # See also {Breakpoint#variable_table}.
39
+ #
40
+ class VariableTable
41
+ extend Forwardable
42
+
43
+ ##
44
+ # @private Array to store variables.
45
+ attr_accessor :variables
46
+
47
+ ##
48
+ # @private Create a new VariableTable instance
49
+ def initialize
50
+ @variables = []
51
+ end
52
+
53
+ ##
54
+ # @private Create a new VariableTable instance from a variable table
55
+ # gRPC struct
56
+ def self.from_grpc grpc_table
57
+ return if grpc_table.nil?
58
+
59
+ new.tap do |vt|
60
+ vt.variables = grpc_table.map do |grpc_var|
61
+ Breakpoint::Variable.from_grpc grpc_var
62
+ end
63
+ end
64
+ end
65
+
66
+ ##
67
+ # @private Search a variable in this VariableTable by matching
68
+ # object_id, return the array index if found.
69
+ def rb_var_index rb_var
70
+ variables.each_with_index do |var, i|
71
+ return i if var.source_var.object_id == rb_var.object_id
72
+ end
73
+
74
+ nil
75
+ end
76
+
77
+ ##
78
+ # @private Add a Breakpoint::Variable to this VariableTable
79
+ def add var
80
+ return unless var.is_a? Breakpoint::Variable
81
+
82
+ variables << var
83
+ end
84
+
85
+ ##
86
+ # @private Export this VariableTable as a gRPC struct
87
+ def to_grpc
88
+ variables.map(&:to_grpc).compact
89
+ end
90
+
91
+ def_instance_delegators :@variables, :size, :first, :[]
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -13,7 +13,8 @@
13
13
  # limitations under the License.
14
14
 
15
15
 
16
- require "google/cloud/debugger/breakpoint"
16
+ require "google/cloud/debugger/snappoint"
17
+ require "google/cloud/debugger/logpoint"
17
18
 
18
19
  module Google
19
20
  module Cloud
@@ -92,10 +93,8 @@ module Google
92
93
 
93
94
  @wait_token = response.next_wait_token
94
95
 
95
- server_breakpoints = response.breakpoints || []
96
- server_breakpoints = server_breakpoints.map do |grpc_b|
97
- Breakpoint.from_grpc grpc_b
98
- end
96
+ server_breakpoints =
97
+ convert_grpc_breakpoints response.breakpoints || []
99
98
 
100
99
  update_breakpoints server_breakpoints
101
100
 
@@ -117,9 +116,9 @@ module Google
117
116
  def update_breakpoints server_breakpoints
118
117
  synchronize do
119
118
  new_breakpoints =
120
- server_breakpoints - @active_breakpoints - @completed_breakpoints
121
- before_breakpoints_count =
122
- @active_breakpoints.size + @completed_breakpoints.size
119
+ filter_breakpoints server_breakpoints - breakpoints
120
+
121
+ before_breakpoints_count = breakpoints.size
123
122
 
124
123
  # Remember new active breakpoints from server
125
124
  @active_breakpoints += new_breakpoints unless new_breakpoints.empty?
@@ -127,8 +126,7 @@ module Google
127
126
  # Forget old breakpoints
128
127
  @completed_breakpoints &= server_breakpoints
129
128
  @active_breakpoints &= server_breakpoints
130
- after_breakpoints_acount =
131
- @active_breakpoints.size + @completed_breakpoints.size
129
+ after_breakpoints_acount = breakpoints.size
132
130
 
133
131
  breakpoints_updated =
134
132
  !new_breakpoints.empty? ||
@@ -159,6 +157,7 @@ module Google
159
157
  # Take this completed breakpoint off manager's active breakpoints
160
158
  # list, submit the breakpoint snapshot, and update Tracer's
161
159
  # breakpoints_cache.
160
+
162
161
  return unless breakpoint.complete?
163
162
 
164
163
  # Remove this breakpoint from active list
@@ -269,6 +268,42 @@ module Google
269
268
  @completed_breakpoints.clear
270
269
  end
271
270
  end
271
+
272
+ private
273
+
274
+ ##
275
+ # @private Convert the list of grpc breakpoints from Debugger service to
276
+ # {Google::Cloud::Debugger::Breakpoint}.
277
+ def convert_grpc_breakpoints grpc_breakpoints
278
+ grpc_breakpoints.map do |grpc_b|
279
+ breakpoint = Breakpoint.from_grpc grpc_b
280
+ breakpoint.app_root = agent.app_root
281
+ breakpoint.init_var_table if breakpoint.is_a? Debugger::Snappoint
282
+ breakpoint
283
+ end
284
+ end
285
+
286
+ ##
287
+ # @private Varify a list of given breakpoints. Filter out those
288
+ # aren't valid and submit them directly.
289
+ def filter_breakpoints breakpoints
290
+ valid_breakpoints = []
291
+ invalid_breakpoints = []
292
+
293
+ breakpoints.each do |breakpoint|
294
+ if breakpoint.valid?
295
+ valid_breakpoints << breakpoint
296
+ else
297
+ invalid_breakpoints << breakpoint
298
+ end
299
+ end
300
+
301
+ invalid_breakpoints.each do |breakpoint|
302
+ agent.transmitter.submit breakpoint if breakpoint.complete?
303
+ end
304
+
305
+ valid_breakpoints
306
+ end
272
307
  end
273
308
  end
274
309
  end
@@ -21,7 +21,8 @@ module Google
21
21
  ##
22
22
  # @private Represents the OAuth 2.0 signing logic for Debugger.
23
23
  class Credentials < Google::Cloud::Credentials
24
- SCOPE = ["https://www.googleapis.com/auth/cloud_debugger"]
24
+ SCOPE = ["https://www.googleapis.com/auth/cloud_debugger"] +
25
+ Google::Cloud::Logging::Credentials::SCOPE
25
26
  PATH_ENV_VARS = %w(DEBUGGER_KEYFILE GOOGLE_CLOUD_KEYFILE GCLOUD_KEYFILE)
26
27
  JSON_ENV_VARS = %w(DEBUGGER_KEYFILE_JSON GOOGLE_CLOUD_KEYFILE_JSON
27
28
  GCLOUD_KEYFILE_JSON)
@@ -0,0 +1,97 @@
1
+ # Copyright 2017 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "google/cloud/debugger/breakpoint"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Debugger
21
+ ##
22
+ # # Logpoint
23
+ #
24
+ # A kind of {Google::Cloud::Debugger::Breakpoint} that can be evaluated
25
+ # to generate a formatted log string, which later can be submitted to
26
+ # Stackdriver Logging service
27
+ #
28
+ class Logpoint < Breakpoint
29
+ ##
30
+ # Evaluate the breakpoint unless it's already marked as completed.
31
+ # Store evaluted expressions and stack frame variables in
32
+ # @evaluated_expressions and @evaluated_log_message.
33
+ #
34
+ # @param [Array<Binding>] call_stack_bindings An array of Ruby Binding
35
+ # objects, from the call stack that leads to the triggering of the
36
+ # breakpoints.
37
+ #
38
+ # @return [Boolean] True if evaluated successfully; false otherwise.
39
+ #
40
+ def evaluate call_stack_bindings
41
+ synchronize do
42
+ binding = call_stack_bindings[0]
43
+
44
+ return false if complete? || !check_condition(binding)
45
+
46
+ begin
47
+ evaluate_log_message binding
48
+ rescue
49
+ return false
50
+ end
51
+ end
52
+
53
+ true
54
+ end
55
+
56
+ ##
57
+ # @private Evaluate the expressions and log message. Store the result
58
+ # in @evaluated_log_message
59
+ def evaluate_log_message binding
60
+ evaluated_expressions = expressions.map do |expression|
61
+ Evaluator.readonly_eval_expression binding, expression
62
+ end
63
+
64
+ @evaluated_log_message =
65
+ format_message log_message_format, evaluated_expressions
66
+ end
67
+
68
+ ##
69
+ # @private Format log message by interpolate expressions.
70
+ #
71
+ # @example
72
+ # Evaluator.format_log_message("Hello $0",
73
+ # ["World"]) #=> "Hello World"
74
+ #
75
+ # @param [String] message_format The message with with
76
+ # expression placeholders such as `$0`, `$1`, etc.
77
+ # @param [Array<Google::Cloud::Debugger::Breakpoint::Variable>]
78
+ # expressions An array of evaluated expression variables to be
79
+ # placed into message_format's placeholders. The variables need
80
+ # to have type equal String.
81
+ #
82
+ # @return [String] The formatted message string
83
+ #
84
+ def format_message message_format, expressions
85
+ # Substitute placeholders with expressions
86
+ message = message_format.gsub(/(?<!\$)\$\d+/) do |placeholder|
87
+ index = placeholder.match(/\$(\d+)/)[1].to_i
88
+ index < expressions.size ? expressions[index].inspect : ""
89
+ end
90
+
91
+ # Unescape "$" characters
92
+ message.gsub(/\$\$/, "$")
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -14,6 +14,7 @@
14
14
 
15
15
 
16
16
  require "google/cloud/logging/logger"
17
+ require "google/cloud/debugger/request_quota_manager"
17
18
 
18
19
  module Google
19
20
  module Cloud
@@ -44,11 +45,18 @@ module Google
44
45
 
45
46
  load_config kwargs
46
47
 
47
- @debugger = debugger ||
48
- Debugger.new(project: configuration.project_id,
49
- keyfile: configuration.keyfile,
50
- module_name: configuration.module_name,
51
- module_version: configuration.module_version)
48
+ if debugger
49
+ @debugger = debugger
50
+ else
51
+ @debugger =
52
+ Debugger.new(project: configuration.project_id,
53
+ keyfile: configuration.keyfile,
54
+ module_name: configuration.module_name,
55
+ module_version: configuration.module_version)
56
+
57
+ @debugger.agent.quota_manager =
58
+ Google::Cloud::Debugger::RequestQuotaManager.new
59
+ end
52
60
 
53
61
  # Immediately start the debugger agent
54
62
  @debugger.start
@@ -77,6 +85,9 @@ module Google
77
85
  ensure
78
86
  # Stop breakpoints tracing beyond this point
79
87
  @debugger.agent.tracer.disable_traces_for_thread
88
+
89
+ # Reset quotas after each request finishes.
90
+ @debugger.agent.quota_manager.reset if @debugger.agent.quota_manager
80
91
  end
81
92
 
82
93
  private
@@ -0,0 +1,95 @@
1
+ # Copyright 2017 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ module Google
17
+ module Cloud
18
+ module Debugger
19
+ ##
20
+ # # RequestQuotaManager
21
+ #
22
+ # Tracking object used by debugger agent to manage quota in
23
+ # request-based applications. This class tracks the amount of time
24
+ # and number of breakpoints to evaluation in a single session.
25
+ #
26
+ # The debugger agent doesn't have use a quota manager by default, which
27
+ # means it will evaluate all breakpoints encountered and takes as much
28
+ # time as needed. This class is utilized by
29
+ # {Google::Cloud::Debugger::Middleware} class to limit latency overhead
30
+ # when used in Rack-based applications.
31
+ #
32
+ class RequestQuotaManager
33
+ # Default Total time allowed to consume, in seconds
34
+ DEFAULT_TIME_QUOTA = 0.05
35
+
36
+ # Default max number of breakpoints to evaluate
37
+ DEFAULT_COUNT_QUOTA = 10
38
+
39
+ ##
40
+ # The time quota for this manager
41
+ attr_accessor :time_quota
42
+
43
+ ##
44
+ # The count quota for this manager
45
+ attr_accessor :count_quota
46
+
47
+ ##
48
+ # The time quota used
49
+ attr_accessor :time_used
50
+
51
+ ##
52
+ # The count quota used
53
+ attr_accessor :count_used
54
+
55
+ ##
56
+ # Construct a new RequestQuotaManager instance
57
+ #
58
+ # @param [Float] time_quota The max quota for time consumed.
59
+ # @param [Integer] count_quota The max quota for count usage.
60
+ def initialize time_quota: DEFAULT_TIME_QUOTA,
61
+ count_quota: DEFAULT_COUNT_QUOTA
62
+ @time_quota = time_quota
63
+ @time_used = 0
64
+ @count_quota = count_quota
65
+ @count_used = 0
66
+ end
67
+
68
+ ##
69
+ # Check if there's more quota left.
70
+ #
71
+ # @return [Boolean] True if there's more quota; false otherwise.
72
+ def more?
73
+ (time_used < time_quota) && (count_used < count_quota)
74
+ end
75
+
76
+ ##
77
+ # Reset all the quota usage.
78
+ def reset
79
+ @time_used = 0
80
+ @count_used = 0
81
+ end
82
+
83
+ ##
84
+ # Notify the quota manager some resource has been consumed. Each time
85
+ # called increases the count quota usage.
86
+ #
87
+ # @param [Float] time Amount of time to deduct from the time quota.
88
+ def consume time: 0
89
+ @time_used += time
90
+ @count_used += 1
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,208 @@
1
+ # Copyright 2017 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "google/cloud/debugger/breakpoint"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Debugger
21
+ ##
22
+ # # Snappoint
23
+ #
24
+ # A kind of {Google::Cloud::Debugger::Breakpoint} that can be evaluated
25
+ # to capture the state of the program at time of evaluation. This is
26
+ # essentially a {Google::Cloud::Debugger::Breakpoint} with action attrubte
27
+ # set to `:CAPTURE`
28
+ #
29
+ class Snappoint < Breakpoint
30
+ ##
31
+ # Max number of top stacks to collect local variables information
32
+ STACK_EVAL_DEPTH = 5
33
+
34
+ ##
35
+ # Max size of payload a Snappoint collects
36
+ MAX_PAYLOAD_SIZE = 32768 # 32KB
37
+
38
+ ##
39
+ # @private Max size an evaluated expression variable is allowed to be
40
+ MAX_EXPRESSION_LIMIT = 32768 # 32KB
41
+
42
+ ##
43
+ # @private Max size a normal evaluated variable is allowed to be
44
+ MAX_VAR_LIMIT = 1024 # 1KB
45
+
46
+ ##
47
+ # @private Construct a new Snappoint instance.
48
+ def initialize *args
49
+ super
50
+
51
+ init_var_table
52
+ end
53
+
54
+ ##
55
+ # @private Initialize the variable table by inserting a buffer full
56
+ # variable at index 0. This variable will be shared by other variable
57
+ # evaluations if this Snappoint exceeds size limit.
58
+ def init_var_table
59
+ return if @variable_table[0] &&
60
+ @variable_table[0].buffer_full_variable?
61
+
62
+ buffer_full_var = Variable.buffer_full_variable
63
+ @variable_table.variables.unshift buffer_full_var
64
+ end
65
+
66
+ ##
67
+ # Evaluate the breakpoint unless it's already marked as completed.
68
+ # Store evaluted expressions and stack frame variables in
69
+ # @evaluated_expressions, @stack_frames. Mark breakpoint complete if
70
+ # successfully evaluated.
71
+ #
72
+ # @param [Array<Binding>] call_stack_bindings An array of Ruby Binding
73
+ # objects, from the call stack that leads to the triggering of the
74
+ # breakpoints.
75
+ #
76
+ # @return [Boolean] True if evaluated successfully; false otherwise.
77
+ #
78
+ def evaluate call_stack_bindings
79
+ synchronize do
80
+ top_binding = call_stack_bindings[0]
81
+
82
+ return false if complete? || !check_condition(top_binding)
83
+
84
+ begin
85
+ eval_expressions top_binding
86
+ eval_call_stack call_stack_bindings
87
+
88
+ complete
89
+ rescue
90
+ return false
91
+ end
92
+ end
93
+
94
+ true
95
+ end
96
+
97
+ ##
98
+ # @private Evaluates the breakpoint expressions at the point that
99
+ # triggered the breakpoint. The expressions subject to the read-only
100
+ # rules. If the expressions do any write operations, the evaluations
101
+ # abort and show an error message in place of the real result.
102
+ #
103
+ # @param [Binding] binding The binding object from the context
104
+ #
105
+ def eval_expressions bind
106
+ @evaluated_expressions = []
107
+
108
+ expressions.each do |expression|
109
+ eval_result = Evaluator.readonly_eval_expression bind, expression
110
+
111
+ if eval_result.is_a?(Exception) &&
112
+ eval_result.instance_variable_get(:@mutation_cause)
113
+ evaluated_var = Variable.new
114
+ evaluated_var.name = expression
115
+ evaluated_var.set_error_state \
116
+ "Error: #{eval_result.message}",
117
+ refers_to: StatusMessage::VARIABLE_VALUE
118
+ else
119
+ evaluated_var = convert_variable eval_result,
120
+ name: expression,
121
+ limit: MAX_EXPRESSION_LIMIT
122
+ end
123
+
124
+ @evaluated_expressions << evaluated_var
125
+ end
126
+ end
127
+
128
+ ##
129
+ # @private Evaluates call stack. Collects function name and location of
130
+ # each frame from given binding objects. Collects local variable
131
+ # information from top frames.
132
+ #
133
+ # @param [Array<Binding>] call_stack_bindings A list of binding
134
+ # objects that come from each of the call stack frames.
135
+ # @return [Array<Google::Cloud::Debugger::Breakpoint::StackFrame>]
136
+ # A list of StackFrame objects that represent state of the
137
+ # call stack
138
+ #
139
+ def eval_call_stack call_stack_bindings
140
+ @stack_frames = []
141
+
142
+ call_stack_bindings.each_with_index do |frame_binding, i|
143
+ frame_info = StackFrame.new.tap do |sf|
144
+ sf.function = frame_binding.eval("__method__").to_s
145
+ sf.location = SourceLocation.new.tap do |l|
146
+ l.path =
147
+ frame_binding.eval "::File.absolute_path(__FILE__)"
148
+ l.line = frame_binding.eval "__LINE__"
149
+ end
150
+ end
151
+
152
+ @stack_frames << frame_info
153
+
154
+ next if i >= STACK_EVAL_DEPTH
155
+
156
+ frame_binding.local_variables.each do |local_var_name|
157
+ local_var = frame_binding.local_variable_get local_var_name
158
+ var = convert_variable local_var, name: local_var_name,
159
+ limit: MAX_VAR_LIMIT
160
+
161
+ frame_info.locals << var
162
+ end
163
+ end
164
+ end
165
+
166
+ private
167
+
168
+ ##
169
+ # @private Compute the total size of all the evaluated variables in
170
+ # this breakpoint.
171
+ def calculate_total_size
172
+ result = evaluated_expressions.inject(0) do |sum, exp|
173
+ sum + exp.payload_size
174
+ end
175
+
176
+ stack_frames.each do |stack_frame|
177
+ result = stack_frame.locals.inject(result) do |sum, local|
178
+ sum + local.payload_size
179
+ end
180
+ end
181
+
182
+ result = variable_table.variables.inject(result) do |sum, var|
183
+ sum + var.payload_size
184
+ end
185
+
186
+ result
187
+ end
188
+
189
+ ##
190
+ # @private Translate a Ruby variable into a {Breakpoint::Variable}.
191
+ # If the existing evaluated variables already exceed maximum allowed
192
+ # size, then return a buffer full warning variable instead.
193
+ def convert_variable source, name: nil, limit: nil
194
+ current_total_size = calculate_total_size
195
+ var = Variable.from_rb_var source, name: name, limit: limit,
196
+ var_table: variable_table
197
+
198
+ if (current_total_size + var.payload_size <= MAX_PAYLOAD_SIZE) ||
199
+ var.reference_variable?
200
+ var
201
+ else
202
+ Breakpoint::Variable.buffer_full_variable variable_table, name: name
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end