elastic-apm 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of elastic-apm might be problematic. Click here for more details.

Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +3 -0
  4. data/README.md +7 -1
  5. data/docs/api.asciidoc +210 -0
  6. data/docs/configuration.asciidoc +32 -0
  7. data/docs/context.asciidoc +27 -0
  8. data/docs/custom-instrumentation.asciidoc +14 -0
  9. data/docs/getting-started-rack.asciidoc +26 -0
  10. data/docs/getting-started-rails.asciidoc +13 -0
  11. data/docs/index.asciidoc +33 -0
  12. data/elastic-apm.gemspec +1 -0
  13. data/lib/elastic_apm.rb +54 -7
  14. data/lib/elastic_apm/agent.rb +20 -4
  15. data/lib/elastic_apm/config.rb +16 -3
  16. data/lib/elastic_apm/context.rb +22 -0
  17. data/lib/elastic_apm/context/request.rb +13 -0
  18. data/lib/elastic_apm/context/request/socket.rb +21 -0
  19. data/lib/elastic_apm/context/request/url.rb +44 -0
  20. data/lib/elastic_apm/context/response.rb +24 -0
  21. data/lib/elastic_apm/context/user.rb +26 -0
  22. data/lib/elastic_apm/context_builder.rb +93 -0
  23. data/lib/elastic_apm/error.rb +6 -3
  24. data/lib/elastic_apm/error_builder.rb +26 -10
  25. data/lib/elastic_apm/http.rb +4 -2
  26. data/lib/elastic_apm/injectors/action_dispatch.rb +1 -1
  27. data/lib/elastic_apm/instrumenter.rb +18 -0
  28. data/lib/elastic_apm/middleware.rb +15 -3
  29. data/lib/elastic_apm/naively_hashable.rb +21 -0
  30. data/lib/elastic_apm/process_info.rb +22 -0
  31. data/lib/elastic_apm/serializers/errors.rb +10 -3
  32. data/lib/elastic_apm/serializers/transactions.rb +4 -2
  33. data/lib/elastic_apm/service_info.rb +0 -3
  34. data/lib/elastic_apm/span/context.rb +2 -4
  35. data/lib/elastic_apm/stacktrace/frame.rb +2 -18
  36. data/lib/elastic_apm/transaction.rb +20 -7
  37. data/lib/elastic_apm/version.rb +1 -1
  38. metadata +20 -4
  39. data/lib/elastic_apm/error/context.rb +0 -119
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'elastic_apm/naively_hashable'
4
+ require 'elastic_apm/context_builder'
3
5
  require 'elastic_apm/error_builder'
4
6
  require 'elastic_apm/error'
5
7
  require 'elastic_apm/http'
@@ -51,6 +53,7 @@ module ElasticAPM
51
53
  @queue = Queue.new
52
54
 
53
55
  @instrumenter = Instrumenter.new(config, self)
56
+ @context_builder = ContextBuilder.new(config)
54
57
  @error_builder = ErrorBuilder.new(config)
55
58
 
56
59
  @serializers = Struct.new(:transactions, :errors).new(
@@ -59,10 +62,10 @@ module ElasticAPM
59
62
  )
60
63
  end
61
64
 
62
- attr_reader :config, :queue, :instrumenter
65
+ attr_reader :config, :queue, :instrumenter, :context_builder
63
66
 
64
67
  def start
65
- debug 'Starting agent reporting to %s', config.server
68
+ debug 'Starting agent reporting to %s', config.server_url
66
69
 
67
70
  @instrumenter.start
68
71
 
@@ -114,12 +117,15 @@ module ElasticAPM
114
117
  instrumenter.span(*args, &block)
115
118
  end
116
119
 
120
+ def build_context(rack_env)
121
+ @context_builder.build(rack_env)
122
+ end
123
+
117
124
  # errors
118
125
 
119
- def report(exception, rack_env: nil, handled: true)
126
+ def report(exception, handled: true)
120
127
  error = @error_builder.build_exception(
121
128
  exception,
122
- rack_env: rack_env,
123
129
  handled: handled
124
130
  )
125
131
  enqueue_errors error
@@ -134,6 +140,16 @@ module ElasticAPM
134
140
  enqueue_errors error
135
141
  end
136
142
 
143
+ # context
144
+
145
+ def set_tag(*args)
146
+ instrumenter.set_tag(*args)
147
+ end
148
+
149
+ def set_custom_context(*args)
150
+ instrumenter.set_custom_context(*args)
151
+ end
152
+
137
153
  def inspect
138
154
  '<ElasticAPM::Agent>'
139
155
  end
@@ -6,7 +6,7 @@ module ElasticAPM
6
6
  # @api private
7
7
  class Config
8
8
  DEFAULTS = {
9
- server: 'http://localhost:8200',
9
+ server_url: 'http://localhost:8200',
10
10
  secret_token: nil,
11
11
 
12
12
  app_name: nil,
@@ -25,6 +25,11 @@ module ElasticAPM
25
25
 
26
26
  enabled_injectors: %w[net_http],
27
27
 
28
+ current_user_method: :current_user,
29
+ current_user_id_method: :id,
30
+ current_user_email_method: :email,
31
+ current_user_username_method: :username,
32
+
28
33
  view_paths: []
29
34
  }.freeze
30
35
 
@@ -42,7 +47,7 @@ module ElasticAPM
42
47
  yield self
43
48
  end
44
49
 
45
- attr_accessor :server
50
+ attr_accessor :server_url
46
51
  attr_accessor :secret_token
47
52
 
48
53
  attr_accessor :app_name
@@ -63,6 +68,11 @@ module ElasticAPM
63
68
 
64
69
  attr_accessor :view_paths
65
70
 
71
+ attr_accessor :current_user_method
72
+ attr_accessor :current_user_id_method
73
+ attr_accessor :current_user_email_method
74
+ attr_accessor :current_user_username_method
75
+
66
76
  attr_writer :logger
67
77
 
68
78
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
@@ -78,6 +88,9 @@ module ElasticAPM
78
88
  self.framework_version = Rails::VERSION::STRING
79
89
  self.logger = Rails.logger
80
90
  self.view_paths = app.config.paths['app/views'].existent
91
+ else
92
+ # TODO: define custom?
93
+ self.app_name = 'ruby'
81
94
  end
82
95
  end
83
96
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
@@ -106,7 +119,7 @@ module ElasticAPM
106
119
  end
107
120
 
108
121
  def use_ssl?
109
- server.start_with?('https')
122
+ server_url.start_with?('https')
110
123
  end
111
124
 
112
125
  private
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elastic_apm/context/request'
4
+ require 'elastic_apm/context/request/socket'
5
+ require 'elastic_apm/context/request/url'
6
+ require 'elastic_apm/context/response'
7
+ require 'elastic_apm/context/user'
8
+
9
+ module ElasticAPM
10
+ # @api private
11
+ class Context
12
+ include NaivelyHashable
13
+
14
+ attr_accessor :request, :response, :user
15
+ attr_reader :custom, :tags
16
+
17
+ def initialize
18
+ @custom = {}
19
+ @tags = {}
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Context
5
+ # @api private
6
+ class Request
7
+ include NaivelyHashable
8
+
9
+ attr_accessor :body, :cookies, :env, :headers, :http_version, :method,
10
+ :socket, :url
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ class Context
6
+ # @api private
7
+ class Request
8
+ # @api private
9
+ class Socket
10
+ include NaivelyHashable
11
+
12
+ def initialize(req)
13
+ @remote_addr = req.ip
14
+ @encrypted = req.scheme == 'https'
15
+ end
16
+
17
+ attr_reader :remote_addr, :encrypted
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ class Context
6
+ # @api private
7
+ class Request
8
+ # @api private
9
+ class Url
10
+ include NaivelyHashable
11
+
12
+ SKIPPED_PORTS = {
13
+ 'http' => 80,
14
+ 'https' => 443
15
+ }.freeze
16
+
17
+ def initialize(req)
18
+ @protocol = req.scheme
19
+ @hostname = req.host
20
+ @port = req.port.to_s
21
+ @pathname = req.path
22
+ @search = req.query_string
23
+ @hash = nil
24
+ @full = build_full_url req
25
+ end
26
+
27
+ attr_reader :protocol, :hostname, :port, :pathname, :search, :hash,
28
+ :full
29
+
30
+ private
31
+
32
+ def build_full_url(req)
33
+ url = "#{req.scheme}://#{req.host}"
34
+
35
+ if req.port != SKIPPED_PORTS[req.scheme]
36
+ url += ":#{req.port}"
37
+ end
38
+
39
+ url + req.fullpath
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Context
5
+ # @api private
6
+ class Response
7
+ include NaivelyHashable
8
+
9
+ def initialize(
10
+ status_code,
11
+ headers: {},
12
+ headers_sent: true,
13
+ finished: true
14
+ )
15
+ @status_code = status_code
16
+ @headers = headers
17
+ @headers_sent = headers_sent
18
+ @finished = finished
19
+ end
20
+
21
+ attr_accessor :status_code, :headers, :headers_sent, :finished
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Context
5
+ # @api private
6
+ class User
7
+ include NaivelyHashable
8
+
9
+ def initialize(config, record)
10
+ return unless record
11
+
12
+ @id = safe_get(record, config.current_user_id_method)
13
+ @email = safe_get(record, config.current_user_email_method)
14
+ @username = safe_get(record, config.current_user_username_method)
15
+ end
16
+
17
+ attr_accessor :id, :email, :username
18
+
19
+ private
20
+
21
+ def safe_get(record, method_name)
22
+ record.respond_to?(method_name) ? record.send(method_name) : nil
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ class ContextBuilder
6
+ CONTROLLER_KEY = 'action_controller.instance'.freeze
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ attr_reader :config
13
+
14
+ def build(rack_env)
15
+ context = Context.new
16
+
17
+ apply_to_request(context, rack_env)
18
+ apply_to_user(context, rack_env)
19
+
20
+ context
21
+ end
22
+
23
+ private
24
+
25
+ # rubocop:disable Metrics/AbcSize
26
+ def apply_to_request(context, rack_env)
27
+ req = rails_req?(rack_env) ? rack_env : Rack::Request.new(rack_env)
28
+
29
+ context.request = Context::Request.new unless context.request
30
+ request = context.request
31
+
32
+ request.socket = Context::Request::Socket.new(req).to_h
33
+ request.http_version = build_http_version rack_env
34
+ request.method = req.request_method
35
+ request.url = Context::Request::Url.new(req).to_h
36
+ request.headers, request.env = get_headers_and_env(rack_env)
37
+ request.body = get_body(req)
38
+
39
+ context
40
+ end
41
+ # rubocop:enable Metrics/AbcSize
42
+
43
+ def apply_to_user(context, rack_env)
44
+ return unless (controller = rack_env[CONTROLLER_KEY])
45
+
46
+ method = config.current_user_method.to_sym
47
+ return unless controller.respond_to?(method)
48
+
49
+ return unless (record = controller.send method)
50
+
51
+ context.user = Context::User.new(config, record)
52
+ context
53
+ end
54
+
55
+ def get_body(req)
56
+ return req.POST if req.form_data?
57
+
58
+ body = req.body.read
59
+ req.body.rewind
60
+ body
61
+ end
62
+
63
+ def rails_req?(env)
64
+ defined?(ActionDispatch::Request) &&
65
+ env.is_a?(ActionDispatch::Request)
66
+ end
67
+
68
+ def get_headers_and_env(rack_env)
69
+ # In Rails < 5 ActionDispatch::Request inherits from Hash
70
+ headers =
71
+ rack_env.respond_to?(:headers) ? rack_env.headers : rack_env
72
+
73
+ headers.each_with_object([{}, {}]) do |(key, value), (http, env)|
74
+ next unless key == key.upcase
75
+
76
+ if key.start_with?('HTTP_')
77
+ http[camel_key(key)] = value
78
+ else
79
+ env[key] = value
80
+ end
81
+ end
82
+ end
83
+
84
+ def camel_key(key)
85
+ key.gsub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
86
+ end
87
+
88
+ def build_http_version(rack_env)
89
+ return unless (http_version = rack_env['HTTP_VERSION'])
90
+ http_version.gsub(%r{HTTP/}, '')
91
+ end
92
+ end
93
+ end
@@ -1,21 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'elastic_apm/stacktrace'
4
+ require 'elastic_apm/context'
4
5
  require 'elastic_apm/error/exception'
5
6
  require 'elastic_apm/error/log'
6
- require 'elastic_apm/error/context'
7
7
 
8
8
  module ElasticAPM
9
9
  # @api private
10
10
  class Error
11
11
  def initialize(culprit: nil)
12
+ @id = SecureRandom.uuid
12
13
  @culprit = culprit
13
14
 
14
15
  @timestamp = Util.micros
15
16
  @context = Context.new
17
+
18
+ @transaction_id = nil
16
19
  end
17
20
 
18
- attr_accessor :culprit, :exception, :log
19
- attr_reader :timestamp, :context
21
+ attr_accessor :id, :culprit, :exception, :log, :transaction_id, :context
22
+ attr_reader :timestamp
20
23
  end
21
24
  end
@@ -9,17 +9,15 @@ module ElasticAPM
9
9
 
10
10
  attr_reader :config
11
11
 
12
- def build_exception(exception, rack_env: nil, handled: true)
12
+ def build_exception(exception, handled: true)
13
13
  error = Error.new
14
14
  error.exception = Error::Exception.new(exception, handled: handled)
15
15
 
16
- if (stacktrace = Stacktrace.build(config, exception.backtrace))
17
- error.exception.stacktrace = stacktrace
18
- error.culprit = stacktrace.frames.last.function
19
- end
16
+ add_stacktrace error, :exception, exception.backtrace
17
+ add_transaction_id error
20
18
 
21
- if rack_env
22
- error.context.request = Error::Context::Request.from_rack_env rack_env
19
+ if (transaction = ElasticAPM.current_transaction)
20
+ error.context = transaction.context.dup
23
21
  end
24
22
 
25
23
  error
@@ -29,12 +27,30 @@ module ElasticAPM
29
27
  error = Error.new
30
28
  error.log = Error::Log.new(message, **attrs)
31
29
 
32
- if (stacktrace = Stacktrace.build(config, backtrace))
30
+ add_stacktrace error, :log, backtrace
31
+ add_transaction_id error
32
+
33
+ error
34
+ end
35
+
36
+ private
37
+
38
+ def add_stacktrace(error, kind, backtrace)
39
+ return unless (stacktrace = Stacktrace.build(config, backtrace))
40
+
41
+ case kind
42
+ when :exception
43
+ error.exception.stacktrace = stacktrace
44
+ when :log
33
45
  error.log.stacktrace = stacktrace
34
- error.culprit = stacktrace.frames.last.function
35
46
  end
36
47
 
37
- error
48
+ error.culprit = stacktrace.frames.last.function
49
+ end
50
+
51
+ def add_transaction_id(error)
52
+ return unless (transaction = ElasticAPM.current_transaction)
53
+ error.transaction_id = transaction.id
38
54
  end
39
55
  end
40
56
  end