elastic-apm 3.7.0 → 3.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ci/Jenkinsfile +139 -96
- data/.ci/packer_cache.sh +12 -10
- data/.rspec +0 -1
- data/CHANGELOG.asciidoc +80 -0
- data/Gemfile +4 -3
- data/bin/run-tests +4 -1
- data/docker-compose.yml +2 -0
- data/docs/configuration.asciidoc +38 -0
- data/docs/debugging.asciidoc +14 -0
- data/docs/supported-technologies.asciidoc +2 -1
- data/lib/elastic_apm/config.rb +36 -13
- data/lib/elastic_apm/config/options.rb +2 -1
- data/lib/elastic_apm/config/round_float.rb +31 -0
- data/lib/elastic_apm/config/wildcard_pattern_list.rb +13 -1
- data/lib/elastic_apm/context_builder.rb +1 -1
- data/lib/elastic_apm/grpc.rb +2 -2
- data/lib/elastic_apm/instrumenter.rb +10 -3
- data/lib/elastic_apm/metadata.rb +3 -1
- data/lib/elastic_apm/metadata/cloud_info.rb +128 -0
- data/lib/elastic_apm/metadata/service_info.rb +5 -2
- data/lib/elastic_apm/metadata/system_info.rb +5 -3
- data/lib/elastic_apm/metadata/system_info/container_info.rb +28 -4
- data/lib/elastic_apm/middleware.rb +8 -2
- data/lib/elastic_apm/opentracing.rb +47 -23
- data/lib/elastic_apm/span.rb +7 -3
- data/lib/elastic_apm/spies.rb +16 -14
- data/lib/elastic_apm/spies/delayed_job.rb +4 -2
- data/lib/elastic_apm/spies/dynamo_db.rb +58 -0
- data/lib/elastic_apm/spies/elasticsearch.rb +26 -2
- data/lib/elastic_apm/spies/mongo.rb +1 -1
- data/lib/elastic_apm/spies/net_http.rb +6 -2
- data/lib/elastic_apm/spies/sequel.rb +1 -1
- data/lib/elastic_apm/trace_context.rb +1 -1
- data/lib/elastic_apm/trace_context/traceparent.rb +2 -4
- data/lib/elastic_apm/trace_context/tracestate.rb +112 -9
- data/lib/elastic_apm/transaction.rb +26 -5
- data/lib/elastic_apm/transport/connection.rb +1 -0
- data/lib/elastic_apm/transport/filters/hash_sanitizer.rb +70 -0
- data/lib/elastic_apm/transport/filters/secrets_filter.rb +14 -56
- data/lib/elastic_apm/transport/serializers.rb +8 -6
- data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +56 -23
- data/lib/elastic_apm/transport/serializers/span_serializer.rb +2 -1
- data/lib/elastic_apm/transport/serializers/transaction_serializer.rb +1 -0
- data/lib/elastic_apm/transport/user_agent.rb +3 -3
- data/lib/elastic_apm/transport/worker.rb +5 -0
- data/lib/elastic_apm/util.rb +2 -0
- data/lib/elastic_apm/util/precision_validator.rb +46 -0
- data/lib/elastic_apm/version.rb +1 -1
- metadata +12 -8
- data/.ci/downstreamTests.groovy +0 -192
@@ -26,16 +26,40 @@ module ElasticAPM
|
|
26
26
|
TYPE = 'db'
|
27
27
|
SUBTYPE = 'elasticsearch'
|
28
28
|
|
29
|
+
def self.sanitizer
|
30
|
+
@sanitizer ||=
|
31
|
+
begin
|
32
|
+
config = ElasticAPM.agent.config
|
33
|
+
ElasticAPM::Transport::Filters::HashSanitizer.new(
|
34
|
+
key_patterns: config.custom_key_filters + config.sanitize_field_names
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
29
39
|
def install
|
30
40
|
::Elasticsearch::Transport::Client.class_eval do
|
31
41
|
alias perform_request_without_apm perform_request
|
32
42
|
|
33
43
|
def perform_request(method, path, *args, &block)
|
44
|
+
unless ElasticAPM.current_transaction
|
45
|
+
return perform_request_without_apm(method, path, *args, &block)
|
46
|
+
end
|
47
|
+
|
34
48
|
name = format(NAME_FORMAT, method, path)
|
35
|
-
statement =
|
49
|
+
statement = []
|
50
|
+
|
51
|
+
statement << { params: args&.[](0) }
|
52
|
+
|
53
|
+
if ElasticAPM.agent.config.capture_elasticsearch_queries
|
54
|
+
unless args[1].nil? || args[1].empty?
|
55
|
+
statement << {
|
56
|
+
body: ElasticAPM::Spies::ElasticsearchSpy.sanitizer.strip_from!(args[1])
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
36
60
|
|
37
61
|
context = Span::Context.new(
|
38
|
-
db: { statement: statement },
|
62
|
+
db: { statement: statement.reduce({}, :merge).to_json },
|
39
63
|
destination: {
|
40
64
|
name: SUBTYPE,
|
41
65
|
resource: SUBTYPE,
|
@@ -64,8 +64,12 @@ module ElasticAPM
|
|
64
64
|
method = req.method.to_s.upcase
|
65
65
|
path, query = req.path.split('?')
|
66
66
|
|
67
|
-
|
68
|
-
|
67
|
+
url = use_ssl? ? +'https://' : +'http://'
|
68
|
+
url << host
|
69
|
+
url << ":#{port}" if port
|
70
|
+
url << path
|
71
|
+
url << "?#{query}" if query
|
72
|
+
uri = URI(url)
|
69
73
|
|
70
74
|
destination =
|
71
75
|
ElasticAPM::Span::Context::Destination.from_uri(uri)
|
@@ -33,8 +33,7 @@ module ElasticAPM
|
|
33
33
|
trace_id: nil,
|
34
34
|
span_id: nil,
|
35
35
|
id: nil,
|
36
|
-
recorded: true
|
37
|
-
tracestate: nil
|
36
|
+
recorded: true
|
38
37
|
)
|
39
38
|
@version = version
|
40
39
|
@trace_id = trace_id || hex(TRACE_ID_LENGTH)
|
@@ -42,11 +41,10 @@ module ElasticAPM
|
|
42
41
|
@parent_id = span_id
|
43
42
|
@id = id || hex(ID_LENGTH)
|
44
43
|
@recorded = recorded
|
45
|
-
@tracestate = tracestate
|
46
44
|
end
|
47
45
|
# rubocop:enable Metrics/ParameterLists
|
48
46
|
|
49
|
-
attr_accessor :version, :id, :trace_id, :parent_id, :recorded
|
47
|
+
attr_accessor :version, :id, :trace_id, :parent_id, :recorded
|
50
48
|
|
51
49
|
alias :recorded? :recorded
|
52
50
|
|
@@ -17,26 +17,129 @@
|
|
17
17
|
|
18
18
|
# frozen_string_literal: true
|
19
19
|
|
20
|
+
require 'elastic_apm/util/precision_validator'
|
21
|
+
|
20
22
|
module ElasticAPM
|
21
23
|
class TraceContext
|
22
24
|
# @api private
|
23
25
|
class Tracestate
|
24
|
-
|
25
|
-
|
26
|
+
# @api private
|
27
|
+
class Entry
|
28
|
+
def initialize(key, value)
|
29
|
+
@key = key
|
30
|
+
@value = value
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :key, :value
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"#{key}=#{value}"
|
37
|
+
end
|
26
38
|
end
|
27
39
|
|
28
|
-
|
40
|
+
class EsEntry
|
41
|
+
ASSIGN = ':'
|
42
|
+
SPLIT = ';'
|
43
|
+
|
44
|
+
SHORT_TO_LONG = { 's' => 'sample_rate' }
|
45
|
+
LONG_TO_SHORT = { 'sample_rate' => 's' }
|
46
|
+
|
47
|
+
def initialize(values = nil)
|
48
|
+
parse(values)
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :sample_rate
|
52
|
+
|
53
|
+
def key
|
54
|
+
'es'
|
55
|
+
end
|
56
|
+
|
57
|
+
def value
|
58
|
+
LONG_TO_SHORT.map do |l, s|
|
59
|
+
"#{s}#{ASSIGN}#{send(l)}"
|
60
|
+
end.join(SPLIT)
|
61
|
+
end
|
62
|
+
|
63
|
+
def empty?
|
64
|
+
!sample_rate
|
65
|
+
end
|
66
|
+
|
67
|
+
def sample_rate=(val)
|
68
|
+
@sample_rate = Util::PrecisionValidator.validate(
|
69
|
+
val, precision: 4, minimum: 0.0001
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s
|
74
|
+
return nil if empty?
|
75
|
+
|
76
|
+
"es=#{value}"
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def parse(values)
|
82
|
+
return unless values
|
83
|
+
|
84
|
+
values.split(SPLIT).map do |kv|
|
85
|
+
k, v = kv.split(ASSIGN)
|
86
|
+
next unless SHORT_TO_LONG.keys.include?(k)
|
87
|
+
send("#{SHORT_TO_LONG[k]}=", v)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
extend Forwardable
|
93
|
+
|
94
|
+
def initialize(entries: {}, sample_rate: nil)
|
95
|
+
@entries = entries
|
96
|
+
|
97
|
+
self.sample_rate = sample_rate if sample_rate
|
98
|
+
end
|
99
|
+
|
100
|
+
attr_accessor :entries
|
101
|
+
|
102
|
+
def_delegators :es_entry, :sample_rate, :sample_rate=
|
29
103
|
|
30
104
|
def self.parse(header)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
105
|
+
entries =
|
106
|
+
split_by_nl_and_comma(header)
|
107
|
+
.each_with_object({}) do |entry, hsh|
|
108
|
+
k, v = entry.split('=')
|
109
|
+
|
110
|
+
hsh[k] =
|
111
|
+
case k
|
112
|
+
when 'es' then EsEntry.new(v)
|
113
|
+
else Entry.new(k, v)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
new(entries: entries)
|
36
118
|
end
|
37
119
|
|
38
120
|
def to_header
|
39
|
-
|
121
|
+
return "" unless entries.any?
|
122
|
+
|
123
|
+
entries.values.map(&:to_s).join(',')
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def es_entry
|
129
|
+
# lazy generate this so we only add it if necessary
|
130
|
+
entries['es'] ||= EsEntry.new
|
131
|
+
end
|
132
|
+
|
133
|
+
class << self
|
134
|
+
private
|
135
|
+
|
136
|
+
def split_by_nl_and_comma(str)
|
137
|
+
# HTTP allows multiple headers with the same name, eg. multiple
|
138
|
+
# Set-Cookie headers per response.
|
139
|
+
# Rack handles this by joining the headers under the same key, separated
|
140
|
+
# by newlines, see https://www.rubydoc.info/github/rack/rack/file/SPEC
|
141
|
+
String(str).split("\n").map { |s| s.split(',') }.flatten
|
142
|
+
end
|
40
143
|
end
|
41
144
|
end
|
42
145
|
end
|
@@ -34,6 +34,7 @@ module ElasticAPM
|
|
34
34
|
name = nil,
|
35
35
|
type = nil,
|
36
36
|
sampled: true,
|
37
|
+
sample_rate: 1,
|
37
38
|
context: nil,
|
38
39
|
config:,
|
39
40
|
trace_context: nil
|
@@ -52,13 +53,19 @@ module ElasticAPM
|
|
52
53
|
@default_labels = config.default_labels
|
53
54
|
|
54
55
|
@sampled = sampled
|
56
|
+
@sample_rate = sample_rate
|
55
57
|
|
56
58
|
@context = context || Context.new # TODO: Lazy generate this?
|
57
59
|
if @default_labels
|
58
60
|
Util.reverse_merge!(@context.labels, @default_labels)
|
59
61
|
end
|
60
62
|
|
61
|
-
@trace_context = trace_context
|
63
|
+
unless (@trace_context = trace_context)
|
64
|
+
@trace_context = TraceContext.new(
|
65
|
+
traceparent: TraceContext::Traceparent.new(recorded: sampled),
|
66
|
+
tracestate: TraceContext::Tracestate.new(sample_rate: sampled ? sample_rate : 0)
|
67
|
+
)
|
68
|
+
end
|
62
69
|
|
63
70
|
@started_spans = 0
|
64
71
|
@dropped_spans = 0
|
@@ -69,10 +76,24 @@ module ElasticAPM
|
|
69
76
|
|
70
77
|
attr_accessor :name, :type, :result
|
71
78
|
|
72
|
-
attr_reader
|
73
|
-
:
|
74
|
-
:
|
75
|
-
:
|
79
|
+
attr_reader(
|
80
|
+
:breakdown_metrics,
|
81
|
+
:collect_metrics,
|
82
|
+
:context,
|
83
|
+
:dropped_spans,
|
84
|
+
:duration,
|
85
|
+
:framework_name,
|
86
|
+
:notifications,
|
87
|
+
:self_time,
|
88
|
+
:sample_rate,
|
89
|
+
:span_frames_min_duration,
|
90
|
+
:started_spans,
|
91
|
+
:timestamp,
|
92
|
+
:trace_context,
|
93
|
+
:transaction_max_spans
|
94
|
+
)
|
95
|
+
|
96
|
+
alias :collect_metrics? :collect_metrics
|
76
97
|
|
77
98
|
def sampled?
|
78
99
|
@sampled
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# Licensed to Elasticsearch B.V. under one or more contributor
|
2
|
+
# license agreements. See the NOTICE file distributed with
|
3
|
+
# this work for additional information regarding copyright
|
4
|
+
# ownership. Elasticsearch B.V. licenses this file to you under
|
5
|
+
# the Apache License, Version 2.0 (the "License"); you may
|
6
|
+
# not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
|
18
|
+
# frozen_string_literal: true
|
19
|
+
|
20
|
+
module ElasticAPM
|
21
|
+
module Transport
|
22
|
+
module Filters
|
23
|
+
class HashSanitizer
|
24
|
+
FILTERED = '[FILTERED]'
|
25
|
+
|
26
|
+
# DEPRECATED: Remove these additions in next major version
|
27
|
+
LEGACY_KEY_FILTERS = [/cookie/i, /auth/i].freeze
|
28
|
+
|
29
|
+
# DEPRECATED: Remove this check in next major version
|
30
|
+
VALUE_FILTERS = [
|
31
|
+
# (probably) credit card number
|
32
|
+
/^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/
|
33
|
+
].freeze
|
34
|
+
|
35
|
+
def initialize(key_patterns:)
|
36
|
+
@key_patterns = key_patterns + LEGACY_KEY_FILTERS
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_accessor :key_patterns
|
40
|
+
|
41
|
+
def strip_from!(obj)
|
42
|
+
return unless obj&.is_a?(Hash)
|
43
|
+
|
44
|
+
obj.each do |k, v|
|
45
|
+
if filter_key?(k)
|
46
|
+
next obj[k] = FILTERED
|
47
|
+
end
|
48
|
+
|
49
|
+
case v
|
50
|
+
when Hash
|
51
|
+
strip_from!(v)
|
52
|
+
when String
|
53
|
+
if filter_value?(v)
|
54
|
+
obj[k] = FILTERED
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def filter_key?(key)
|
61
|
+
@key_patterns.any? { |regex| regex.match(key) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def filter_value?(value)
|
65
|
+
VALUE_FILTERS.any? { |regex| regex.match(value) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -17,75 +17,33 @@
|
|
17
17
|
|
18
18
|
# frozen_string_literal: true
|
19
19
|
|
20
|
+
require 'elastic_apm/transport/filters/hash_sanitizer'
|
21
|
+
|
20
22
|
module ElasticAPM
|
21
23
|
module Transport
|
22
24
|
module Filters
|
23
25
|
# @api private
|
24
26
|
class SecretsFilter
|
25
|
-
FILTERED = '[FILTERED]'
|
26
|
-
|
27
|
-
KEY_FILTERS = [
|
28
|
-
/passw(or)?d/i,
|
29
|
-
/auth/i,
|
30
|
-
/^pw$/,
|
31
|
-
/secret/i,
|
32
|
-
/token/i,
|
33
|
-
/api[-._]?key/i,
|
34
|
-
/session[-._]?id/i,
|
35
|
-
/(set[-_])?cookie/i
|
36
|
-
].freeze
|
37
|
-
|
38
|
-
VALUE_FILTERS = [
|
39
|
-
# (probably) credit card number
|
40
|
-
/^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/
|
41
|
-
].freeze
|
42
|
-
|
43
27
|
def initialize(config)
|
44
28
|
@config = config
|
45
|
-
@
|
46
|
-
|
47
|
-
|
48
|
-
|
29
|
+
@sanitizer =
|
30
|
+
HashSanitizer.new(
|
31
|
+
key_patterns: config.custom_key_filters + config.sanitize_field_names
|
32
|
+
)
|
49
33
|
end
|
50
34
|
|
51
35
|
def call(payload)
|
52
|
-
strip_from! payload.dig(:transaction, :context, :request, :
|
53
|
-
strip_from! payload.dig(:transaction, :context, :request, :
|
54
|
-
strip_from! payload.dig(:transaction, :context, :request, :
|
55
|
-
strip_from! payload.dig(:transaction, :context, :
|
56
|
-
strip_from! payload.dig(:
|
57
|
-
strip_from! payload.dig(:error, :context, :
|
58
|
-
strip_from! payload.dig(:
|
36
|
+
@sanitizer.strip_from! payload.dig(:transaction, :context, :request, :body)
|
37
|
+
@sanitizer.strip_from! payload.dig(:transaction, :context, :request, :cookies)
|
38
|
+
@sanitizer.strip_from! payload.dig(:transaction, :context, :request, :env)
|
39
|
+
@sanitizer.strip_from! payload.dig(:transaction, :context, :request, :headers)
|
40
|
+
@sanitizer.strip_from! payload.dig(:transaction, :context, :response, :headers)
|
41
|
+
@sanitizer.strip_from! payload.dig(:error, :context, :request, :cookies)
|
42
|
+
@sanitizer.strip_from! payload.dig(:error, :context, :request, :headers)
|
43
|
+
@sanitizer.strip_from! payload.dig(:error, :context, :response, :headers)
|
59
44
|
|
60
45
|
payload
|
61
46
|
end
|
62
|
-
|
63
|
-
def strip_from!(obj)
|
64
|
-
return unless obj&.is_a?(Hash)
|
65
|
-
|
66
|
-
obj.each do |k, v|
|
67
|
-
if filter_key?(k)
|
68
|
-
next obj[k] = FILTERED
|
69
|
-
end
|
70
|
-
|
71
|
-
case v
|
72
|
-
when Hash
|
73
|
-
strip_from!(v)
|
74
|
-
when String
|
75
|
-
if filter_value?(v)
|
76
|
-
obj[k] = FILTERED
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def filter_key?(key)
|
83
|
-
@key_filters.any? { |regex| regex.match(key) }
|
84
|
-
end
|
85
|
-
|
86
|
-
def filter_value?(value)
|
87
|
-
VALUE_FILTERS.any? { |regex| regex.match(value) }
|
88
|
-
end
|
89
47
|
end
|
90
48
|
end
|
91
49
|
end
|