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.
- checksums.yaml +4 -4
- data/.ci/.jenkins_exclude.yml +4 -4
- data/.ci/.jenkins_ruby.yml +1 -1
- data/.ci/Jenkinsfile +5 -3
- data/.ci/jobs/apm-agent-ruby-downstream.yml +1 -0
- data/.ci/jobs/apm-agent-ruby-linting-mbp.yml +1 -0
- data/.ci/jobs/apm-agent-ruby-mbp.yml +1 -0
- data/.ci/prepare-git-context.sh +5 -2
- data/.github/ISSUE_TEMPLATE/Bug_report.md +38 -0
- data/.github/ISSUE_TEMPLATE/Feature_request.md +17 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +6 -0
- data/CHANGELOG.asciidoc +25 -1
- data/Gemfile +6 -2
- data/bench/sql.rb +49 -0
- data/bin/build_docs +1 -1
- data/codecov.yml +32 -0
- data/docs/api.asciidoc +37 -0
- data/docs/configuration.asciidoc +18 -1
- data/docs/supported-technologies.asciidoc +20 -1
- data/lib/elastic_apm.rb +29 -5
- data/lib/elastic_apm/agent.rb +6 -2
- data/lib/elastic_apm/child_durations.rb +9 -4
- data/lib/elastic_apm/config.rb +8 -1
- data/lib/elastic_apm/config/options.rb +3 -4
- data/lib/elastic_apm/context/response.rb +10 -2
- data/lib/elastic_apm/instrumenter.rb +20 -11
- data/lib/elastic_apm/normalizers/rails/active_record.rb +12 -5
- data/lib/elastic_apm/rails.rb +1 -10
- data/lib/elastic_apm/railtie.rb +1 -1
- data/lib/elastic_apm/span.rb +3 -2
- data/lib/elastic_apm/span/context.rb +26 -44
- data/lib/elastic_apm/span/context/db.rb +19 -0
- data/lib/elastic_apm/span/context/destination.rb +44 -0
- data/lib/elastic_apm/span/context/http.rb +26 -0
- data/lib/elastic_apm/spies/elasticsearch.rb +18 -5
- data/lib/elastic_apm/spies/faraday.rb +36 -18
- data/lib/elastic_apm/spies/http.rb +16 -2
- data/lib/elastic_apm/spies/mongo.rb +5 -0
- data/lib/elastic_apm/spies/net_http.rb +27 -7
- data/lib/elastic_apm/spies/sequel.rb +25 -15
- data/lib/elastic_apm/spies/shoryuken.rb +48 -0
- data/lib/elastic_apm/spies/sneakers.rb +57 -0
- data/lib/elastic_apm/sql.rb +19 -0
- data/lib/elastic_apm/sql/signature.rb +152 -0
- data/lib/elastic_apm/sql/tokenizer.rb +247 -0
- data/lib/elastic_apm/sql/tokens.rb +46 -0
- data/lib/elastic_apm/sql_summarizer.rb +1 -2
- data/lib/elastic_apm/transaction.rb +11 -11
- data/lib/elastic_apm/transport/connection/proxy_pipe.rb +2 -2
- data/lib/elastic_apm/transport/headers.rb +4 -0
- data/lib/elastic_apm/transport/serializers/span_serializer.rb +24 -7
- data/lib/elastic_apm/version.rb +1 -1
- metadata +16 -3
- 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
|