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

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