elastic-apm 2.6.1 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.

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