datadog 2.11.0 → 2.12.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -2
  3. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +23 -6
  4. data/lib/datadog/appsec/contrib/active_record/patcher.rb +63 -12
  5. data/lib/datadog/appsec/contrib/rest_client/integration.rb +45 -0
  6. data/lib/datadog/appsec/contrib/rest_client/patcher.rb +28 -0
  7. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +39 -0
  8. data/lib/datadog/appsec.rb +1 -0
  9. data/lib/datadog/core/configuration/components.rb +10 -9
  10. data/lib/datadog/core/metrics/client.rb +9 -8
  11. data/lib/datadog/core/remote/client.rb +5 -4
  12. data/lib/datadog/core/remote/component.rb +14 -12
  13. data/lib/datadog/core/remote/negotiation.rb +1 -1
  14. data/lib/datadog/core/remote/transport/http.rb +4 -33
  15. data/lib/datadog/core/remote/worker.rb +10 -7
  16. data/lib/datadog/core/telemetry/component.rb +5 -1
  17. data/lib/datadog/core/telemetry/worker.rb +9 -5
  18. data/lib/datadog/core/transport/http.rb +38 -0
  19. data/lib/datadog/core/workers/runtime_metrics.rb +1 -1
  20. data/lib/datadog/di/component.rb +1 -3
  21. data/lib/datadog/di/probe_notifier_worker.rb +20 -4
  22. data/lib/datadog/di/transport/diagnostics.rb +61 -0
  23. data/lib/datadog/di/transport/http/api.rb +52 -0
  24. data/lib/datadog/di/transport/http/client.rb +46 -0
  25. data/lib/datadog/di/transport/http/diagnostics.rb +92 -0
  26. data/lib/datadog/di/transport/http/input.rb +94 -0
  27. data/lib/datadog/di/transport/http.rb +105 -0
  28. data/lib/datadog/di/transport/input.rb +61 -0
  29. data/lib/datadog/di.rb +2 -1
  30. data/lib/datadog/tracing/component.rb +1 -0
  31. data/lib/datadog/tracing/sync_writer.rb +9 -4
  32. data/lib/datadog/tracing/tracer.rb +15 -7
  33. data/lib/datadog/tracing/transport/http.rb +3 -32
  34. data/lib/datadog/tracing/workers/trace_writer.rb +10 -3
  35. data/lib/datadog/tracing/workers.rb +5 -4
  36. data/lib/datadog/tracing/writer.rb +12 -4
  37. data/lib/datadog/version.rb +2 -2
  38. metadata +15 -5
  39. data/lib/datadog/di/transport.rb +0 -79
@@ -5,7 +5,7 @@ module Datadog
5
5
  module Remote
6
6
  # Worker executes a block every interval on a separate Thread
7
7
  class Worker
8
- def initialize(interval:, &block)
8
+ def initialize(interval:, logger:, &block)
9
9
  @mutex = Mutex.new
10
10
  @thr = nil
11
11
 
@@ -14,18 +14,21 @@ module Datadog
14
14
  @stopped = false
15
15
 
16
16
  @interval = interval
17
+ @logger = logger
17
18
  raise ArgumentError, 'can not initialize a worker without a block' unless block
18
19
 
19
20
  @block = block
20
21
  end
21
22
 
23
+ attr_reader :logger
24
+
22
25
  def start
23
- Datadog.logger.debug { 'remote worker starting' }
26
+ logger.debug { 'remote worker starting' }
24
27
 
25
28
  acquire_lock
26
29
 
27
30
  if @stopped
28
- Datadog.logger.debug('remote worker: refusing to restart after previous stop')
31
+ logger.debug('remote worker: refusing to restart after previous stop')
29
32
  return
30
33
  end
31
34
 
@@ -41,13 +44,13 @@ module Datadog
41
44
  @started = true
42
45
  @starting = false
43
46
 
44
- Datadog.logger.debug { 'remote worker started' }
47
+ logger.debug { 'remote worker started' }
45
48
  ensure
46
49
  release_lock
47
50
  end
48
51
 
49
52
  def stop
50
- Datadog.logger.debug { 'remote worker stopping' }
53
+ logger.debug { 'remote worker stopping' }
51
54
 
52
55
  acquire_lock
53
56
 
@@ -62,7 +65,7 @@ module Datadog
62
65
  @thr = nil
63
66
  @stopped = true
64
67
 
65
- Datadog.logger.debug { 'remote worker stopped' }
68
+ logger.debug { 'remote worker stopped' }
66
69
  ensure
67
70
  release_lock
68
71
  end
@@ -92,7 +95,7 @@ module Datadog
92
95
  end
93
96
 
94
97
  def call
95
- Datadog.logger.debug { 'remote worker perform' }
98
+ logger.debug { 'remote worker perform' }
96
99
 
97
100
  @block.call
98
101
  end
@@ -16,7 +16,7 @@ module Datadog
16
16
  # Telemetry entrypoint, coordinates sending telemetry events at various points in app lifecycle.
17
17
  # Note: Telemetry does not spawn its worker thread in fork processes, thus no telemetry is sent in forked processes.
18
18
  class Component
19
- attr_reader :enabled
19
+ attr_reader :enabled, :logger
20
20
 
21
21
  include Core::Utils::Forking
22
22
  include Telemetry::Logging
@@ -52,6 +52,7 @@ module Datadog
52
52
  heartbeat_interval_seconds: settings.telemetry.heartbeat_interval_seconds,
53
53
  metrics_aggregation_interval_seconds: settings.telemetry.metrics_aggregation_interval_seconds,
54
54
  dependency_collection: settings.telemetry.dependency_collection,
55
+ logger: logger,
55
56
  shutdown_timeout_seconds: settings.telemetry.shutdown_timeout_seconds,
56
57
  log_collection_enabled: settings.telemetry.log_collection_enabled
57
58
  )
@@ -66,6 +67,7 @@ module Datadog
66
67
  heartbeat_interval_seconds:,
67
68
  metrics_aggregation_interval_seconds:,
68
69
  dependency_collection:,
70
+ logger:,
69
71
  http_transport:,
70
72
  shutdown_timeout_seconds:,
71
73
  enabled: true,
@@ -74,6 +76,7 @@ module Datadog
74
76
  )
75
77
  @enabled = enabled
76
78
  @log_collection_enabled = log_collection_enabled
79
+ @logger = logger
77
80
 
78
81
  @metrics_manager = MetricsManager.new(
79
82
  enabled: enabled && metrics_enabled,
@@ -87,6 +90,7 @@ module Datadog
87
90
  emitter: Emitter.new(http_transport: http_transport),
88
91
  metrics_manager: @metrics_manager,
89
92
  dependency_collection: dependency_collection,
93
+ logger: logger,
90
94
  shutdown_timeout: shutdown_timeout_seconds
91
95
  )
92
96
 
@@ -25,6 +25,7 @@ module Datadog
25
25
  emitter:,
26
26
  metrics_manager:,
27
27
  dependency_collection:,
28
+ logger:,
28
29
  enabled: true,
29
30
  shutdown_timeout: Workers::Polling::DEFAULT_SHUTDOWN_TIMEOUT,
30
31
  buffer_size: DEFAULT_BUFFER_MAX_SIZE
@@ -32,6 +33,7 @@ module Datadog
32
33
  @emitter = emitter
33
34
  @metrics_manager = metrics_manager
34
35
  @dependency_collection = dependency_collection
36
+ @logger = logger
35
37
 
36
38
  @ticks_per_heartbeat = (heartbeat_interval_seconds / metrics_aggregation_interval_seconds).to_i
37
39
  @current_ticks = 0
@@ -48,6 +50,8 @@ module Datadog
48
50
  self.buffer = buffer_klass.new(@buffer_size)
49
51
  end
50
52
 
53
+ attr_reader :logger
54
+
51
55
  def start
52
56
  return if !enabled? || forked?
53
57
 
@@ -99,7 +103,7 @@ module Datadog
99
103
 
100
104
  events = deduplicate_logs(events)
101
105
 
102
- Datadog.logger.debug { "Sending #{events&.count} telemetry events" }
106
+ logger.debug { "Sending #{events&.count} telemetry events" }
103
107
  send_event(Event::MessageBatch.new(events))
104
108
  end
105
109
 
@@ -113,7 +117,7 @@ module Datadog
113
117
  return unless enabled?
114
118
 
115
119
  if failed_to_start?
116
- Datadog.logger.debug('Telemetry app-started event exhausted retries, disabling telemetry worker')
120
+ logger.debug('Telemetry app-started event exhausted retries, disabling telemetry worker')
117
121
  disable!
118
122
  return
119
123
  end
@@ -122,13 +126,13 @@ module Datadog
122
126
  res = send_event(Event::AppStarted.new)
123
127
 
124
128
  if res.ok?
125
- Datadog.logger.debug('Telemetry app-started event is successfully sent')
129
+ logger.debug('Telemetry app-started event is successfully sent')
126
130
 
127
131
  send_event(Event::AppDependenciesLoaded.new) if @dependency_collection
128
132
 
129
133
  true
130
134
  else
131
- Datadog.logger.debug('Error sending telemetry app-started event, retry after heartbeat interval...')
135
+ logger.debug('Error sending telemetry app-started event, retry after heartbeat interval...')
132
136
  false
133
137
  end
134
138
  end
@@ -166,7 +170,7 @@ module Datadog
166
170
  def disable_on_not_found!(response)
167
171
  return unless response.not_found?
168
172
 
169
- Datadog.logger.debug('Agent does not support telemetry; disabling future telemetry events.')
173
+ logger.debug('Agent does not support telemetry; disabling future telemetry events.')
170
174
  disable!
171
175
  end
172
176
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'http/builder'
4
+ require_relative 'http/adapters/net'
5
+ require_relative 'http/adapters/unix_socket'
6
+ require_relative 'http/adapters/test'
7
+
8
+ module Datadog
9
+ module Core
10
+ module Transport
11
+ # HTTP transport
12
+ module HTTP
13
+ # Add adapters to registry
14
+ Builder::REGISTRY.set(
15
+ Transport::HTTP::Adapters::Net,
16
+ Core::Configuration::Ext::Agent::HTTP::ADAPTER
17
+ )
18
+ Builder::REGISTRY.set(
19
+ Transport::HTTP::Adapters::Test,
20
+ Transport::Ext::Test::ADAPTER
21
+ )
22
+ Builder::REGISTRY.set(
23
+ Transport::HTTP::Adapters::UnixSocket,
24
+ Transport::Ext::UnixSocket::ADAPTER
25
+ )
26
+
27
+ module_function
28
+
29
+ # Helper function that delegates to Builder.new
30
+ # but is under HTTP namespace so that client code requires this file
31
+ # to get the adapters configured, and not the builder directly.
32
+ def build(api_instance_class:, &block)
33
+ Builder.new(api_instance_class: api_instance_class, &block)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -21,7 +21,7 @@ module Datadog
21
21
  :metrics
22
22
 
23
23
  def initialize(options = {})
24
- @metrics = options.fetch(:metrics) { Core::Runtime::Metrics.new }
24
+ @metrics = options.fetch(:metrics) { Core::Runtime::Metrics.new(logger: options[:logger]) }
25
25
 
26
26
  # Workers::Async::Thread settings
27
27
  self.fork_policy = options.fetch(:fork_policy, Workers::Async::Thread::FORK_POLICY_STOP)
@@ -81,8 +81,7 @@ module Datadog
81
81
  @redactor = Redactor.new(settings)
82
82
  @serializer = Serializer.new(settings, redactor, telemetry: telemetry)
83
83
  @instrumenter = Instrumenter.new(settings, serializer, logger, code_tracker: code_tracker, telemetry: telemetry)
84
- @transport = Transport.new(agent_settings)
85
- @probe_notifier_worker = ProbeNotifierWorker.new(settings, transport, logger, telemetry: telemetry)
84
+ @probe_notifier_worker = ProbeNotifierWorker.new(settings, logger, agent_settings: agent_settings, telemetry: telemetry)
86
85
  @probe_notification_builder = ProbeNotificationBuilder.new(settings, serializer)
87
86
  @probe_manager = ProbeManager.new(settings, instrumenter, probe_notification_builder, probe_notifier_worker, logger, telemetry: telemetry)
88
87
  probe_notifier_worker.start
@@ -94,7 +93,6 @@ module Datadog
94
93
  attr_reader :telemetry
95
94
  attr_reader :code_tracker
96
95
  attr_reader :instrumenter
97
- attr_reader :transport
98
96
  attr_reader :probe_notifier_worker
99
97
  attr_reader :probe_notification_builder
100
98
  attr_reader :probe_manager
@@ -23,12 +23,12 @@ module Datadog
23
23
  #
24
24
  # @api private
25
25
  class ProbeNotifierWorker
26
- def initialize(settings, transport, logger, telemetry: nil)
26
+ def initialize(settings, logger, agent_settings:, telemetry: nil)
27
27
  @settings = settings
28
28
  @telemetry = telemetry
29
29
  @status_queue = []
30
30
  @snapshot_queue = []
31
- @transport = transport
31
+ @agent_settings = agent_settings
32
32
  @logger = logger
33
33
  @lock = Mutex.new
34
34
  @wake = Core::Semaphore.new
@@ -43,6 +43,7 @@ module Datadog
43
43
  attr_reader :settings
44
44
  attr_reader :logger
45
45
  attr_reader :telemetry
46
+ attr_reader :agent_settings
46
47
 
47
48
  def start
48
49
  return if @thread && @pid == Process.pid
@@ -154,7 +155,6 @@ module Datadog
154
155
 
155
156
  private
156
157
 
157
- attr_reader :transport
158
158
  attr_reader :wake
159
159
  attr_reader :thread
160
160
 
@@ -170,6 +170,22 @@ module Datadog
170
170
 
171
171
  attr_reader :last_sent
172
172
 
173
+ def status_transport
174
+ @status_transport ||= DI::Transport::HTTP.diagnostics(agent_settings: agent_settings)
175
+ end
176
+
177
+ def do_send_status(batch)
178
+ status_transport.send_diagnostics(batch)
179
+ end
180
+
181
+ def snapshot_transport
182
+ @snapshot_transport ||= DI::Transport::HTTP.input(agent_settings: agent_settings)
183
+ end
184
+
185
+ def do_send_snapshot(batch)
186
+ snapshot_transport.send_input(batch)
187
+ end
188
+
173
189
  [
174
190
  [:status, 'probe status'],
175
191
  [:snapshot, 'snapshot'],
@@ -245,7 +261,7 @@ module Datadog
245
261
  if batch.any? # steep:ignore
246
262
  begin
247
263
  logger.trace { "di: sending #{batch.length} #{event_type} event(s) to agent" } # steep:ignore
248
- transport.public_send("send_#{event_type}", batch)
264
+ send("do_send_#{event_type}", batch)
249
265
  time = Core::Utils::Time.get_time
250
266
  @lock.synchronize do
251
267
  @last_sent = time
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../core/transport/parcel'
4
+ require_relative 'http/client'
5
+
6
+ module Datadog
7
+ module DI
8
+ module Transport
9
+ module Diagnostics
10
+ class EncodedParcel
11
+ include Datadog::Core::Transport::Parcel
12
+ end
13
+
14
+ class Request < Datadog::Core::Transport::Request
15
+ end
16
+
17
+ class Transport
18
+ attr_reader :client, :apis, :default_api, :current_api_id
19
+
20
+ def initialize(apis, default_api)
21
+ @apis = apis
22
+
23
+ @client = HTTP::Client.new(current_api)
24
+ end
25
+
26
+ def current_api
27
+ @apis[HTTP::API::DIAGNOSTICS]
28
+ end
29
+
30
+ def send_diagnostics(payload)
31
+ json = JSON.dump(payload)
32
+ parcel = EncodedParcel.new(json)
33
+ request = Request.new(parcel)
34
+
35
+ response = @client.send_diagnostics_payload(request)
36
+ unless response.ok?
37
+ # TODO Datadog::Core::Transport::InternalErrorResponse
38
+ # does not have +code+ method, what is the actual API of
39
+ # these response objects?
40
+ raise Error::AgentCommunicationError, "send_diagnostics failed: #{begin
41
+ response.code
42
+ rescue
43
+ "???"
44
+ end}: #{response.payload}"
45
+ end
46
+ rescue Error::AgentCommunicationError
47
+ raise
48
+ # Datadog::Core::Transport does not perform any exception mapping,
49
+ # therefore we could have any exception here from failure to parse
50
+ # agent URI for example.
51
+ # If we ever implement retries for network errors, we should distinguish
52
+ # actual network errors from non-network errors that are raised by
53
+ # transport code.
54
+ rescue => exc
55
+ raise Error::AgentCommunicationError, "send_diagnostics failed: #{exc.class}: #{exc}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../core/encoding'
4
+ require_relative '../../../core/transport/http/api/map'
5
+ require_relative '../../../core/transport/http/api/instance'
6
+ require_relative '../../../core/transport/http/api/spec'
7
+ require_relative 'diagnostics'
8
+ require_relative 'input'
9
+
10
+ module Datadog
11
+ module DI
12
+ module Transport
13
+ module HTTP
14
+ # Namespace for API components
15
+ module API
16
+ # Default API versions
17
+ DIAGNOSTICS = 'diagnostics'
18
+ INPUT = 'input'
19
+
20
+ module_function
21
+
22
+ def defaults
23
+ Datadog::Core::Transport::HTTP::API::Map[
24
+ DIAGNOSTICS => Spec.new do |s|
25
+ s.diagnostics = Diagnostics::API::Endpoint.new(
26
+ '/debugger/v1/diagnostics',
27
+ Core::Encoding::JSONEncoder,
28
+ )
29
+ end,
30
+ INPUT => Spec.new do |s|
31
+ s.input = Input::API::Endpoint.new(
32
+ '/debugger/v1/input',
33
+ Core::Encoding::JSONEncoder,
34
+ )
35
+ end,
36
+ ]
37
+ end
38
+
39
+ class Instance < Core::Transport::HTTP::API::Instance
40
+ include Diagnostics::API::Instance
41
+ include Input::API::Instance
42
+ end
43
+
44
+ class Spec < Core::Transport::HTTP::API::Spec
45
+ include Diagnostics::API::Spec
46
+ include Input::API::Spec
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../core/transport/http/env'
4
+ require_relative '../../../core/transport/http/response'
5
+
6
+ # TODO: Decouple transport/http/client
7
+ #
8
+ # The standard one does `include Transport::HTTP::Statistics` and performs
9
+ # stats updates, which may or may not be desirable in general.
10
+
11
+ module Datadog
12
+ module DI
13
+ module Transport
14
+ module HTTP
15
+ # Routes, encodes, and sends DI data to the trace agent via HTTP.
16
+ class Client
17
+ attr_reader :api
18
+
19
+ def initialize(api)
20
+ @api = api
21
+ end
22
+
23
+ def send_request(request, &block)
24
+ # Build request into env
25
+ env = build_env(request)
26
+
27
+ # Get responses from API
28
+ yield(api, env)
29
+ rescue => e
30
+ message =
31
+ "Internal error during #{self.class.name} request. Cause: #{e.class.name} #{e.message} " \
32
+ "Location: #{Array(e.backtrace).first}"
33
+
34
+ Datadog.logger.debug(message)
35
+
36
+ Datadog::Core::Transport::InternalErrorResponse.new(e)
37
+ end
38
+
39
+ def build_env(request)
40
+ Datadog::Core::Transport::HTTP::Env.new(request)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client'
4
+
5
+ module Datadog
6
+ module DI
7
+ module Transport
8
+ module HTTP
9
+ module Diagnostics
10
+ module Client
11
+ def send_diagnostics_payload(request)
12
+ send_request(request) do |api, env|
13
+ api.send_diagnostics(env)
14
+ end
15
+ end
16
+ end
17
+
18
+ module API
19
+ module Instance
20
+ def send_diagnostics(env)
21
+ raise DiagnosticsNotSupportedError, spec unless spec.is_a?(Diagnostics::API::Spec)
22
+
23
+ spec.send_diagnostics(env) do |request_env|
24
+ call(request_env)
25
+ end
26
+ end
27
+
28
+ class DiagnosticsNotSupportedError < StandardError
29
+ attr_reader :spec
30
+
31
+ def initialize(spec)
32
+ super
33
+
34
+ @spec = spec
35
+ end
36
+
37
+ def message
38
+ 'Diagnostics not supported for this API!'
39
+ end
40
+ end
41
+ end
42
+
43
+ module Spec
44
+ attr_accessor :diagnostics
45
+
46
+ def send_diagnostics(env, &block)
47
+ raise NoDiagnosticsEndpointDefinedError, self if diagnostics.nil?
48
+
49
+ diagnostics.call(env, &block)
50
+ end
51
+
52
+ class NoDiagnosticsEndpointDefinedError < StandardError
53
+ attr_reader :spec
54
+
55
+ def initialize(spec)
56
+ super
57
+
58
+ @spec = spec
59
+ end
60
+
61
+ def message
62
+ 'No diagnostics endpoint is defined for API specification!'
63
+ end
64
+ end
65
+ end
66
+
67
+ # Endpoint for negotiation
68
+ class Endpoint < Datadog::Core::Transport::HTTP::API::Endpoint
69
+ attr_reader :encoder
70
+
71
+ def initialize(path, encoder)
72
+ super(:post, path)
73
+ @encoder = encoder
74
+ end
75
+
76
+ def call(env, &block)
77
+ event_payload = Core::Vendor::Multipart::Post::UploadIO.new(
78
+ StringIO.new(env.request.parcel.data), 'application/json', 'event.json'
79
+ )
80
+ env.form = {'event' => event_payload}
81
+
82
+ super(env, &block)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ HTTP::Client.include(Diagnostics::Client)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client'
4
+
5
+ module Datadog
6
+ module DI
7
+ module Transport
8
+ module HTTP
9
+ module Input
10
+ module Client
11
+ def send_input_payload(request)
12
+ send_request(request) do |api, env|
13
+ api.send_input(env)
14
+ end
15
+ end
16
+ end
17
+
18
+ module API
19
+ module Instance
20
+ def send_input(env)
21
+ raise InputNotSupportedError, spec unless spec.is_a?(Input::API::Spec)
22
+
23
+ spec.send_input(env) do |request_env|
24
+ call(request_env)
25
+ end
26
+ end
27
+
28
+ class InputNotSupportedError < StandardError
29
+ attr_reader :spec
30
+
31
+ def initialize(spec)
32
+ super
33
+
34
+ @spec = spec
35
+ end
36
+
37
+ def message
38
+ 'Input not supported for this API!'
39
+ end
40
+ end
41
+ end
42
+
43
+ module Spec
44
+ attr_accessor :input
45
+
46
+ def send_input(env, &block)
47
+ raise NoInputEndpointDefinedError, self if input.nil?
48
+
49
+ input.call(env, &block)
50
+ end
51
+
52
+ class NoInputEndpointDefinedError < StandardError
53
+ attr_reader :spec
54
+
55
+ def initialize(spec)
56
+ super
57
+
58
+ @spec = spec
59
+ end
60
+
61
+ def message
62
+ 'No input endpoint is defined for API specification!'
63
+ end
64
+ end
65
+ end
66
+
67
+ # Endpoint for negotiation
68
+ class Endpoint < Datadog::Core::Transport::HTTP::API::Endpoint
69
+ HEADER_CONTENT_TYPE = 'Content-Type'
70
+
71
+ attr_reader \
72
+ :encoder
73
+
74
+ def initialize(path, encoder)
75
+ super(:post, path)
76
+ @encoder = encoder
77
+ end
78
+
79
+ def call(env, &block)
80
+ # Encode body & type
81
+ env.headers[HEADER_CONTENT_TYPE] = encoder.content_type
82
+ env.body = env.request.parcel.data
83
+
84
+ super(env, &block)
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ HTTP::Client.include(Input::Client)
91
+ end
92
+ end
93
+ end
94
+ end