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
@@ -4,53 +4,35 @@ module ElasticAPM
4
4
  class Span
5
5
  # @api private
6
6
  class Context
7
- def initialize(db: nil, http: nil, labels: {})
8
- @sync = true
9
- @db = db && Db.new(db)
10
- @http = http && Http.new(http)
7
+ def initialize(
8
+ db: nil,
9
+ destination: nil,
10
+ http: nil,
11
+ labels: {},
12
+ sync: nil
13
+ )
14
+ @sync = sync
15
+ @db = db && Db.new(**db)
16
+ @http = http && Http.new(**http)
17
+ @destination =
18
+ case destination
19
+ when Destination then destination
20
+ when Hash then Destination.new(**destination)
21
+ end
11
22
  @labels = labels
12
23
  end
13
24
 
14
- attr_accessor :sync, :db, :http, :labels
15
-
16
- # @api private
17
- class Db
18
- def initialize(instance: nil, statement: nil, type: nil, user: nil)
19
- @instance = instance
20
- @statement = statement
21
- @type = type
22
- @user = user
23
- end
24
-
25
- attr_accessor :instance, :statement, :type, :user
26
- end
27
-
28
- # @api private
29
- class Http
30
- def initialize(url: nil, status_code: nil, method: nil)
31
- @url = sanitize_url(url)
32
- @status_code = status_code
33
- @method = method
34
- end
35
-
36
- attr_accessor :url, :status_code, :method
37
-
38
- private
39
-
40
- def sanitize_url(url)
41
- uri = URI(url)
42
-
43
- return url unless uri.userinfo
44
-
45
- format(
46
- '%s://%s@%s%s',
47
- uri.scheme,
48
- uri.user,
49
- uri.hostname,
50
- uri.path
51
- )
52
- end
53
- end
25
+ attr_reader(
26
+ :db,
27
+ :destination,
28
+ :http,
29
+ :labels,
30
+ :sync
31
+ )
54
32
  end
55
33
  end
56
34
  end
35
+
36
+ require 'elastic_apm/span/context/db'
37
+ require 'elastic_apm/span/context/http'
38
+ require 'elastic_apm/span/context/destination'
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Span
5
+ class Context
6
+ # @api private
7
+ class Db
8
+ def initialize(instance: nil, statement: nil, type: nil, user: nil)
9
+ @instance = instance
10
+ @statement = statement
11
+ @type = type
12
+ @user = user
13
+ end
14
+
15
+ attr_accessor :instance, :statement, :type, :user
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Span
5
+ class Context
6
+ # @api private
7
+ class Destination
8
+ def initialize(name: nil, resource: nil, type: nil)
9
+ @name = name
10
+ @resource = resource
11
+ @type = type
12
+ end
13
+
14
+ attr_reader :name, :resource, :type
15
+
16
+ def self.from_uri(uri_or_str, type: 'external')
17
+ uri = normalize(uri_or_str)
18
+
19
+ new(
20
+ name: only_scheme_and_host(uri),
21
+ resource: "#{uri.host}:#{uri.port}",
22
+ type: type
23
+ )
24
+ end
25
+
26
+ def self.only_scheme_and_host(uri_or_str)
27
+ uri = normalize(uri_or_str)
28
+ uri.path = ''
29
+ uri.password = uri.query = uri.fragment = nil
30
+ uri.to_s
31
+ end
32
+
33
+ class << self
34
+ private
35
+
36
+ def normalize(uri_or_str)
37
+ return uri_or_str.dup if uri_or_str.is_a?(URI)
38
+ URI(uri_or_str)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Span
5
+ class Context
6
+ # @api private
7
+ class Http
8
+ def initialize(url: nil, status_code: nil, method: nil)
9
+ @url = sanitize_url(url)
10
+ @status_code = status_code
11
+ @method = method
12
+ end
13
+
14
+ attr_accessor :url, :status_code, :method
15
+
16
+ private
17
+
18
+ def sanitize_url(uri_or_str)
19
+ uri = uri_or_str.is_a?(URI) ? uri_or_str.dup : URI(uri_or_str)
20
+ uri.password = nil
21
+ uri.to_s
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -6,7 +6,9 @@ module ElasticAPM
6
6
  # @api private
7
7
  class ElasticsearchSpy
8
8
  NAME_FORMAT = '%s %s'
9
- TYPE = 'db.elasticsearch'
9
+ TYPE = 'db'
10
+ SUBTYPE = 'elasticsearch'
11
+
10
12
  def install
11
13
  ::Elasticsearch::Transport::Client.class_eval do
12
14
  alias perform_request_without_apm perform_request
@@ -14,11 +16,22 @@ module ElasticAPM
14
16
  def perform_request(method, path, *args, &block)
15
17
  name = format(NAME_FORMAT, method, path)
16
18
  statement = args[0].is_a?(String) ? args[0] : args[0].to_json
17
- context = Span::Context.new(db: { statement: statement })
18
19
 
19
- ElasticAPM.with_span name, TYPE, context: context do
20
- perform_request_without_apm(method, path, *args, &block)
21
- end
20
+ context = Span::Context.new(
21
+ db: { statement: statement },
22
+ destination: {
23
+ name: SUBTYPE,
24
+ resource: SUBTYPE,
25
+ type: TYPE
26
+ }
27
+ )
28
+
29
+ ElasticAPM.with_span(
30
+ name,
31
+ TYPE,
32
+ subtype: SUBTYPE,
33
+ context: context
34
+ ) { perform_request_without_apm(method, path, *args, &block) }
22
35
  end
23
36
  end
24
37
  end
@@ -16,7 +16,6 @@ module ElasticAPM
16
16
  end
17
17
  end
18
18
 
19
- # rubocop:disable Metrics/PerceivedComplexity
20
19
  # rubocop:disable Metrics/CyclomaticComplexity
21
20
  def install
22
21
  ::Faraday::Connection.class_eval do
@@ -27,40 +26,59 @@ module ElasticAPM
27
26
  return run_request_without_apm(method, url, body, headers, &block)
28
27
  end
29
28
 
30
- host = if url_prefix.is_a?(URI) && url_prefix.host
31
- url_prefix.host
32
- elsif url.nil?
33
- tmp_request = build_request(method) do |req|
34
- yield(req) if block_given?
35
- end
36
- URI(tmp_request.path).host
37
- else
38
- URI(url).host
39
- end
29
+ uri = URI(build_url(url))
40
30
 
41
- name = "#{method.upcase} #{host}"
31
+ # If url is set inside block it isn't available until yield,
32
+ # so we temporarily build the request to yield. This could be a
33
+ # problem if the block has side effects as it will be yielded twice
34
+ # ~mikker
35
+ unless uri.host
36
+ tmp_request = build_request(method) do |req|
37
+ yield(req) if block_given?
38
+ end
39
+ uri = URI(tmp_request.path)
40
+ end
41
+
42
+ host = uri.host
43
+
44
+ upcased_method = method.to_s.upcase
45
+
46
+ destination = ElasticAPM::Span::Context::Destination.from_uri(uri)
47
+
48
+ context =
49
+ ElasticAPM::Span::Context.new(
50
+ http: { url: uri, method: upcased_method },
51
+ destination: destination
52
+ )
42
53
 
43
54
  ElasticAPM.with_span(
44
- name,
55
+ "#{upcased_method} #{host}",
45
56
  TYPE,
46
57
  subtype: SUBTYPE,
47
- action: method.to_s
58
+ action: upcased_method,
59
+ context: context
48
60
  ) do |span|
49
61
  ElasticAPM::Spies::FaradaySpy.without_net_http do
50
62
  trace_context = span&.trace_context || transaction.trace_context
51
63
 
52
- run_request_without_apm(method, url, body, headers) do |req|
53
- req['Elastic-Apm-Traceparent'] = trace_context.to_header
64
+ result =
65
+ run_request_without_apm(method, url, body, headers) do |req|
66
+ req['Elastic-Apm-Traceparent'] = trace_context.to_header
54
67
 
55
- yield req if block_given?
68
+ yield req if block_given?
69
+ end
70
+
71
+ if (http = span&.context&.http)
72
+ http.status_code = result.status.to_s
56
73
  end
74
+
75
+ result
57
76
  end
58
77
  end
59
78
  end
60
79
  end
61
80
  end
62
81
  # rubocop:enable Metrics/CyclomaticComplexity
63
- # rubocop:enable Metrics/PerceivedComplexity
64
82
  end
65
83
 
66
84
  register 'Faraday', 'faraday', FaradaySpy.new
@@ -19,17 +19,31 @@ module ElasticAPM
19
19
  method = req.verb.to_s.upcase
20
20
  host = req.uri.host
21
21
 
22
+ destination =
23
+ ElasticAPM::Span::Context::Destination.from_uri(req.uri)
24
+ context = ElasticAPM::Span::Context.new(
25
+ http: { url: req.uri, method: method },
26
+ destination: destination
27
+ )
28
+
22
29
  name = "#{method} #{host}"
23
30
 
24
31
  ElasticAPM.with_span(
25
32
  name,
26
33
  TYPE,
27
34
  subtype: SUBTYPE,
28
- action: method
35
+ action: method,
36
+ context: context
29
37
  ) do |span|
30
38
  trace_context = span&.trace_context || transaction.trace_context
31
39
  req['Elastic-Apm-Traceparent'] = trace_context.to_header
32
- perform_without_apm(req, options)
40
+ result = perform_without_apm(req, options)
41
+
42
+ if (http = span&.context&.http)
43
+ http.status_code = result.status.to_s
44
+ end
45
+
46
+ result
33
47
  end
34
48
  end
35
49
  end
@@ -81,6 +81,11 @@ module ElasticAPM
81
81
  statement: event.command.to_s,
82
82
  type: 'mongodb',
83
83
  user: nil
84
+ },
85
+ destination: {
86
+ name: SUBTYPE,
87
+ resource: SUBTYPE,
88
+ type: TYPE
84
89
  }
85
90
  )
86
91
  end
@@ -8,6 +8,7 @@ module ElasticAPM
8
8
  KEY = :__elastic_apm_net_http_disabled
9
9
  TYPE = 'ext'
10
10
  SUBTYPE = 'net_http'
11
+
11
12
  class << self
12
13
  def disabled=(disabled)
13
14
  Thread.current[KEY] = disabled
@@ -28,6 +29,7 @@ module ElasticAPM
28
29
  end
29
30
  end
30
31
 
32
+ # rubocop:disable Metrics/CyclomaticComplexity
31
33
  def install
32
34
  Net::HTTP.class_eval do
33
35
  alias request_without_apm request
@@ -36,30 +38,48 @@ module ElasticAPM
36
38
  unless (transaction = ElasticAPM.current_transaction)
37
39
  return request_without_apm(req, body, &block)
38
40
  end
41
+
39
42
  if ElasticAPM::Spies::NetHTTPSpy.disabled?
40
43
  return request_without_apm(req, body, &block)
41
44
  end
42
45
 
43
- host, = req['host']&.split(':')
44
- method = req.method
46
+ host = req['host']&.split(':')&.first || address
47
+ method = req.method.to_s.upcase
48
+ path, query = req.path.split('?')
49
+
50
+ cls = use_ssl? ? URI::HTTPS : URI::HTTP
51
+ uri = cls.build([nil, host, port, path, query, nil])
45
52
 
46
- host ||= address
53
+ destination =
54
+ ElasticAPM::Span::Context::Destination.from_uri(uri)
47
55
 
48
- name = "#{method} #{host}"
56
+ context =
57
+ ElasticAPM::Span::Context.new(
58
+ http: { url: uri, method: method },
59
+ destination: destination
60
+ )
49
61
 
50
62
  ElasticAPM.with_span(
51
- name,
63
+ "#{method} #{host}",
52
64
  TYPE,
53
65
  subtype: SUBTYPE,
54
- action: method.to_s
66
+ action: method,
67
+ context: context
55
68
  ) do |span|
56
69
  trace_context = span&.trace_context || transaction.trace_context
57
70
  req['Elastic-Apm-Traceparent'] = trace_context.to_header
58
- request_without_apm(req, body, &block)
71
+ result = request_without_apm(req, body, &block)
72
+
73
+ if (http = span&.context&.http)
74
+ http.status_code = result.code
75
+ end
76
+
77
+ result
59
78
  end
60
79
  end
61
80
  end
62
81
  end
82
+ # rubocop:enable Metrics/CyclomaticComplexity
63
83
  end
64
84
 
65
85
  register 'Net::HTTP', 'net/http', NetHTTPSpy.new
@@ -1,22 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'elastic_apm/sql_summarizer'
3
+ require 'elastic_apm/sql'
4
4
 
5
5
  module ElasticAPM
6
6
  # @api private
7
7
  module Spies
8
8
  # @api private
9
9
  class SequelSpy
10
- TYPE = 'db.sequel.sql'
10
+ TYPE = 'db'
11
+ ACTION = 'query'
11
12
 
12
13
  def self.summarizer
13
- @summarizer ||= SqlSummarizer.new
14
- end
15
-
16
- def self.build_context(sql, opts)
17
- Span::Context.new(
18
- db: { statement: sql, type: 'sql', user: opts[:user] }
19
- )
14
+ @summarizer = Sql.summarizer
20
15
  end
21
16
 
22
17
  def install
@@ -25,16 +20,31 @@ module ElasticAPM
25
20
  ::Sequel::Database.class_eval do
26
21
  alias log_connection_yield_without_apm log_connection_yield
27
22
 
28
- def log_connection_yield(sql, *args, &block)
23
+ def log_connection_yield(sql, connection, args = nil, &block)
29
24
  unless ElasticAPM.current_transaction
30
- return log_connection_yield_without_apm(sql, *args, &block)
25
+ return log_connection_yield_without_apm(
26
+ sql, connection, args, &block
27
+ )
31
28
  end
32
29
 
33
- summarizer = ElasticAPM::Spies::SequelSpy.summarizer
34
- name = summarizer.summarize sql
35
- context = ElasticAPM::Spies::SequelSpy.build_context(sql, opts)
30
+ subtype = database_type.to_s
31
+
32
+ name =
33
+ ElasticAPM::Spies::SequelSpy.summarizer.summarize sql
34
+
35
+ context = ElasticAPM::Span::Context.new(
36
+ db: { statement: sql, type: 'sql', user: opts[:user] },
37
+ destination: { name: subtype, resource: subtype, type: TYPE }
38
+ )
36
39
 
37
- ElasticAPM.with_span(name, TYPE, context: context, &block)
40
+ ElasticAPM.with_span(
41
+ name,
42
+ TYPE,
43
+ subtype: subtype,
44
+ action: ACTION,
45
+ context: context,
46
+ &block
47
+ )
38
48
  end
39
49
  end
40
50
  end