elastic-apm 2.6.1 → 2.7.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.

Potentially problematic release.


This version of elastic-apm might be problematic. Click here for more details.

@@ -23,7 +23,10 @@ module ElasticAPM
23
23
  end
24
24
 
25
25
  config.after_initialize do
26
- require 'elastic_apm/spies/action_dispatch'
26
+ if ElasticAPM.running? &&
27
+ !ElasticAPM.agent.config.disabled_spies.include?('action_dispatch')
28
+ require 'elastic_apm/spies/action_dispatch'
29
+ end
27
30
  end
28
31
 
29
32
  private
@@ -31,8 +34,10 @@ module ElasticAPM
31
34
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
32
35
  def start(config)
33
36
  if (reason = should_skip?(config))
34
- config.alert_logger.info "Skipping because: #{reason}. " \
35
- "Start manually with `ElasticAPM.start'"
37
+ unless config.disable_start_message?
38
+ config.alert_logger.info "Skipping because: #{reason}. " \
39
+ "Start manually with `ElasticAPM.start'"
40
+ end
36
41
  return
37
42
  end
38
43
 
@@ -5,6 +5,14 @@ module ElasticAPM
5
5
  module Spies
6
6
  # @api private
7
7
  class FaradaySpy
8
+ def self.without_net_http
9
+ return yield unless defined?(NetHTTPSpy)
10
+
11
+ ElasticAPM::Spies::NetHTTPSpy.disable_in do
12
+ yield
13
+ end
14
+ end
15
+
8
16
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
9
17
  # rubocop:disable Metrics/BlockLength, Metrics/PerceivedComplexity
10
18
  # rubocop:disable Metrics/CyclomaticComplexity
@@ -32,7 +40,7 @@ module ElasticAPM
32
40
  type = "ext.faraday.#{method}"
33
41
 
34
42
  ElasticAPM.with_span name, type do |span|
35
- ElasticAPM::Spies::NetHTTPSpy.disable_in do
43
+ ElasticAPM::Spies::FaradaySpy.without_net_http do
36
44
  trace_context = span&.trace_context || transaction.trace_context
37
45
 
38
46
  run_request_without_apm(method, url, body, headers) do |req|
@@ -5,40 +5,70 @@ require 'elastic_apm/transport/connection'
5
5
  require 'elastic_apm/transport/worker'
6
6
  require 'elastic_apm/transport/serializers'
7
7
  require 'elastic_apm/transport/filters'
8
+ require 'elastic_apm/util/throttle'
8
9
 
9
10
  module ElasticAPM
10
11
  module Transport
12
+ # rubocop:disable Metrics/ClassLength
11
13
  # @api private
12
14
  class Base
13
15
  include Logging
14
16
 
17
+ WATCHER_EXECUTION_INTERVAL = 5
18
+ WATCHER_TIMEOUT_INTERVAL = 4
19
+ WORKER_JOIN_TIMEOUT = 5
20
+
15
21
  def initialize(config)
16
22
  @config = config
17
23
  @queue = SizedQueue.new(config.api_buffer_size)
18
- @pool = Concurrent::FixedThreadPool.new(config.pool_size)
19
- @workers = []
20
24
 
21
25
  @serializers = Serializers.new(config)
22
26
  @filters = Filters.new(config)
27
+
28
+ @stopped = Concurrent::AtomicBoolean.new
29
+ @workers = Array.new(config.pool_size)
30
+
31
+ @watcher_mutex = Mutex.new
32
+ @worker_mutex = Mutex.new
23
33
  end
24
34
 
25
- attr_reader :config, :queue, :workers, :filters
35
+ attr_reader :config, :queue, :filters, :workers, :watcher, :stopped
26
36
 
27
37
  def start
38
+ debug '%s: Starting Transport', pid_str
39
+
40
+ ensure_watcher_running
41
+ ensure_worker_count
28
42
  end
29
43
 
30
44
  def stop
45
+ debug '%s: Stopping Transport', pid_str
46
+
47
+ @stopped.make_true
48
+
49
+ stop_watcher
31
50
  stop_workers
32
51
  end
33
52
 
53
+ # rubocop:disable Metrics/MethodLength
34
54
  def submit(resource)
55
+ if @stopped.true?
56
+ warn '%s: Transport stopping, no new events accepted', pid_str
57
+ return false
58
+ end
59
+
60
+ ensure_watcher_running
35
61
  queue.push(resource, true)
36
62
 
37
- ensure_worker_count
63
+ true
38
64
  rescue ThreadError
39
- warn 'Queue is full (%i items), skipping…', config.api_buffer_size
65
+ throttled_queue_full_warning
66
+ nil
67
+ rescue Exception => e
68
+ error '%s: Failed adding to the transport queue: %p', pid_str, e.inspect
40
69
  nil
41
70
  end
71
+ # rubocop:enable Metrics/MethodLength
42
72
 
43
73
  def add_filter(key, callback)
44
74
  @filters.add(key, callback)
@@ -46,52 +76,99 @@ module ElasticAPM
46
76
 
47
77
  private
48
78
 
79
+ def pid_str
80
+ format('[PID:%s]', Process.pid)
81
+ end
82
+
83
+ def ensure_watcher_running
84
+ # pid has changed == we've forked
85
+ return if @pid == Process.pid
86
+
87
+ @watcher_mutex.synchronize do
88
+ return if @pid == Process.pid
89
+ @pid = Process.pid
90
+
91
+ @watcher = Concurrent::TimerTask.execute(
92
+ execution_interval: WATCHER_EXECUTION_INTERVAL,
93
+ timeout_interval: WATCHER_TIMEOUT_INTERVAL
94
+ ) { ensure_worker_count }
95
+ end
96
+ end
97
+
49
98
  def ensure_worker_count
50
- missing = config.pool_size - @workers.length
51
- return unless missing > 0
99
+ @worker_mutex.synchronize do
100
+ return if all_workers_alive?
101
+ return if stopped.true?
52
102
 
53
- info 'Booting %i workers', missing
54
- missing.times { boot_worker }
103
+ @workers.map! do |thread|
104
+ next thread if thread&.alive?
105
+
106
+ boot_worker
107
+ end
108
+ end
109
+ end
110
+
111
+ def all_workers_alive?
112
+ !!workers.all? { |t| t&.alive? }
55
113
  end
56
114
 
57
- # rubocop:disable Metrics/MethodLength
58
115
  def boot_worker
59
- worker = Worker.new(
60
- config,
61
- queue,
62
- serializers: @serializers,
63
- filters: @filters
64
- )
65
-
66
- @workers.push worker
67
-
68
- @pool.post do
69
- worker.work_forever
70
- @workers.delete(worker)
116
+ debug '%s: Booting worker...', pid_str
117
+
118
+ Thread.new do
119
+ Worker.new(
120
+ config, queue,
121
+ serializers: @serializers,
122
+ filters: @filters
123
+ ).work_forever
71
124
  end
72
125
  end
73
- # rubocop:enable Metrics/MethodLength
74
126
 
127
+ # rubocop:disable Metrics/MethodLength
75
128
  def stop_workers
76
- return unless @pool.running?
129
+ debug '%s: Stopping workers', pid_str
77
130
 
78
- debug 'Stopping workers'
79
131
  send_stop_messages
80
132
 
81
- debug 'Shutting down pool'
82
- @pool.shutdown
133
+ @worker_mutex.synchronize do
134
+ workers.each do |thread|
135
+ next if thread.nil?
136
+ next if thread.join(WORKER_JOIN_TIMEOUT)
83
137
 
84
- return if @pool.wait_for_termination(5)
138
+ debug(
139
+ '%s: Worker did not stop in %ds, killing...',
140
+ pid_str, WORKER_JOIN_TIMEOUT
141
+ )
142
+ thread.kill
143
+ end
85
144
 
86
- warn "Worker pool didn't close in 5 secs, killing ..."
87
- @pool.kill
145
+ @workers.clear
146
+ end
88
147
  end
148
+ # rubocop:enable Metrics/MethodLength
89
149
 
90
150
  def send_stop_messages
91
- @workers.each { queue.push(Worker::StopMessage.new, true) }
151
+ config.pool_size.times { queue.push(Worker::StopMessage.new, true) }
92
152
  rescue ThreadError
93
153
  warn 'Cannot push stop messages to worker queue as it is full'
94
154
  end
155
+
156
+ def stop_watcher
157
+ @watcher_mutex.synchronize do
158
+ return if watcher.nil? || @pid != Process.pid
159
+ watcher.shutdown
160
+ end
161
+ end
162
+
163
+ def throttled_queue_full_warning
164
+ (@queue_full_log ||= Util::Throttle.new(5) do
165
+ warn(
166
+ '%s: Queue is full (%i items), skipping…',
167
+ pid_str, config.api_buffer_size
168
+ )
169
+ end).call
170
+ end
95
171
  end
172
+ # rubocop:enable Metrics/ClassLength
96
173
  end
97
174
  end
@@ -1,204 +1,128 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'http'
4
3
  require 'concurrent'
5
4
  require 'zlib'
6
5
 
6
+ require 'elastic_apm/transport/connection/http'
7
+
7
8
  module ElasticAPM
8
9
  module Transport
9
10
  # @api private
10
- class Connection # rubocop:disable Metrics/ClassLength
11
+ class Connection
11
12
  include Logging
12
13
 
13
- class FailedToConnectError < InternalError; end
14
-
15
- # @api private
16
- # HTTP.rb calls #rewind the body stream which IO.pipes don't support
17
- class ModdedIO < IO
18
- def self.pipe(ext_enc = nil)
19
- super(ext_enc).tap do |rw|
20
- rw[0].define_singleton_method(:rewind) { nil }
21
- end
22
- end
23
- end
14
+ # A connection holds an instance `http` of an Http::Connection.
15
+ #
16
+ # The HTTP::Connection itself is not thread safe.
17
+ #
18
+ # The connection sends write requests and close requests to `http`, and
19
+ # has to ensure no write requests are sent after closing `http`.
20
+ #
21
+ # The connection schedules a separate thread to close an `http`
22
+ # connection some time in the future. To avoid the thread interfering
23
+ # with ongoing write requests to `http`, write and close
24
+ # requests have to be synchronized.
24
25
 
25
26
  HEADERS = {
26
27
  'Content-Type' => 'application/x-ndjson',
27
28
  'Transfer-Encoding' => 'chunked'
28
29
  }.freeze
29
- GZIP_HEADERS = HEADERS.merge('Content-Encoding' => 'gzip').freeze
30
+ GZIP_HEADERS = HEADERS.merge(
31
+ 'Content-Encoding' => 'gzip'
32
+ ).freeze
30
33
 
31
- # rubocop:disable Metrics/MethodLength
32
34
  def initialize(config, metadata)
33
35
  @config = config
34
- @metadata = metadata.to_json
35
-
36
+ @metadata = JSON.fast_generate(metadata)
36
37
  @url = config.server_url + '/intake/v2/events'
37
-
38
- headers =
39
- (@config.http_compression? ? GZIP_HEADERS : HEADERS).dup
40
-
41
- if (token = config.secret_token)
42
- headers['Authorization'] = "Bearer #{token}"
43
- end
44
-
45
- @client = HTTP.headers(headers).persistent(@url)
46
-
47
- configure_proxy
48
- configure_ssl
49
-
38
+ @headers = build_headers
39
+ @ssl_context = build_ssl_context
50
40
  @mutex = Mutex.new
51
41
  end
52
- # rubocop:enable Metrics/MethodLength
53
-
54
- def configure_proxy
55
- unless @config.proxy_address && @config.proxy_port
56
- return
57
- end
58
-
59
- @client = @client.via(
60
- @config.proxy_address,
61
- @config.proxy_port,
62
- @config.proxy_username,
63
- @config.proxy_password,
64
- @config.proxy_headers
65
- )
66
- end
67
42
 
68
- def configure_ssl
69
- return unless @config.use_ssl? && @config.server_ca_cert
70
-
71
- @ssl_context = OpenSSL::SSL::SSLContext.new.tap do |context|
72
- context.ca_file = @config.server_ca_cert
73
- end
74
- end
43
+ attr_reader :http
75
44
 
45
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
76
46
  def write(str)
77
- return if @config.disable_send
78
-
79
- connect_unless_connected
80
-
81
- @mutex.synchronize { append(str) }
82
-
83
- return unless @bytes_sent >= @config.api_request_size
84
-
85
- flush
86
- rescue FailedToConnectError => e
87
- error "Couldn't establish connection to APM Server:\n%p", e
88
- flush
89
-
90
- nil
91
- end
47
+ return false if @config.disable_send
92
48
 
93
- def connected?
94
- @mutex.synchronize { @connected }
95
- end
49
+ begin
50
+ bytes_written = 0
96
51
 
97
- def flush
98
- @mutex.synchronize do
99
- return unless @connected
52
+ # The request might get closed from timertask so let's make sure we
53
+ # hold it open until we've written.
54
+ @mutex.synchronize do
55
+ connect if http.nil? || http.closed?
56
+ bytes_written = http.write(str)
57
+ end
100
58
 
101
- debug 'Closing request'
102
- @wr.close
103
- @conn_thread.join 5 if @conn_thread
59
+ flush(:api_request_size) if bytes_written >= @config.api_request_size
60
+ rescue IOError => e
61
+ error('Connection error: %s', e.inspect)
62
+ flush(:ioerror)
63
+ rescue Errno::EPIPE => e
64
+ error('Connection error: %s', e.inspect)
65
+ flush(:broken_pipe)
66
+ rescue Exception => e
67
+ error('Connection error: %s', e.inspect)
68
+ flush(:connection_error)
104
69
  end
105
70
  end
71
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
106
72
 
107
- private
108
-
109
- # rubocop:disable Metrics/MethodLength
110
- def connect_unless_connected
73
+ def flush(reason = :force)
74
+ # Could happen from the timertask so we need to sync
111
75
  @mutex.synchronize do
112
- return true if @connected
113
-
114
- debug 'Opening new request'
115
-
116
- reset!
117
-
118
- @rd, @wr = ModdedIO.pipe
119
-
120
- enable_compression! if @config.http_compression?
121
-
122
- perform_request_in_thread
123
- wait_for_connection
124
-
125
- schedule_closing if @config.api_request_time
126
-
127
- append(@metadata)
128
-
129
- true
76
+ return if http.nil?
77
+ http.close(reason)
130
78
  end
131
79
  end
132
- # rubocop:enable Metrics/MethodLength
133
-
134
- # rubocop:disable Metrics/MethodLength
135
- def perform_request_in_thread
136
- @conn_thread = Thread.new do
137
- begin
138
- @connected = true
139
-
140
- resp = @client.post(
141
- @url,
142
- body: @rd,
143
- ssl_context: @ssl_context
144
- ).flush
145
- rescue Exception => e
146
- @connection_error = e
147
- ensure
148
- @connected = false
149
- end
150
80
 
151
- if resp&.status == 202
152
- debug 'APM Server responded with status 202'
153
- elsif resp
154
- error "APM Server responded with an error:\n%p", resp.body.to_s
155
- end
156
-
157
- resp
158
- end
81
+ def inspect
82
+ format(
83
+ '@%s http connection closed? :%s>',
84
+ super.split.first,
85
+ http.closed?
86
+ )
159
87
  end
160
- # rubocop:enable Metrics/MethodLength
161
-
162
- def append(str)
163
- bytes =
164
- if @config.http_compression
165
- @bytes_sent = @wr.tell
166
- else
167
- @bytes_sent += str.bytesize
168
- end
169
88
 
170
- debug 'Bytes sent during this request: %d', bytes
89
+ private
90
+
91
+ def connect
92
+ schedule_closing if @config.api_request_time
171
93
 
172
- @wr.puts(str)
94
+ @http =
95
+ Http.open(
96
+ @config, @url,
97
+ headers: @headers,
98
+ ssl_context: @ssl_context
99
+ ).tap { |http| http.write(@metadata) }
173
100
  end
101
+ # rubocop:enable
174
102
 
175
103
  def schedule_closing
104
+ @close_task&.cancel
176
105
  @close_task =
177
106
  Concurrent::ScheduledTask.execute(@config.api_request_time) do
178
- flush
107
+ flush(:timeout)
179
108
  end
180
109
  end
181
110
 
182
- def enable_compression!
183
- @wr.binmode
184
- @wr = Zlib::GzipWriter.new(@wr)
185
- end
186
-
187
- def reset!
188
- @bytes_sent = 0
189
- @connected = false
190
- @connection_error = nil
191
- @close_task = nil
111
+ def build_headers
112
+ (
113
+ @config.http_compression? ? GZIP_HEADERS : HEADERS
114
+ ).dup.tap do |headers|
115
+ if (token = @config.secret_token)
116
+ headers['Authorization'] = "Bearer #{token}"
117
+ end
118
+ end
192
119
  end
193
120
 
194
- def wait_for_connection
195
- until @connected
196
- if (exception = @connection_error)
197
- @wr&.close
198
- raise FailedToConnectError, exception
199
- end
121
+ def build_ssl_context
122
+ return unless @config.use_ssl? && @config.server_ca_cert
200
123
 
201
- sleep 0.01
124
+ OpenSSL::SSL::SSLContext.new.tap do |context|
125
+ context.ca_file = @config.server_ca_cert
202
126
  end
203
127
  end
204
128
  end