elastic-apm 1.1.0 → 2.0.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.

Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +7 -1
  5. data/CHANGELOG.md +45 -0
  6. data/Gemfile +17 -12
  7. data/bench/app.rb +1 -2
  8. data/bench/benchmark.rb +1 -1
  9. data/bench/stackprof.rb +1 -1
  10. data/docs/api.asciidoc +115 -76
  11. data/docs/configuration.asciidoc +232 -167
  12. data/docs/context.asciidoc +7 -3
  13. data/docs/custom-instrumentation.asciidoc +17 -28
  14. data/docs/index.asciidoc +13 -7
  15. data/docs/supported-technologies.asciidoc +65 -0
  16. data/elastic-apm.gemspec +3 -2
  17. data/lib/elastic_apm.rb +272 -121
  18. data/lib/elastic_apm/agent.rb +56 -107
  19. data/lib/elastic_apm/config.rb +130 -106
  20. data/lib/elastic_apm/config/duration.rb +25 -0
  21. data/lib/elastic_apm/config/size.rb +28 -0
  22. data/lib/elastic_apm/context_builder.rb +1 -0
  23. data/lib/elastic_apm/deprecations.rb +19 -0
  24. data/lib/elastic_apm/error.rb +5 -2
  25. data/lib/elastic_apm/error/exception.rb +1 -1
  26. data/lib/elastic_apm/error_builder.rb +5 -0
  27. data/lib/elastic_apm/instrumenter.rb +121 -53
  28. data/lib/elastic_apm/internal_error.rb +1 -0
  29. data/lib/elastic_apm/{log.rb → logging.rb} +16 -11
  30. data/lib/elastic_apm/metadata.rb +20 -0
  31. data/lib/elastic_apm/metadata/process_info.rb +26 -0
  32. data/lib/elastic_apm/metadata/service_info.rb +56 -0
  33. data/lib/elastic_apm/metadata/system_info.rb +30 -0
  34. data/lib/elastic_apm/middleware.rb +31 -15
  35. data/lib/elastic_apm/normalizers/action_controller.rb +1 -1
  36. data/lib/elastic_apm/normalizers/action_mailer.rb +1 -1
  37. data/lib/elastic_apm/normalizers/action_view.rb +3 -3
  38. data/lib/elastic_apm/normalizers/active_record.rb +2 -1
  39. data/lib/elastic_apm/railtie.rb +1 -1
  40. data/lib/elastic_apm/span.rb +59 -29
  41. data/lib/elastic_apm/span/context.rb +30 -4
  42. data/lib/elastic_apm/span_helpers.rb +1 -1
  43. data/lib/elastic_apm/spies/delayed_job.rb +7 -7
  44. data/lib/elastic_apm/spies/elasticsearch.rb +4 -4
  45. data/lib/elastic_apm/spies/http.rb +38 -0
  46. data/lib/elastic_apm/spies/mongo.rb +22 -11
  47. data/lib/elastic_apm/spies/net_http.rb +7 -4
  48. data/lib/elastic_apm/spies/rake.rb +5 -6
  49. data/lib/elastic_apm/spies/redis.rb +1 -1
  50. data/lib/elastic_apm/spies/sequel.rb +9 -7
  51. data/lib/elastic_apm/spies/sidekiq.rb +5 -5
  52. data/lib/elastic_apm/spies/tilt.rb +2 -2
  53. data/lib/elastic_apm/sql_summarizer.rb +3 -3
  54. data/lib/elastic_apm/stacktrace_builder.rb +6 -6
  55. data/lib/elastic_apm/subscriber.rb +3 -3
  56. data/lib/elastic_apm/traceparent.rb +62 -0
  57. data/lib/elastic_apm/transaction.rb +62 -93
  58. data/lib/elastic_apm/transport/base.rb +98 -0
  59. data/lib/elastic_apm/transport/connection.rb +175 -0
  60. data/lib/elastic_apm/transport/filters.rb +45 -0
  61. data/lib/elastic_apm/transport/filters/request_body_filter.rb +31 -0
  62. data/lib/elastic_apm/transport/filters/secrets_filter.rb +59 -0
  63. data/lib/elastic_apm/transport/serializers.rb +58 -0
  64. data/lib/elastic_apm/transport/serializers/error_serializer.rb +59 -0
  65. data/lib/elastic_apm/transport/serializers/span_serializer.rb +30 -0
  66. data/lib/elastic_apm/transport/serializers/transaction_serializer.rb +33 -0
  67. data/lib/elastic_apm/transport/worker.rb +73 -0
  68. data/lib/elastic_apm/util.rb +11 -8
  69. data/lib/elastic_apm/version.rb +1 -1
  70. metadata +40 -21
  71. data/.travis.yml +0 -5
  72. data/docs/troubleshooting.asciidoc +0 -28
  73. data/lib/elastic_apm/filters.rb +0 -46
  74. data/lib/elastic_apm/filters/request_body_filter.rb +0 -33
  75. data/lib/elastic_apm/filters/secrets_filter.rb +0 -59
  76. data/lib/elastic_apm/http.rb +0 -139
  77. data/lib/elastic_apm/process_info.rb +0 -24
  78. data/lib/elastic_apm/serializers.rb +0 -28
  79. data/lib/elastic_apm/serializers/errors.rb +0 -61
  80. data/lib/elastic_apm/serializers/transactions.rb +0 -51
  81. data/lib/elastic_apm/service_info.rb +0 -54
  82. data/lib/elastic_apm/system_info.rb +0 -28
  83. data/lib/elastic_apm/util/dig.rb +0 -31
  84. data/lib/elastic_apm/util/inspector.rb +0 -61
  85. data/lib/elastic_apm/worker.rb +0 -106
@@ -5,144 +5,113 @@ require 'securerandom'
5
5
  module ElasticAPM
6
6
  # @api private
7
7
  class Transaction
8
- DEFAULT_TYPE = 'custom'.freeze
8
+ extend Deprecations
9
9
 
10
- # rubocop:disable Metrics/MethodLength
10
+ DEFAULT_TYPE = 'custom'
11
+
12
+ # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
11
13
  def initialize(
12
- instrumenter,
13
14
  name = nil,
14
15
  type = nil,
16
+ sampled: true,
15
17
  context: nil,
16
- sampled: true
18
+ tags: nil,
19
+ traceparent: nil
17
20
  )
18
- @id = SecureRandom.uuid
19
- @instrumenter = instrumenter
20
21
  @name = name
21
22
  @type = type || DEFAULT_TYPE
22
23
 
23
- @timestamp = Util.micros
24
+ @sampled = sampled
24
25
 
25
- @spans = []
26
- @span_id_ticker = -1
27
- @dropped_spans = 0
26
+ @context = context || Context.new # TODO: Lazy generate this?
27
+ Util.reverse_merge!(@context.tags, tags) if tags
28
28
 
29
- @notifications = [] # for AS::Notifications
29
+ @id = SecureRandom.hex(8)
30
30
 
31
- @context = context || Context.new
32
- @context.tags.merge!(instrumenter.config.default_tags) { |_, old, _| old }
31
+ if traceparent
32
+ @traceparent = traceparent
33
+ @parent_id = traceparent.span_id
34
+ else
35
+ @traceparent = Traceparent.from_transaction(self)
36
+ end
33
37
 
34
- @sampled = sampled
38
+ @started_spans = 0
39
+ @dropped_spans = 0
35
40
 
36
- yield self if block_given?
41
+ @notifications = [] # for AS::Notifications
37
42
  end
38
- # rubocop:enable Metrics/MethodLength
43
+ # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
39
44
 
40
- attr_accessor :name, :type
41
- attr_reader :id, :context, :duration, :dropped_spans, :root_span,
42
- :timestamp, :spans, :result, :notifications, :sampled, :instrumenter
45
+ attr_accessor :name, :type, :result
43
46
 
44
- def release
45
- @instrumenter.current_transaction = nil
46
- end
47
+ attr_reader :id, :context, :duration, :started_spans, :dropped_spans,
48
+ :timestamp, :traceparent, :notifications, :parent_id
47
49
 
48
- def done(result = nil)
49
- @duration = Util.micros - @timestamp
50
- @result = result
50
+ def sampled?
51
+ @sampled
52
+ end
51
53
 
52
- self
54
+ def stopped?
55
+ !!duration
53
56
  end
54
57
 
55
58
  def done?
56
- !!@duration
59
+ stopped?
57
60
  end
58
61
 
59
- def submit(result = nil, status: nil, headers: {})
60
- done result unless duration
61
-
62
- if status
63
- context.response = Context::Response.new(status, headers: headers)
64
- end
65
-
66
- release
67
-
68
- @instrumenter.submit_transaction self
62
+ deprecate :done?, :stopped?
69
63
 
70
- self
64
+ def trace_id
65
+ traceparent&.trace_id
71
66
  end
72
67
 
73
- # rubocop:disable Metrics/MethodLength
74
- def span(name, type = nil, backtrace: nil, context: nil)
75
- unless sampled?
76
- return yield if block_given?
77
- return
78
- end
68
+ # life cycle
79
69
 
80
- if spans.length >= instrumenter.config.transaction_max_spans
81
- @dropped_spans += 1
82
-
83
- return yield if block_given?
84
- return
85
- end
86
-
87
- span = build_and_start_span(name, type, context, backtrace)
88
-
89
- return span unless block_given?
90
-
91
- begin
92
- result = yield span
93
- ensure
94
- span.done
95
- end
96
-
97
- result
70
+ def start
71
+ @timestamp = Util.micros
72
+ self
98
73
  end
99
- # rubocop:enable Metrics/MethodLength
100
74
 
101
- def current_span
102
- spans.reverse.lazy.find(&:running?)
75
+ def stop
76
+ raise 'Transaction not yet start' unless timestamp
77
+ @duration = Util.micros - timestamp
78
+ self
103
79
  end
104
80
 
105
- def sampled?
106
- !!sampled
81
+ def done(result = nil)
82
+ stop
83
+ self.result = result if result
84
+ self
107
85
  end
108
86
 
109
- def inspect
110
- "<ElasticAPM::Transaction id:#{id}" \
111
- " name:#{name.inspect}" \
112
- " type:#{type.inspect}" \
113
- '>'
87
+ def ensure_parent_id
88
+ @parent_id ||= SecureRandom.hex(8)
89
+ @parent_id
114
90
  end
115
91
 
116
- private
92
+ # spans
117
93
 
118
- def next_span_id
119
- @span_id_ticker += 1
94
+ def inc_started_spans!
95
+ @started_spans += 1
120
96
  end
121
97
 
122
- def next_span(name, type, context)
123
- Span.new(
124
- self,
125
- next_span_id,
126
- name,
127
- type,
128
- parent: current_span,
129
- context: context
130
- )
98
+ def inc_dropped_spans!
99
+ @dropped_spans += 1
131
100
  end
132
101
 
133
- def span_frames_min_duration?
134
- @instrumenter.agent.config.span_frames_min_duration != 0
102
+ def max_spans_reached?(config)
103
+ started_spans > config.transaction_max_spans
135
104
  end
136
105
 
137
- def build_and_start_span(name, type, context, backtrace)
138
- span = next_span(name, type, context)
139
- spans << span
106
+ # context
140
107
 
141
- if backtrace && span_frames_min_duration?
142
- span.original_backtrace = backtrace
143
- end
108
+ def add_response(*args)
109
+ context.response = Context::Response.new(*args)
110
+ end
144
111
 
145
- span.start
112
+ def inspect
113
+ "<ElasticAPM::Transaction id:#{id}" \
114
+ " name:#{name.inspect} type:#{type.inspect}>"
146
115
  end
147
116
  end
148
117
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elastic_apm/transport/connection'
4
+ require 'elastic_apm/transport/worker'
5
+
6
+ require 'elastic_apm/transport/serializers'
7
+ require 'elastic_apm/transport/filters'
8
+
9
+ module ElasticAPM
10
+ module Transport
11
+ # @api private
12
+ class Base
13
+ include Logging
14
+
15
+ def initialize(config)
16
+ @config = config
17
+ @queue = SizedQueue.new(config.api_buffer_size)
18
+ @pool = Concurrent::FixedThreadPool.new(config.pool_size)
19
+ @workers = []
20
+
21
+ @serializers = Serializers.new(config)
22
+ @filters = Filters.new(config)
23
+ end
24
+
25
+ attr_reader :config, :queue, :workers, :filters
26
+
27
+ def start
28
+ ensure_worker_count
29
+ end
30
+
31
+ def stop
32
+ stop_workers
33
+ end
34
+
35
+ def submit(resource)
36
+ queue.push(resource, true)
37
+
38
+ ensure_worker_count
39
+ rescue ThreadError
40
+ warn 'Queue is full (%i items), skipping…', config.api_buffer_size
41
+ nil
42
+ end
43
+
44
+ def add_filter(key, callback)
45
+ @filters.add(key, callback)
46
+ end
47
+
48
+ private
49
+
50
+ def ensure_worker_count
51
+ missing = config.pool_size - @workers.length
52
+ return unless missing > 0
53
+
54
+ info 'Booting %i workers', missing
55
+ missing.times { boot_worker }
56
+ end
57
+
58
+ # rubocop:disable Metrics/MethodLength
59
+ def boot_worker
60
+ worker = Worker.new(
61
+ config,
62
+ queue,
63
+ serializers: @serializers,
64
+ filters: @filters
65
+ )
66
+
67
+ @workers.push worker
68
+
69
+ @pool.post do
70
+ worker.work_forever
71
+ @workers.delete(worker)
72
+ end
73
+ end
74
+ # rubocop:enable Metrics/MethodLength
75
+
76
+ def stop_workers
77
+ return unless @pool.running?
78
+
79
+ debug 'Stopping workers'
80
+ send_stop_messages
81
+
82
+ debug 'Shutting down pool'
83
+ @pool.shutdown
84
+
85
+ return if @pool.wait_for_termination(5)
86
+
87
+ warn "Worker pool didn't close in 5 secs, killing ..."
88
+ @pool.kill
89
+ end
90
+
91
+ def send_stop_messages
92
+ @workers.each { queue.push(Worker::StopMessage.new, true) }
93
+ rescue ThreadError
94
+ warn 'Cannot push stop messages to worker queue as it is full'
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'http'
4
+ require 'concurrent'
5
+ require 'zlib'
6
+
7
+ require 'elastic_apm/metadata'
8
+
9
+ module ElasticAPM
10
+ module Transport
11
+ # @api private
12
+ class Connection # rubocop:disable Metrics/ClassLength
13
+ include Logging
14
+
15
+ class FailedToConnectError < InternalError; end
16
+
17
+ # @api private
18
+ # HTTP.rb calls #rewind the body stream which IO.pipes don't support
19
+ class ModdedIO < IO
20
+ def self.pipe(ext_enc = nil)
21
+ super(ext_enc).tap do |rw|
22
+ rw[0].define_singleton_method(:rewind) { nil }
23
+ end
24
+ end
25
+ end
26
+
27
+ HEADERS = {
28
+ 'Content-Type' => 'application/x-ndjson',
29
+ 'Transfer-Encoding' => 'chunked'
30
+ }.freeze
31
+ GZIP_HEADERS = HEADERS.merge('Content-Encoding' => 'gzip').freeze
32
+
33
+ def initialize(config)
34
+ @config = config
35
+
36
+ @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
+ @metadata = Metadata.build(config)
48
+
49
+ @mutex = Mutex.new
50
+ end
51
+
52
+ def write(str)
53
+ connect_unless_connected
54
+
55
+ @mutex.synchronize { append(str) }
56
+
57
+ return unless @bytes_sent >= @config.api_request_size
58
+
59
+ flush
60
+ rescue FailedToConnectError => e
61
+ error "Couldn't establish connection to APM Server:\n%p", e
62
+ flush
63
+
64
+ nil
65
+ end
66
+
67
+ def connected?
68
+ @mutex.synchronize { @connected }
69
+ end
70
+
71
+ def flush
72
+ @mutex.synchronize do
73
+ return unless @connected
74
+
75
+ debug 'Closing request'
76
+ @wr.close
77
+ @conn_thread.join 5 if @conn_thread
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ # rubocop:disable Metrics/MethodLength
84
+ def connect_unless_connected
85
+ @mutex.synchronize do
86
+ return true if @connected
87
+
88
+ debug 'Opening new request'
89
+
90
+ reset!
91
+
92
+ @rd, @wr = ModdedIO.pipe
93
+
94
+ enable_compression! if @config.http_compression?
95
+
96
+ perform_request_in_thread
97
+ wait_for_connection
98
+
99
+ schedule_closing if @config.api_request_time
100
+
101
+ append(@metadata)
102
+
103
+ true
104
+ end
105
+ end
106
+ # rubocop:enable Metrics/MethodLength
107
+
108
+ # rubocop:disable Metrics/MethodLength
109
+ def perform_request_in_thread
110
+ @conn_thread = Thread.new do
111
+ begin
112
+ @connected = true
113
+ resp = @client.post(@url, body: @rd).flush
114
+ rescue Exception => e
115
+ @connection_error = e
116
+ ensure
117
+ @connected = false
118
+ end
119
+
120
+ if resp&.status == 202
121
+ debug 'APM Server responded with status 202'
122
+ elsif resp
123
+ error "APM Server reponded with an error:\n%p", resp.body.to_s
124
+ end
125
+
126
+ resp
127
+ end
128
+ end
129
+ # rubocop:enable Metrics/MethodLength
130
+
131
+ def append(str)
132
+ bytes =
133
+ if @config.http_compression
134
+ @bytes_sent = @wr.tell
135
+ else
136
+ @bytes_sent += str.bytesize
137
+ end
138
+
139
+ debug 'Bytes sent during this request: %d', bytes
140
+
141
+ @wr.puts(str)
142
+ end
143
+
144
+ def schedule_closing
145
+ @close_task =
146
+ Concurrent::ScheduledTask.execute(@config.api_request_time) do
147
+ flush
148
+ end
149
+ end
150
+
151
+ def enable_compression!
152
+ @wr.binmode
153
+ @wr = Zlib::GzipWriter.new(@wr)
154
+ end
155
+
156
+ def reset!
157
+ @bytes_sent = 0
158
+ @connected = false
159
+ @connection_error = nil
160
+ @close_task = nil
161
+ end
162
+
163
+ def wait_for_connection
164
+ until @connected
165
+ if (exception = @connection_error)
166
+ @wr&.close
167
+ raise FailedToConnectError, exception
168
+ end
169
+
170
+ sleep 0.01
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end