datadog 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  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_api/crashtracker.c +3 -5
  16. data/ext/libdatadog_extconf_helpers.rb +1 -1
  17. data/lib/datadog/appsec/configuration/settings.rb +8 -0
  18. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -5
  19. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +7 -20
  20. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -15
  21. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +6 -18
  22. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +7 -20
  23. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +5 -18
  24. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +3 -1
  25. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +3 -5
  26. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +5 -18
  27. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +6 -10
  28. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +7 -20
  29. data/lib/datadog/appsec/event.rb +24 -0
  30. data/lib/datadog/appsec/ext.rb +4 -0
  31. data/lib/datadog/appsec/monitor/gateway/watcher.rb +3 -5
  32. data/lib/datadog/appsec/monitor/reactive/set_user.rb +7 -20
  33. data/lib/datadog/appsec/processor/context.rb +107 -0
  34. data/lib/datadog/appsec/processor.rb +7 -71
  35. data/lib/datadog/appsec/scope.rb +1 -4
  36. data/lib/datadog/appsec/utils/trace_operation.rb +15 -0
  37. data/lib/datadog/appsec/utils.rb +2 -0
  38. data/lib/datadog/appsec.rb +1 -0
  39. data/lib/datadog/core/configuration/agent_settings_resolver.rb +26 -25
  40. data/lib/datadog/core/configuration/settings.rb +12 -0
  41. data/lib/datadog/core/configuration.rb +1 -3
  42. data/lib/datadog/core/crashtracking/component.rb +8 -5
  43. data/lib/datadog/core/environment/yjit.rb +5 -0
  44. data/lib/datadog/core/remote/transport/http.rb +5 -0
  45. data/lib/datadog/core/remote/worker.rb +1 -1
  46. data/lib/datadog/core/runtime/ext.rb +1 -0
  47. data/lib/datadog/core/runtime/metrics.rb +4 -0
  48. data/lib/datadog/core/semaphore.rb +35 -0
  49. data/lib/datadog/core/telemetry/logging.rb +10 -10
  50. data/lib/datadog/core/transport/ext.rb +1 -0
  51. data/lib/datadog/core/workers/async.rb +1 -1
  52. data/lib/datadog/di/code_tracker.rb +11 -13
  53. data/lib/datadog/di/instrumenter.rb +301 -0
  54. data/lib/datadog/di/probe.rb +29 -0
  55. data/lib/datadog/di/probe_builder.rb +7 -1
  56. data/lib/datadog/di/probe_notification_builder.rb +207 -0
  57. data/lib/datadog/di/probe_notifier_worker.rb +244 -0
  58. data/lib/datadog/di/serializer.rb +23 -1
  59. data/lib/datadog/di/transport.rb +67 -0
  60. data/lib/datadog/di/utils.rb +39 -0
  61. data/lib/datadog/di.rb +43 -0
  62. data/lib/datadog/profiling/collectors/thread_context.rb +9 -11
  63. data/lib/datadog/profiling/component.rb +1 -0
  64. data/lib/datadog/profiling/stack_recorder.rb +37 -9
  65. data/lib/datadog/tracing/component.rb +13 -0
  66. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -0
  67. data/lib/datadog/tracing/contrib/excon/middleware.rb +3 -0
  68. data/lib/datadog/tracing/contrib/faraday/middleware.rb +3 -0
  69. data/lib/datadog/tracing/contrib/grape/endpoint.rb +5 -2
  70. data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +9 -0
  71. data/lib/datadog/tracing/contrib/http/instrumentation.rb +4 -0
  72. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +4 -0
  73. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +4 -0
  74. data/lib/datadog/tracing/contrib/rails/runner.rb +1 -1
  75. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +3 -0
  76. data/lib/datadog/tracing/sampling/rule_sampler.rb +6 -4
  77. data/lib/datadog/tracing/tracer.rb +15 -10
  78. data/lib/datadog/tracing/transport/http.rb +4 -0
  79. data/lib/datadog/tracing/workers.rb +1 -1
  80. data/lib/datadog/tracing/writer.rb +26 -28
  81. data/lib/datadog/version.rb +1 -1
  82. 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