elastic-apm 3.3.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
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