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.
- 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
|