elastic-apm 3.3.0 → 3.4.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/.jenkins_exclude.yml +4 -4
  3. data/.ci/.jenkins_ruby.yml +1 -1
  4. data/.ci/Jenkinsfile +5 -3
  5. data/.ci/jobs/apm-agent-ruby-downstream.yml +1 -0
  6. data/.ci/jobs/apm-agent-ruby-linting-mbp.yml +1 -0
  7. data/.ci/jobs/apm-agent-ruby-mbp.yml +1 -0
  8. data/.ci/prepare-git-context.sh +5 -2
  9. data/.github/ISSUE_TEMPLATE/Bug_report.md +38 -0
  10. data/.github/ISSUE_TEMPLATE/Feature_request.md +17 -0
  11. data/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  12. data/.gitignore +3 -0
  13. data/.rubocop.yml +6 -0
  14. data/CHANGELOG.asciidoc +25 -1
  15. data/Gemfile +6 -2
  16. data/bench/sql.rb +49 -0
  17. data/bin/build_docs +1 -1
  18. data/codecov.yml +32 -0
  19. data/docs/api.asciidoc +37 -0
  20. data/docs/configuration.asciidoc +18 -1
  21. data/docs/supported-technologies.asciidoc +20 -1
  22. data/lib/elastic_apm.rb +29 -5
  23. data/lib/elastic_apm/agent.rb +6 -2
  24. data/lib/elastic_apm/child_durations.rb +9 -4
  25. data/lib/elastic_apm/config.rb +8 -1
  26. data/lib/elastic_apm/config/options.rb +3 -4
  27. data/lib/elastic_apm/context/response.rb +10 -2
  28. data/lib/elastic_apm/instrumenter.rb +20 -11
  29. data/lib/elastic_apm/normalizers/rails/active_record.rb +12 -5
  30. data/lib/elastic_apm/rails.rb +1 -10
  31. data/lib/elastic_apm/railtie.rb +1 -1
  32. data/lib/elastic_apm/span.rb +3 -2
  33. data/lib/elastic_apm/span/context.rb +26 -44
  34. data/lib/elastic_apm/span/context/db.rb +19 -0
  35. data/lib/elastic_apm/span/context/destination.rb +44 -0
  36. data/lib/elastic_apm/span/context/http.rb +26 -0
  37. data/lib/elastic_apm/spies/elasticsearch.rb +18 -5
  38. data/lib/elastic_apm/spies/faraday.rb +36 -18
  39. data/lib/elastic_apm/spies/http.rb +16 -2
  40. data/lib/elastic_apm/spies/mongo.rb +5 -0
  41. data/lib/elastic_apm/spies/net_http.rb +27 -7
  42. data/lib/elastic_apm/spies/sequel.rb +25 -15
  43. data/lib/elastic_apm/spies/shoryuken.rb +48 -0
  44. data/lib/elastic_apm/spies/sneakers.rb +57 -0
  45. data/lib/elastic_apm/sql.rb +19 -0
  46. data/lib/elastic_apm/sql/signature.rb +152 -0
  47. data/lib/elastic_apm/sql/tokenizer.rb +247 -0
  48. data/lib/elastic_apm/sql/tokens.rb +46 -0
  49. data/lib/elastic_apm/sql_summarizer.rb +1 -2
  50. data/lib/elastic_apm/transaction.rb +11 -11
  51. data/lib/elastic_apm/transport/connection/proxy_pipe.rb +2 -2
  52. data/lib/elastic_apm/transport/headers.rb +4 -0
  53. data/lib/elastic_apm/transport/serializers/span_serializer.rb +24 -7
  54. data/lib/elastic_apm/version.rb +1 -1
  55. metadata +16 -3
  56. data/.github/workflows/main.yml +0 -14
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ module Spies
6
+ # @api private
7
+ class ShoryukenSpy
8
+ # @api private
9
+ class Middleware
10
+ def call(worker_instance, queue, sqs_msg, body)
11
+ transaction =
12
+ ElasticAPM.start_transaction(
13
+ job_class(worker_instance, body),
14
+ 'shoryuken.job'
15
+ )
16
+
17
+ ElasticAPM.set_label('shoryuken.id', sqs_msg.message_id)
18
+ ElasticAPM.set_label('shoryuken.queue', queue)
19
+
20
+ yield
21
+
22
+ transaction&.done :success
23
+ rescue ::Exception => e
24
+ ElasticAPM.report(e, handled: false)
25
+ transaction&.done :error
26
+ raise
27
+ ensure
28
+ ElasticAPM.end_transaction
29
+ end
30
+
31
+ private
32
+
33
+ def job_class(worker_instance, body)
34
+ klass = body['job_class'] if body.is_a?(Hash)
35
+ klass || worker_instance.class.name
36
+ end
37
+ end
38
+
39
+ def install
40
+ ::Shoryuken.server_middleware do |chain|
41
+ chain.add Middleware
42
+ end
43
+ end
44
+ end
45
+
46
+ register 'Shoryuken', 'shoryuken', ShoryukenSpy.new
47
+ end
48
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ module Spies
6
+ # @api private
7
+ class SneakersSpy
8
+ include Logging
9
+
10
+ def self.supported_version?
11
+ Gem.loaded_specs['sneakers'].version >= Gem::Version.create('2.12.0')
12
+ end
13
+
14
+ def install
15
+ unless SneakersSpy.supported_version?
16
+ warn(
17
+ 'Sneakers version is below 2.12.0. Sneakers spy installation failed'
18
+ )
19
+ return
20
+ end
21
+
22
+ Sneakers.middleware.use(Middleware, nil)
23
+ end
24
+
25
+ # @api private
26
+ class Middleware
27
+ def initialize(app, *args)
28
+ @app = app
29
+ @args = args
30
+ end
31
+
32
+ def call(deserialized_msg, delivery_info, metadata, handler)
33
+ transaction =
34
+ ElasticAPM.start_transaction(
35
+ delivery_info.consumer.queue.name,
36
+ 'Sneakers'
37
+ )
38
+
39
+ ElasticAPM.set_label(:routing_key, delivery_info.routing_key)
40
+
41
+ res = @app.call(deserialized_msg, delivery_info, metadata, handler)
42
+ transaction&.done(:success)
43
+
44
+ res
45
+ rescue ::Exception => e
46
+ ElasticAPM.report(e, handled: false)
47
+ transaction&.done(:error)
48
+ raise
49
+ ensure
50
+ ElasticAPM.end_transaction
51
+ end
52
+ end
53
+ end
54
+
55
+ register 'Sneakers', 'sneakers', SneakersSpy.new
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ module Sql
6
+ # This method is only here as a shortcut while the agent ships with
7
+ # both implementations ~mikker
8
+ def self.summarizer
9
+ @summarizer ||=
10
+ if ElasticAPM.agent&.config&.use_experimental_sql_parser
11
+ require 'elastic_apm/sql/signature'
12
+ Sql::Signature::Summarizer.new
13
+ else
14
+ require 'elastic_apm/sql_summarizer'
15
+ SqlSummarizer.new
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elastic_apm/sql/tokenizer'
4
+
5
+ module ElasticAPM
6
+ module Sql
7
+ # @api private
8
+ class Signature
9
+ include Tokens
10
+
11
+ # Mostly here to provide a similar API to new SqlSummarizer for easier
12
+ # swapping out
13
+ #
14
+ # @api private
15
+ class Summarizer
16
+ def summarize(sql)
17
+ Signature.parse(sql)
18
+ end
19
+ end
20
+
21
+ def initialize(sql)
22
+ @sql = sql
23
+ @tokenizer = Tokenizer.new(sql)
24
+ end
25
+
26
+ def parse
27
+ @tokenizer.scan # until tokenizer.token != COMMENT
28
+
29
+ parsed = parse_tokens
30
+ return parsed if parsed
31
+
32
+ # If all else fails, just return the first token of the query.
33
+ parts = @sql.split
34
+ return '' unless parts.any?
35
+
36
+ parts.first.upcase
37
+ end
38
+
39
+ def self.parse(sql)
40
+ new(sql).parse
41
+ end
42
+
43
+ private
44
+
45
+ # rubocop:disable Metrics/CyclomaticComplexity
46
+ # rubocop:disable Metrics/PerceivedComplexity
47
+ def parse_tokens
48
+ t = @tokenizer
49
+
50
+ case t.token
51
+
52
+ when CALL
53
+ return unless scan_until IDENT
54
+ "CALL #{t.text}"
55
+
56
+ when DELETE
57
+ return unless scan_until FROM
58
+ return unless scan_token IDENT
59
+ table = scan_dotted_identifier
60
+ "DELETE FROM #{table}"
61
+
62
+ when INSERT, REPLACE
63
+ action = t.text
64
+ return unless scan_until INTO
65
+ return unless scan_token IDENT
66
+ table = scan_dotted_identifier
67
+ "#{action} INTO #{table}"
68
+
69
+ when SELECT
70
+ level = 0
71
+ while t.scan
72
+ case t.token
73
+ when LPAREN then level += 1
74
+ when RPAREN then level -= 1
75
+ when FROM
76
+ next unless level == 0
77
+ break unless scan_token IDENT
78
+ table = scan_dotted_identifier
79
+ return "SELECT FROM #{table}"
80
+ end
81
+ end
82
+
83
+ when UPDATE
84
+ # Scan for the table name. Some dialects allow option keywords before
85
+ # the table name.
86
+ return 'UPDATE' unless scan_token IDENT
87
+
88
+ table = t.text
89
+
90
+ period = false
91
+ first_period = false
92
+
93
+ while t.scan
94
+ case t.token
95
+ when IDENT
96
+ if period
97
+ table += t.text
98
+ period = false
99
+ end
100
+
101
+ unless first_period
102
+ table = t.text
103
+ end
104
+
105
+ # Two adjacent identifiers found after the first period. Ignore
106
+ # the secondary ones, in case they are unknown keywords.
107
+ when PERIOD
108
+ period = true
109
+ first_period = true
110
+ table += '.'
111
+ else
112
+ return "UPDATE #{table}"
113
+ end
114
+ end
115
+ end
116
+ end
117
+ # rubocop:enable Metrics/CyclomaticComplexity
118
+ # rubocop:enable Metrics/PerceivedComplexity
119
+
120
+ # Scans until finding token of `kind`
121
+ def scan_until(kind)
122
+ while @tokenizer.scan
123
+ break true if @tokenizer.token == kind
124
+ false
125
+ end
126
+ end
127
+
128
+ # Scans next token, ignoring comments
129
+ # Returns whether next token is of `kind`
130
+ def scan_token(kind)
131
+ while @tokenizer.scan
132
+ next if @tokenizer.token == COMMENT
133
+ break
134
+ end
135
+
136
+ return true if @tokenizer.token == kind
137
+
138
+ false
139
+ end
140
+
141
+ def scan_dotted_identifier
142
+ table = @tokenizer.text
143
+
144
+ while scan_token(PERIOD) && scan_token(IDENT)
145
+ table += ".#{@tokenizer.text}"
146
+ end
147
+
148
+ table
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+ require 'elastic_apm/sql/tokens'
5
+
6
+ module ElasticAPM
7
+ module Sql
8
+ # @api private
9
+ class Tokenizer
10
+ include Tokens
11
+
12
+ ALPHA = /[[:alpha:]]/.freeze
13
+ DIGIT = /[[:digit:]]/.freeze
14
+ SPACE = /[[:space:]]+/.freeze
15
+
16
+ def initialize(input)
17
+ @input = input
18
+
19
+ @scanner = StringScanner.new(input)
20
+ @byte_start = 0
21
+ end
22
+
23
+ attr_reader :input, :scanner, :token
24
+
25
+ def text
26
+ @input.byteslice(@byte_start, @byte_end - @byte_start)
27
+ end
28
+
29
+ def scan
30
+ scanner.skip(SPACE)
31
+
32
+ @byte_start = scanner.pos
33
+ char = next_char
34
+
35
+ return false unless char
36
+
37
+ @token = next_token(char)
38
+
39
+ true
40
+ end
41
+
42
+ private
43
+
44
+ # rubocop:disable Metrics/CyclomaticComplexity
45
+ def next_token(char)
46
+ case char
47
+ when '_' then scan_keyword_or_identifier(possible_keyword: false)
48
+ when '.' then PERIOD
49
+ when '$' then scan_dollar_sign
50
+ when '`' then scan_quoted_indentifier('`')
51
+ when '"' then scan_quoted_indentifier('"')
52
+ when '[' then scan_quoted_indentifier(']')
53
+ when '(' then LPAREN
54
+ when ')' then RPAREN
55
+ when '/' then scan_bracketed_comment
56
+ when '-' then scan_simple_comment
57
+ when "'" then scan_string_literal
58
+ when ALPHA then scan_keyword_or_identifier(possible_keyword: true)
59
+ when DIGIT then scan_numeric_literal
60
+ else OTHER
61
+ end
62
+ end
63
+ # rubocop:enable Metrics/CyclomaticComplexity
64
+
65
+ def next_char
66
+ char = @scanner.getch
67
+ @byte_end = @scanner.pos
68
+ char
69
+ end
70
+
71
+ # StringScanner#peek returns next byte which could be an incomplete utf
72
+ # multi-byte character
73
+ def peek_char(length = 1)
74
+ # The maximum byte count of utf chars is 4:
75
+ # > In UTF-8, characters from the U+0000..U+10FFFF range (the UTF-16
76
+ # accessible range) are encoded using sequences of 1 to 4 octets.
77
+ # # https://tools.ietf.org/html/rfc3629
78
+ return nil if length > 4
79
+
80
+ char = @scanner.peek(length)
81
+
82
+ return nil if char.empty?
83
+ return char if char.valid_encoding?
84
+
85
+ peek_char(length + 1)
86
+ end
87
+
88
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
89
+ def scan_keyword_or_identifier(possible_keyword:)
90
+ while (peek = peek_char)
91
+ if peek == '_' || peek == '$' || peek =~ DIGIT
92
+ possible_keyword = false
93
+ next next_char
94
+ end
95
+
96
+ next next_char if peek =~ ALPHA
97
+
98
+ break
99
+ end
100
+
101
+ return IDENT unless possible_keyword
102
+
103
+ snap = text
104
+
105
+ if snap.length < KEYWORD_MIN_LENGTH || snap.length > KEYWORD_MAX_LENGTH
106
+ return IDENT
107
+ end
108
+
109
+ keyword = KEYWORDS[snap.length].find { |kw| snap.upcase == kw.to_s }
110
+ return keyword if keyword
111
+
112
+ IDENT
113
+ end
114
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
115
+
116
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
117
+ def scan_dollar_sign
118
+ while (peek = peek_char)
119
+ case peek
120
+ when DIGIT
121
+ next_char while peek_char =~ DIGIT
122
+ when '$', '_', ALPHA, SPACE
123
+ # PostgreSQL supports dollar-quoted string literal syntax,
124
+ # like $foo$...$foo$. The tag (foo in this case) is optional,
125
+ # and if present follows identifier rules.
126
+ while (char = next_char)
127
+ case char
128
+ when '$'
129
+ # This marks the end of the initial $foo$.
130
+ snap = text
131
+ slice = input.slice(scanner.pos, input.length)
132
+ index = slice.index(snap)
133
+ next unless index && index >= 0
134
+
135
+ delta = index + snap.bytesize
136
+ @byte_end += delta
137
+ scanner.pos += delta
138
+ return STRING
139
+ when SPACE
140
+ # Unknown token starting with $, consume chars until space.
141
+ @byte_end -= char.bytesize
142
+ return OTHER
143
+ end
144
+ end
145
+ else break
146
+ end
147
+ end
148
+
149
+ OTHER
150
+ end
151
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
152
+
153
+ def scan_quoted_indentifier(delimiter)
154
+ while (char = next_char)
155
+ next unless char == delimiter
156
+
157
+ if delimiter == '"' && peek_char == delimiter
158
+ next next_char
159
+ end
160
+
161
+ break
162
+ end
163
+
164
+ # Remove quotes from identifier
165
+ @byte_start += char.bytesize
166
+ @byte_end -= char.bytesize
167
+
168
+ IDENT
169
+ end
170
+
171
+ # rubocop:disable Metrics/CyclomaticComplexity
172
+ def scan_bracketed_comment
173
+ return OTHER unless peek_char == '*'
174
+
175
+ nesting = 1
176
+
177
+ while (char = next_char)
178
+ case char
179
+ when '/'
180
+ next unless peek_char == '*'
181
+ next_char
182
+ nesting += 1
183
+ when '*'
184
+ next unless peek_char == '/'
185
+ next_char
186
+ nesting -= 1
187
+ return COMMENT if nesting == 0
188
+ end
189
+ end
190
+ end
191
+ # rubocop:enable Metrics/CyclomaticComplexity
192
+
193
+ def scan_simple_comment
194
+ return OTHER unless peek_char == '-'
195
+
196
+ while (char = next_char)
197
+ break if char == "\n"
198
+ end
199
+
200
+ COMMENT
201
+ end
202
+
203
+ def scan_string_literal
204
+ delimiter = "'"
205
+
206
+ while (char = next_char)
207
+ if char == '\\'
208
+ # Skip escaped character, e.g. 'what\'s up?'
209
+ next_char
210
+ next
211
+ end
212
+
213
+ next unless char == delimiter
214
+
215
+ return STRING unless peek_char
216
+ return STRING if peek_char != delimiter
217
+
218
+ next_char
219
+ end
220
+ end
221
+
222
+ # rubocop:disable Metrics/CyclomaticComplexity
223
+ def scan_numeric_literal
224
+ period = false
225
+ exponent = false
226
+
227
+ while (peek = peek_char)
228
+ case peek
229
+ when DIGIT then next_char
230
+ when '.'
231
+ return NUMBER if period
232
+ next_char
233
+ period = true
234
+ when 'e', 'E'
235
+ return NUMBER if exponent
236
+ next_char
237
+ next_char if peek_char =~ /[+-]/
238
+ else break
239
+ end
240
+ end
241
+
242
+ NUMBER
243
+ end
244
+ # rubocop:enable Metrics/CyclomaticComplexity
245
+ end
246
+ end
247
+ end