datadog 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -2
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +3 -3
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +57 -18
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +93 -106
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +8 -2
  7. data/ext/datadog_profiling_native_extension/extconf.rb +8 -8
  8. data/ext/datadog_profiling_native_extension/heap_recorder.c +174 -28
  9. data/ext/datadog_profiling_native_extension/heap_recorder.h +11 -0
  10. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +1 -1
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +1 -1
  12. data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -11
  13. data/ext/datadog_profiling_native_extension/stack_recorder.c +58 -22
  14. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  15. data/ext/libdatadog_extconf_helpers.rb +1 -1
  16. data/lib/datadog/appsec/configuration/settings.rb +8 -0
  17. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -5
  18. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +7 -20
  19. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -15
  20. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +6 -18
  21. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +7 -20
  22. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +5 -18
  23. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +3 -1
  24. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +3 -5
  25. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +5 -18
  26. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +6 -10
  27. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +7 -20
  28. data/lib/datadog/appsec/event.rb +24 -0
  29. data/lib/datadog/appsec/ext.rb +4 -0
  30. data/lib/datadog/appsec/monitor/gateway/watcher.rb +3 -5
  31. data/lib/datadog/appsec/monitor/reactive/set_user.rb +7 -20
  32. data/lib/datadog/appsec/processor/context.rb +109 -0
  33. data/lib/datadog/appsec/processor.rb +7 -71
  34. data/lib/datadog/appsec/scope.rb +1 -4
  35. data/lib/datadog/appsec/utils/trace_operation.rb +15 -0
  36. data/lib/datadog/appsec/utils.rb +2 -0
  37. data/lib/datadog/appsec.rb +1 -0
  38. data/lib/datadog/core/configuration/agent_settings_resolver.rb +26 -25
  39. data/lib/datadog/core/configuration/settings.rb +12 -0
  40. data/lib/datadog/core/configuration.rb +1 -3
  41. data/lib/datadog/core/crashtracking/component.rb +8 -5
  42. data/lib/datadog/core/environment/yjit.rb +5 -0
  43. data/lib/datadog/core/remote/transport/http.rb +5 -0
  44. data/lib/datadog/core/remote/worker.rb +1 -1
  45. data/lib/datadog/core/runtime/ext.rb +1 -0
  46. data/lib/datadog/core/runtime/metrics.rb +4 -0
  47. data/lib/datadog/core/semaphore.rb +35 -0
  48. data/lib/datadog/core/telemetry/logging.rb +10 -10
  49. data/lib/datadog/core/transport/ext.rb +1 -0
  50. data/lib/datadog/core/workers/async.rb +1 -1
  51. data/lib/datadog/di/code_tracker.rb +11 -13
  52. data/lib/datadog/di/instrumenter.rb +301 -0
  53. data/lib/datadog/di/probe.rb +29 -0
  54. data/lib/datadog/di/probe_builder.rb +7 -1
  55. data/lib/datadog/di/probe_notification_builder.rb +207 -0
  56. data/lib/datadog/di/probe_notifier_worker.rb +244 -0
  57. data/lib/datadog/di/serializer.rb +23 -1
  58. data/lib/datadog/di/transport.rb +67 -0
  59. data/lib/datadog/di/utils.rb +39 -0
  60. data/lib/datadog/di.rb +43 -0
  61. data/lib/datadog/profiling/collectors/thread_context.rb +9 -11
  62. data/lib/datadog/profiling/component.rb +1 -0
  63. data/lib/datadog/profiling/stack_recorder.rb +37 -9
  64. data/lib/datadog/tracing/component.rb +13 -0
  65. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -0
  66. data/lib/datadog/tracing/contrib/excon/middleware.rb +3 -0
  67. data/lib/datadog/tracing/contrib/faraday/middleware.rb +3 -0
  68. data/lib/datadog/tracing/contrib/grape/endpoint.rb +5 -2
  69. data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +9 -0
  70. data/lib/datadog/tracing/contrib/http/instrumentation.rb +4 -0
  71. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +4 -0
  72. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +4 -0
  73. data/lib/datadog/tracing/contrib/rails/runner.rb +1 -1
  74. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +3 -0
  75. data/lib/datadog/tracing/sampling/rule_sampler.rb +6 -4
  76. data/lib/datadog/tracing/tracer.rb +15 -10
  77. data/lib/datadog/tracing/transport/http.rb +4 -0
  78. data/lib/datadog/tracing/workers.rb +1 -1
  79. data/lib/datadog/tracing/writer.rb +26 -28
  80. data/lib/datadog/version.rb +1 -1
  81. metadata +22 -14
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "error"
4
+ require_relative "utils"
4
5
  require_relative "../core/rate_limiter"
5
6
 
6
7
  module Datadog
@@ -31,11 +32,17 @@ module Datadog
31
32
  #
32
33
  # @api private
33
34
  class Probe
35
+ KNOWN_TYPES = %i[log].freeze
36
+
34
37
  def initialize(id:, type:,
35
38
  file: nil, line_no: nil, type_name: nil, method_name: nil,
36
39
  template: nil, capture_snapshot: false, max_capture_depth: nil, rate_limit: nil)
37
40
  # Perform some sanity checks here to detect unexpected attribute
38
41
  # combinations, in order to not do them in subsequent code.
42
+ unless KNOWN_TYPES.include?(type)
43
+ raise ArgumentError, "Unknown probe type: #{type}"
44
+ end
45
+
39
46
  if line_no && method_name
40
47
  raise ArgumentError, "Probe contains both line number and method name: #{id}"
41
48
  end
@@ -128,6 +135,28 @@ module Datadog
128
135
  raise NotImplementedError
129
136
  end
130
137
  end
138
+
139
+ # Returns whether the provided +path+ matches the user-designated
140
+ # file (of a line probe).
141
+ #
142
+ # If file is an absolute path (i.e., it starts with a slash), the file
143
+ # must be identical to path to match.
144
+ #
145
+ # If file is not an absolute path, the path matches if the file is its suffix,
146
+ # at a path component boundary.
147
+ def file_matches?(path)
148
+ unless file
149
+ raise ArgumentError, "Probe does not have a file to match against"
150
+ end
151
+ Utils.path_matches_suffix?(path, file)
152
+ end
153
+
154
+ # Instrumentation module for method probes.
155
+ attr_accessor :instrumentation_module
156
+
157
+ # Line trace point for line probes. Normally this would be a targeted
158
+ # trace point.
159
+ attr_accessor :instrumentation_trace_point
131
160
  end
132
161
  end
133
162
  end
@@ -17,11 +17,17 @@ module Datadog
17
17
  #
18
18
  # @api private
19
19
  module ProbeBuilder
20
+ PROBE_TYPES = {
21
+ 'LOG_PROBE' => :log,
22
+ }.freeze
23
+
20
24
  module_function def build_from_remote_config(config)
21
25
  # The validations here are not yet comprehensive.
26
+ type = config.fetch('type')
27
+ type_symbol = PROBE_TYPES[type] or raise ArgumentError, "Unrecognized probe type: #{type}"
22
28
  Probe.new(
23
29
  id: config.fetch("id"),
24
- type: config.fetch("type"),
30
+ type: type_symbol,
25
31
  file: config["where"]&.[]("sourceFile"),
26
32
  # Sometimes lines are sometimes received as an array of nil
27
33
  # for some reason.
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # Builds probe status notification and snapshot payloads.
6
+ #
7
+ # @api private
8
+ class ProbeNotificationBuilder
9
+ def initialize(settings, serializer)
10
+ @settings = settings
11
+ @serializer = serializer
12
+ end
13
+
14
+ attr_reader :settings
15
+ attr_reader :serializer
16
+
17
+ def build_received(probe)
18
+ build_status(probe,
19
+ message: "Probe #{probe.id} has been received correctly",
20
+ status: 'RECEIVED',)
21
+ end
22
+
23
+ def build_installed(probe)
24
+ build_status(probe,
25
+ message: "Probe #{probe.id} has been instrumented correctly",
26
+ status: 'INSTALLED',)
27
+ end
28
+
29
+ def build_emitting(probe)
30
+ build_status(probe,
31
+ message: "Probe #{probe.id} is emitting",
32
+ status: 'EMITTING',)
33
+ end
34
+
35
+ # Duration is in seconds.
36
+ def build_executed(probe,
37
+ trace_point: nil, rv: nil, duration: nil, caller_locations: nil,
38
+ args: nil, kwargs: nil, serialized_entry_args: nil)
39
+ snapshot = if probe.line? && probe.capture_snapshot?
40
+ if trace_point.nil?
41
+ raise "Cannot create snapshot because there is no trace point"
42
+ end
43
+ get_local_variables(trace_point)
44
+ end
45
+ # TODO check how many stack frames we should be keeping/sending,
46
+ # this should be all frames for enriched probes and no frames for
47
+ # non-enriched probes?
48
+ build_snapshot(probe, rv: rv, snapshot: snapshot,
49
+ duration: duration, caller_locations: caller_locations, args: args, kwargs: kwargs,
50
+ serialized_entry_args: serialized_entry_args)
51
+ end
52
+
53
+ def build_snapshot(probe, rv: nil, snapshot: nil,
54
+ duration: nil, caller_locations: nil, args: nil, kwargs: nil,
55
+ serialized_entry_args: nil)
56
+ # TODO also verify that non-capturing probe does not pass
57
+ # snapshot or vars/args into this method
58
+ captures = if probe.capture_snapshot?
59
+ if probe.method?
60
+ {
61
+ entry: {
62
+ # standard:disable all
63
+ arguments: if serialized_entry_args
64
+ serialized_entry_args
65
+ else
66
+ (args || kwargs) && serializer.serialize_args(args, kwargs)
67
+ end,
68
+ throwable: nil,
69
+ # standard:enable all
70
+ },
71
+ return: {
72
+ arguments: {
73
+ "@return": serializer.serialize_value(rv),
74
+ },
75
+ throwable: nil,
76
+ },
77
+ }
78
+ elsif probe.line?
79
+ {
80
+ lines: snapshot && {
81
+ probe.line_no => {locals: serializer.serialize_vars(snapshot)},
82
+ },
83
+ }
84
+ end
85
+ end
86
+
87
+ location = if probe.line?
88
+ actual_file = if probe.file
89
+ # Normally caller_locations should always be filled for a line probe
90
+ # but in the test suite we don't always provide all arguments.
91
+ actual_file_basename = File.basename(probe.file)
92
+ caller_locations&.detect do |loc|
93
+ # TODO record actual path that probe was installed into,
94
+ # perform exact match here against that path.
95
+ File.basename(loc.path) == actual_file_basename
96
+ end&.path || probe.file
97
+ end
98
+ {
99
+ file: actual_file,
100
+ lines: [probe.line_no],
101
+ }
102
+ elsif probe.method?
103
+ {
104
+ method: probe.method_name,
105
+ type: probe.type_name,
106
+ }
107
+ end
108
+
109
+ stack = if caller_locations
110
+ format_caller_locations(caller_locations)
111
+ end
112
+
113
+ timestamp = timestamp_now
114
+ {
115
+ service: settings.service,
116
+ "debugger.snapshot": {
117
+ id: SecureRandom.uuid,
118
+ timestamp: timestamp,
119
+ evaluationErrors: [],
120
+ probe: {
121
+ id: probe.id,
122
+ version: 0,
123
+ location: location,
124
+ },
125
+ language: 'ruby',
126
+ # TODO add test coverage for callers being nil
127
+ stack: stack,
128
+ captures: captures,
129
+ },
130
+ # In python tracer duration is under debugger.snapshot,
131
+ # but UI appears to expect it here at top level.
132
+ duration: duration ? (duration * 10**9).to_i : nil,
133
+ host: nil,
134
+ logger: {
135
+ name: probe.file,
136
+ method: probe.method_name || 'no_method',
137
+ thread_name: Thread.current.name,
138
+ # Dynamic instrumentation currently does not need thread_id for
139
+ # anything. It can be sent if a customer requests it at which point
140
+ # we can also determine which thread identifier to send
141
+ # (Thread#native_thread_id or something else).
142
+ thread_id: nil,
143
+ version: 2,
144
+ },
145
+ # TODO add tests that the trace/span id is correctly propagated
146
+ "dd.trace_id": Datadog::Tracing.active_trace&.id,
147
+ "dd.span_id": Datadog::Tracing.active_span&.id,
148
+ ddsource: 'dd_debugger',
149
+ message: probe.template && evaluate_template(probe.template,
150
+ duration: duration ? duration * 1000 : nil),
151
+ timestamp: timestamp,
152
+ }
153
+ end
154
+
155
+ def build_status(probe, message:, status:)
156
+ {
157
+ service: settings.service,
158
+ timestamp: timestamp_now,
159
+ message: message,
160
+ ddsource: 'dd_debugger',
161
+ debugger: {
162
+ diagnostics: {
163
+ probeId: probe.id,
164
+ probeVersion: 0,
165
+ runtimeId: Core::Environment::Identity.id,
166
+ parentId: nil,
167
+ status: status,
168
+ },
169
+ },
170
+ }
171
+ end
172
+
173
+ def format_caller_locations(caller_locations)
174
+ caller_locations.map do |loc|
175
+ {fileName: loc.path, function: loc.label, lineNumber: loc.lineno}
176
+ end
177
+ end
178
+
179
+ def evaluate_template(template, **vars)
180
+ message = template.dup
181
+ vars.each do |key, value|
182
+ message.gsub!("{@#{key}}", value.to_s)
183
+ end
184
+ message
185
+ end
186
+
187
+ def timestamp_now
188
+ (Time.now.to_f * 1000).to_i
189
+ end
190
+
191
+ def get_local_variables(trace_point)
192
+ # binding appears to be constructed on access, therefore
193
+ # 1) we should attempt to cache it and
194
+ # 2) we should not call +binding+ until we actually need variable values.
195
+ binding = trace_point.binding
196
+
197
+ # steep hack - should never happen
198
+ return {} unless binding
199
+
200
+ binding.local_variables.each_with_object({}) do |name, map|
201
+ value = binding.local_variable_get(name)
202
+ map[name] = value
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../core/semaphore'
4
+
5
+ module Datadog
6
+ module DI
7
+ # Background worker thread for sending probe statuses and snapshots
8
+ # to the backend (via the agent).
9
+ #
10
+ # The loop inside the worker rescues all exceptions to prevent termination
11
+ # due to unhandled exceptions raised by any downstream code.
12
+ # This includes communication and protocol errors when sending the
13
+ # payloads to the agent.
14
+ #
15
+ # The worker groups the data to send into batches. The goal is to perform
16
+ # no more than one network operation per event type per second.
17
+ # There is also a limit on the length of the sending queue to prevent
18
+ # it from growing without bounds if upstream code generates an enormous
19
+ # number of events for some reason.
20
+ #
21
+ # Wake-up events are used (via ConditionVariable) to keep the thread
22
+ # asleep if there is no work to be done.
23
+ #
24
+ # @api private
25
+ class ProbeNotifierWorker
26
+ # Minimum interval between submissions.
27
+ # TODO make this into an internal setting and increase default to 2 or 3.
28
+ MIN_SEND_INTERVAL = 1
29
+
30
+ def initialize(settings, transport, logger)
31
+ @settings = settings
32
+ @status_queue = []
33
+ @snapshot_queue = []
34
+ @transport = transport
35
+ @logger = logger
36
+ @lock = Mutex.new
37
+ @wake = Core::Semaphore.new
38
+ @io_in_progress = false
39
+ @sleep_remaining = nil
40
+ @wake_scheduled = false
41
+ @thread = nil
42
+ end
43
+
44
+ attr_reader :settings
45
+ attr_reader :logger
46
+
47
+ def start
48
+ return if @thread
49
+ @thread = Thread.new do
50
+ loop do
51
+ # TODO If stop is requested, we stop immediately without
52
+ # flushing the submissions. Should we send pending submissions
53
+ # and then quit?
54
+ break if @stop_requested
55
+
56
+ sleep_remaining = @lock.synchronize do
57
+ if sleep_remaining && sleep_remaining > 0
58
+ # Recalculate how much sleep time is remaining, then sleep that long.
59
+ set_sleep_remaining
60
+ else
61
+ 0
62
+ end
63
+ end
64
+
65
+ if sleep_remaining > 0
66
+ # Do not need to update @wake_scheduled here because
67
+ # wake-up is already scheduled for the earliest possible time.
68
+ wake.wait(sleep_remaining)
69
+ next
70
+ end
71
+
72
+ begin
73
+ more = maybe_send
74
+ rescue => exc
75
+ raise if settings.dynamic_instrumentation.propagate_all_exceptions
76
+
77
+ logger.warn("Error in probe notifier worker: #{exc.class}: #{exc} (at #{exc.backtrace.first})")
78
+ end
79
+ @lock.synchronize do
80
+ @wake_scheduled = more
81
+ end
82
+ wake.wait(more ? MIN_SEND_INTERVAL : nil)
83
+ end
84
+ end
85
+ end
86
+
87
+ # Stops the background thread.
88
+ #
89
+ # Attempts a graceful stop with the specified timeout, then falls back
90
+ # to killing the thread using Thread#kill.
91
+ def stop(timeout = 1)
92
+ @stop_requested = true
93
+ wake.signal
94
+ if thread
95
+ unless thread.join(timeout)
96
+ thread.kill
97
+ end
98
+ @thread = nil
99
+ end
100
+ end
101
+
102
+ # Waits for background thread to send pending notifications.
103
+ #
104
+ # This method waits for the notification queue to become empty
105
+ # rather than for a particular set of notifications to be sent out,
106
+ # therefore, it should only be called when there is no parallel
107
+ # activity (in another thread) that causes more notifications
108
+ # to be generated.
109
+ def flush
110
+ loop do
111
+ if @thread.nil? || !@thread.alive?
112
+ return
113
+ end
114
+
115
+ io_in_progress, queues_empty = @lock.synchronize do
116
+ [io_in_progress?, status_queue.empty? && snapshot_queue.empty?]
117
+ end
118
+
119
+ if io_in_progress
120
+ # If we just call Thread.pass we could be in a busy loop -
121
+ # add a sleep.
122
+ sleep 0.25
123
+ next
124
+ elsif queues_empty
125
+ break
126
+ else
127
+ sleep 0.25
128
+ next
129
+ end
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ attr_reader :transport
136
+ attr_reader :wake
137
+ attr_reader :thread
138
+
139
+ # This method should be called while @lock is held.
140
+ def io_in_progress?
141
+ @io_in_progress
142
+ end
143
+
144
+ attr_reader :last_sent
145
+
146
+ [
147
+ [:status, 'probe status'],
148
+ [:snapshot, 'snapshot'],
149
+ ].each do |(event_type, event_name)|
150
+ attr_reader "#{event_type}_queue"
151
+
152
+ # Adds a status or a snapshot to the queue to be sent to the agent
153
+ # at the next opportunity.
154
+ #
155
+ # If the queue is too large, the event will not be added.
156
+ #
157
+ # Signals the background thread to wake up (and do the sending)
158
+ # if it has been more than 1 second since the last send of the same
159
+ # event type.
160
+ define_method("add_#{event_type}") do |event|
161
+ @lock.synchronize do
162
+ queue = send("#{event_type}_queue")
163
+ # TODO determine a suitable limit via testing/benchmarking
164
+ if queue.length > 100
165
+ logger.warn("#{self.class.name}: dropping #{event_type} because queue is full")
166
+ else
167
+ queue << event
168
+ end
169
+ end
170
+
171
+ # Figure out whether to wake up the worker thread.
172
+ # If minimum send interval has elapsed since the last send,
173
+ # wake up immediately.
174
+ @lock.synchronize do
175
+ unless @wake_scheduled
176
+ @wake_scheduled = true
177
+ set_sleep_remaining
178
+ wake.signal
179
+ end
180
+ end
181
+ end
182
+
183
+ # Determine how much longer the worker thread should sleep
184
+ # so as not to send in less than MIN_SEND_INTERVAL since the last send.
185
+ # Important: this method must be called when @lock is held.
186
+ #
187
+ # Returns the time remaining to sleep.
188
+ def set_sleep_remaining
189
+ now = Core::Utils::Time.get_time
190
+ @sleep_remaining = if last_sent
191
+ [last_sent + MIN_SEND_INTERVAL - now, 0].max
192
+ else
193
+ 0
194
+ end
195
+ end
196
+
197
+ public "add_#{event_type}"
198
+
199
+ # Sends pending probe statuses or snapshots.
200
+ #
201
+ # This method should ideally only be called when there are actually
202
+ # events to send, but it can be called when there is nothing to do.
203
+ # Currently we only have one wake-up signaling object and two
204
+ # types of events. Therefore on most wake-ups we expect to only
205
+ # send one type of events.
206
+ define_method("maybe_send_#{event_type}") do
207
+ batch = nil
208
+ @lock.synchronize do
209
+ batch = instance_variable_get("@#{event_type}_queue")
210
+ instance_variable_set("@#{event_type}_queue", [])
211
+ @io_in_progress = batch.any? # steep:ignore
212
+ end
213
+ if batch.any? # steep:ignore
214
+ begin
215
+ transport.public_send("send_#{event_type}", batch)
216
+ time = Core::Utils::Time.get_time
217
+ @lock.synchronize do
218
+ @last_sent = time
219
+ end
220
+ rescue => exc
221
+ raise if settings.dynamic_instrumentation.propagate_all_exceptions
222
+ logger.warn("failed to send #{event_name}: #{exc.class}: #{exc} (at #{exc.backtrace.first})")
223
+ end
224
+ end
225
+ batch.any? # steep:ignore
226
+ rescue ThreadError
227
+ # Normally the queue should only be consumed in this method,
228
+ # however if anyone consumes it elsewhere we don't want to block
229
+ # while consuming it here. Rescue ThreadError and return.
230
+ logger.warn("unexpected #{event_name} queue underflow - consumed elsewhere?")
231
+ ensure
232
+ @lock.synchronize do
233
+ @io_in_progress = false
234
+ end
235
+ end
236
+ end
237
+
238
+ def maybe_send
239
+ rv = maybe_send_status
240
+ rv || maybe_send_snapshot
241
+ end
242
+ end
243
+ end
244
+ end
@@ -26,6 +26,16 @@ module Datadog
26
26
  # All serialization methods take the names of the variables being
27
27
  # serialized in order to be able to redact values.
28
28
  #
29
+ # The result of serialization should not reference parameter values when
30
+ # the values are mutable (currently, this only applies to string values).
31
+ # Serializer will duplicate such mutable values, so that if method
32
+ # arguments are captured at entry and then modified during method execution,
33
+ # the serialized values from entry are correctly preserved.
34
+ # Alternatively, we could pass a parameter to the serialization methods
35
+ # which would control whether values are duplicated. This may be more
36
+ # efficient but there would be additional overhead from passing this
37
+ # parameter all the time and the API would get more complex.
38
+ #
29
39
  # @api private
30
40
  class Serializer
31
41
  def initialize(settings, redactor)
@@ -92,12 +102,24 @@ module Datadog
92
102
  when Integer, Float, TrueClass, FalseClass
93
103
  serialized.update(value: value.to_s)
94
104
  when String, Symbol
95
- value = value.to_s
105
+ need_dup = false
106
+ value = if String === value
107
+ # This is the only place where we duplicate the value, currently.
108
+ # All other values are immutable primitives (e.g. numbers).
109
+ # However, do not duplicate if the string is frozen, or if
110
+ # it is later truncated.
111
+ need_dup = !value.frozen?
112
+ value
113
+ else
114
+ value.to_s
115
+ end
96
116
  max = settings.dynamic_instrumentation.max_capture_string_length
97
117
  if value.length > max
98
118
  serialized.update(truncated: true, size: value.length)
99
119
  value = value[0...max]
120
+ need_dup = false
100
121
  end
122
+ value = value.dup if need_dup
101
123
  serialized.update(value: value)
102
124
  when Array
103
125
  if depth < 0
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+
5
+ module Datadog
6
+ module DI
7
+ # Transport for sending probe statuses and snapshots to local agent.
8
+ #
9
+ # Handles encoding of the payloads into multipart posts if necessary,
10
+ # body formatting/encoding, setting correct headers, etc.
11
+ #
12
+ # The transport does not handle batching of statuses or snapshots -
13
+ # the batching should be implemented upstream of this class.
14
+ #
15
+ # Timeout settings are forwarded from agent settings to the Net adapter.
16
+ #
17
+ # The send_* methods raise Error::AgentCommunicationError on errors
18
+ # (network errors and HTTP protocol errors). It is the responsibility
19
+ # of upstream code to rescue these exceptions appropriately to prevent them
20
+ # from being propagated to the application.
21
+ #
22
+ # @api private
23
+ class Transport
24
+ DIAGNOSTICS_PATH = '/debugger/v1/diagnostics'
25
+ INPUT_PATH = '/debugger/v1/input'
26
+
27
+ def initialize(agent_settings)
28
+ # Note that this uses host, port, timeout and TLS flag from
29
+ # agent settings.
30
+ @client = Core::Transport::HTTP::Adapters::Net.new(agent_settings)
31
+ end
32
+
33
+ def send_diagnostics(payload)
34
+ event_payload = Core::Vendor::Multipart::Post::UploadIO.new(
35
+ StringIO.new(JSON.dump(payload)), 'application/json', 'event.json'
36
+ )
37
+ payload = {'event' => event_payload}
38
+ send_request('Probe status submission', DIAGNOSTICS_PATH, payload)
39
+ end
40
+
41
+ def send_input(payload)
42
+ send_request('Probe snapshot submission', INPUT_PATH, payload,
43
+ headers: {'content-type' => 'application/json'},)
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :client
49
+
50
+ def send_request(desc, path, payload, headers: {})
51
+ # steep:ignore:start
52
+ env = OpenStruct.new(
53
+ path: path,
54
+ form: payload,
55
+ headers: headers,
56
+ )
57
+ # steep:ignore:end
58
+ response = client.post(env)
59
+ unless response.ok?
60
+ raise Error::AgentCommunicationError, "#{desc} failed: #{response.code}: #{response.payload}"
61
+ end
62
+ rescue IOError, SystemCallError => exc
63
+ raise Error::AgentCommunicationError, "#{desc} failed: #{exc.class}: #{exc}"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module Utils
6
+ # Returns whether the provided +path+ matches the user-designated
7
+ # file suffix (of a line probe).
8
+ #
9
+ # If suffix is an absolute path (i.e., it starts with a slash), the path
10
+ # must be identical for it to match.
11
+ #
12
+ # If suffix is not an absolute path, the path matches if its suffix is
13
+ # the provided suffix, at a path component boundary.
14
+ module_function def path_matches_suffix?(path, suffix)
15
+ if suffix.start_with?('/')
16
+ path == suffix
17
+ else
18
+ # Exact match is not possible here, meaning any matching path
19
+ # has to be longer than the suffix. Require full component matches,
20
+ # meaning either the first character of the suffix is a slash
21
+ # or the previous character in the path is a slash.
22
+ # For now only check for forward slashes for Unix-like OSes;
23
+ # backslash is a legitimate character of a file name in Unix
24
+ # therefore simply permitting forward or back slash is not
25
+ # sufficient, we need to perform an OS check to know which
26
+ # path separator to use.
27
+ !!
28
+ if path.length > suffix.length && path.end_with?(suffix)
29
+ previous_char = path[path.length - suffix.length - 1]
30
+ previous_char == "/" || suffix[0] == "/"
31
+ end
32
+
33
+ # Alternative implementation using a regular expression:
34
+ # !!(path =~ %r,(/|\A)#{Regexp.quote(suffix)}\z,)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end