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.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +18 -1
- data/Jenkinsfile +3 -7
- data/README.md +1 -1
- data/docs/api.asciidoc +6 -0
- data/docs/configuration.asciidoc +16 -4
- data/docs/getting-started-rack.asciidoc +1 -1
- data/docs/getting-started-rails.asciidoc +1 -1
- data/docs/index.asciidoc +2 -0
- data/docs/release-notes.asciidoc +4 -0
- data/lib/elastic_apm/agent.rb +8 -3
- data/lib/elastic_apm/config.rb +4 -1
- data/lib/elastic_apm/error_builder.rb +2 -0
- data/lib/elastic_apm/instrumenter.rb +3 -1
- data/lib/elastic_apm/metadata/system_info.rb +2 -2
- data/lib/elastic_apm/metadata/system_info/container_info.rb +27 -23
- data/lib/elastic_apm/metrics.rb +13 -2
- data/lib/elastic_apm/railtie.rb +8 -3
- data/lib/elastic_apm/spies/faraday.rb +9 -1
- data/lib/elastic_apm/transport/base.rb +108 -31
- data/lib/elastic_apm/transport/connection.rb +77 -153
- data/lib/elastic_apm/transport/connection/http.rb +116 -0
- data/lib/elastic_apm/transport/connection/proxy_pipe.rb +68 -0
- data/lib/elastic_apm/transport/filters/secrets_filter.rb +1 -0
- data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +2 -1
- data/lib/elastic_apm/transport/worker.rb +14 -19
- data/lib/elastic_apm/util.rb +4 -2
- data/lib/elastic_apm/util/throttle.rb +35 -0
- data/lib/elastic_apm/version.rb +1 -1
- metadata +6 -2
data/lib/elastic_apm/railtie.rb
CHANGED
@@ -23,7 +23,10 @@ module ElasticAPM
|
|
23
23
|
end
|
24
24
|
|
25
25
|
config.after_initialize do
|
26
|
-
|
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.
|
35
|
-
"
|
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::
|
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, :
|
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
|
-
|
63
|
+
true
|
38
64
|
rescue ThreadError
|
39
|
-
|
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
|
-
|
51
|
-
|
99
|
+
@worker_mutex.synchronize do
|
100
|
+
return if all_workers_alive?
|
101
|
+
return if stopped.true?
|
52
102
|
|
53
|
-
|
54
|
-
|
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
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
129
|
+
debug '%s: Stopping workers', pid_str
|
77
130
|
|
78
|
-
debug 'Stopping workers'
|
79
131
|
send_stop_messages
|
80
132
|
|
81
|
-
|
82
|
-
|
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
|
-
|
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
|
-
|
87
|
-
|
145
|
+
@workers.clear
|
146
|
+
end
|
88
147
|
end
|
148
|
+
# rubocop:enable Metrics/MethodLength
|
89
149
|
|
90
150
|
def send_stop_messages
|
91
|
-
|
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
|
11
|
+
class Connection
|
11
12
|
include Logging
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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(
|
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
|
35
|
-
|
36
|
+
@metadata = JSON.fast_generate(metadata)
|
36
37
|
@url = config.server_url + '/intake/v2/events'
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
94
|
-
|
95
|
-
end
|
49
|
+
begin
|
50
|
+
bytes_written = 0
|
96
51
|
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
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
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
89
|
+
private
|
90
|
+
|
91
|
+
def connect
|
92
|
+
schedule_closing if @config.api_request_time
|
171
93
|
|
172
|
-
@
|
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
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
195
|
-
|
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
|
-
|
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
|