elastic-apm 0.2.0 → 0.3.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.

Potentially problematic release.


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

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e0d3d869c607c4a439fb2c19ae77c35572cadf74f34f6bb4c2075e393a00127
4
- data.tar.gz: a126c14da9817c69b4b76ba17bd12521d865ea2b791b59ff978d0fa6bbafa256
3
+ metadata.gz: f6272279575b41ace4dc7581df1d6ea387f46c260d591c338fceb256f0b46ff2
4
+ data.tar.gz: f793a0c902a18abfe647e5a3a41c86d96926910dfef21094aa3b1c37deac45eb
5
5
  SHA512:
6
- metadata.gz: c4951d4fce0cb88c5cb45006a24f272156785d2bf9558ce93b84b2756a178957dbb75149bf6a529e018845f626cdf581721750131227f095e09e06ed228ba208
7
- data.tar.gz: 66407124c6987fffc19a0340a57cf781dc8655ca7385da04d3ebc08f4c1342195b7dce0b0c0e87ebbc1f79e81ec60697556fe1dfae7630ff9b893d8edd7c3edf
6
+ metadata.gz: 3f16cbdffc10a05005c03ec0d122235c84b56a9bda9f2201fd913e1d3b96925d3f5ef0847cfd2cc369da0b1d1c39c21dccd187579f278d7439218c97b4548a97
7
+ data.tar.gz: 4daea6a3d579f4fba6435de9277bdd842889b6afeab63c97da9a9424809d6e4721bd873da4edecae1eeb66281ae43989385027099f0c828a38bff94c4ec3addd
@@ -21,6 +21,10 @@ Metrics/BlockLength:
21
21
  Exclude:
22
22
  - 'spec/**/*.rb'
23
23
 
24
+ Metrics/ModuleLength:
25
+ Exclude:
26
+ - 'spec/**/*.rb'
27
+
24
28
  Naming/PredicateName:
25
29
  Enabled: false
26
30
 
data/README.md CHANGED
@@ -1,57 +1,25 @@
1
- # elastic-apm – Elastic APM agent for Ruby (ALPHA)
1
+ # elastic-apm – Elastic APM agent for Ruby (BETA)
2
2
 
3
3
  [![Jenkins](https://img.shields.io/jenkins/s/https/apm-ci.elastic.co/job/elastic+apm-agent-ruby+master.svg)](https://apm-ci.elastic.co/job/elastic+apm-agent-ruby+master/) [![Gem](https://img.shields.io/gem/v/formatador.svg?style=flat-square)](https://rubygems.org/gems/elastic-apm)
4
4
 
5
5
  This is the official Rubygem for adding [Elastic][]'s [APM][] to your Ruby app.
6
6
 
7
- ## Setup
8
-
9
- Add the gem to your `Gemfile`:
10
-
11
- ```ruby
12
- gem 'elastic-apm'
13
- ```
14
-
15
- ## Getting started with Rails
16
-
17
- If you're using Rails the gem automatically inserts itself where it needs to be.
18
-
19
- _Describe configuration using yaml, config, etc_
20
-
21
- ### Optional: Configure the agent
22
-
23
- The suggested way to configure is to create a file `config/elastic_apm.yml` with your config:
24
-
25
- ```yaml
26
- # config/elastic_apm.yml
27
-
28
- server_url: http://localhost:8200
29
- secret_token: YOUR_SECRET
30
- ```
31
-
32
- ## Getting started with Sinatra
33
-
34
- ```ruby
35
- # config.ru
36
-
37
- require 'sinatra/base'
38
-
39
- class MySinatraApp < Sinatra::Base
40
- use ElasticAPM::Middleware
41
-
42
- # ...
43
- end
44
-
45
- # Takes optional ElasticAPM::Config values
46
- ElasticAPM.start(
47
- app: MySinatraApp, # required
48
- server_url: 'http://localhost:8200'
49
- )
50
-
51
- run MySinatraApp
52
-
53
- at_exit { ElasticAPM.stop }
54
- ```
7
+ <div>
8
+ <ul>
9
+ <li><a href="https://www.elastic.co/guide/en/apm/agent/ruby/1.x/_introduction.html">Introduction</a></li>
10
+ <li><a href="https://www.elastic.co/guide/en/apm/agent/ruby/1.x/getting-started-rails.html">Getting started with Rails</a></li>
11
+ <li><a href="https://www.elastic.co/guide/en/apm/agent/ruby/1.x/getting-started-rack.html">Getting started with Rack</a></li>
12
+ <li><a href="https://www.elastic.co/guide/en/apm/agent/ruby/1.x/configuration.html">Configuration</a></li>
13
+ <li class="collapsible">
14
+ <a href="https://www.elastic.co/guide/en/apm/agent/ruby/1.x/advanced.html">Advanced Topics</a>
15
+ <ul>
16
+ <li><a href="https://www.elastic.co/guide/en/apm/agent/ruby/1.x/custom-instrumentation.html">Custom instrumentation</a></li>
17
+ <li><a href="https://www.elastic.co/guide/en/apm/agent/ruby/1.x/injectors.html">Injectors — automatic integrations with third-party libraries</a></li>
18
+ </ul>
19
+ </li>
20
+ <li><a href="https://www.elastic.co/guide/en/apm/agent/ruby/1.x/api.html">Public API</a></li>
21
+ </ul>
22
+ </div>
55
23
 
56
24
  # License
57
25
 
@@ -44,6 +44,7 @@ Returns whether the ElasticAPM Agent is currently running.
44
44
 
45
45
  Returns the currently running agent or nil.
46
46
 
47
+ [float]
47
48
  === Instrumentation
48
49
 
49
50
  [float]
@@ -119,6 +120,7 @@ Arguments:
119
120
 
120
121
  Returns the built context.
121
122
 
123
+ [float]
122
124
  === Errors
123
125
 
124
126
  [float]
@@ -165,6 +167,7 @@ Arguments:
165
167
 
166
168
  Returns `[ElasticAPM::Error]`.
167
169
 
170
+ [float]
168
171
  === Context
169
172
 
170
173
  [float]
@@ -208,3 +211,15 @@ Arguments:
208
211
 
209
212
  Returns current custom context.
210
213
 
214
+ [float]
215
+ [[api-set-user]]
216
+ ==== `ElasticAPM.set_user`
217
+
218
+ Add the current user to the current transaction's context.
219
+
220
+ Arguments:
221
+
222
+ * `user`: An object representing the user
223
+
224
+ Returns the given user
225
+
@@ -3,6 +3,7 @@
3
3
 
4
4
  There are several ways to shape how Elastic APM behaves.
5
5
 
6
+ [float]
6
7
  === Rails
7
8
 
8
9
  The recommended way to configure Elastic APM for Rails is to create a file `config/elastic_apm.yml` and specify options in there:
@@ -14,6 +15,7 @@ server_url: 'http://localhost:8200'
14
15
  secret_token: 'very_very_secret'
15
16
  ----
16
17
 
18
+ [float]
17
19
  === Sinatra and Rack
18
20
 
19
21
  When using APM with Sinatra and Rack, you should configure it when starting the agent:
@@ -1,6 +1,8 @@
1
+ [float]
1
2
  [[context]]
2
3
  === Adding additional context
3
4
 
5
+ [float]
4
6
  ==== Adding custom context
5
7
 
6
8
  You can add your own custom, nested JSON-compatible data to the current transaction using `ElasticAPM.add_custom_context(hash)` eg.:
@@ -16,6 +18,7 @@ class ThingsController < ApplicationController
16
18
  end
17
19
  ----
18
20
 
21
+ [float]
19
22
  ==== Adding tags
20
23
 
21
24
  Tags are special in that they are indexed in your Elasticsearch database and therefore searchable.
@@ -25,3 +28,16 @@ Tags are special in that they are indexed in your Elasticsearch database and the
25
28
  ElasticAPM.set_tag(:company_name, 'Acme, Inc.')
26
29
  ----
27
30
 
31
+ [float]
32
+ ==== Providing info about the user
33
+
34
+ You can provide ElasticAPM with info about the current user.
35
+
36
+ [source,ruby]
37
+ ----
38
+ class ApplicationController < ActionController::Base
39
+ before_action do
40
+ current_user && ElasticAPM.set_user(current_user)
41
+ end
42
+ end
43
+ ----
@@ -1,6 +1,7 @@
1
1
  [[getting-started-rails]]
2
2
  == Getting started with Rails
3
3
 
4
+ [float]
4
5
  === Setup
5
6
 
6
7
  Add the gem to your `Gemfile`:
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'elastic_apm/version'
4
4
  require 'elastic_apm/log'
5
+ require 'elastic_apm/util/dig'
5
6
 
6
7
  # Core
7
8
  require 'elastic_apm/agent'
@@ -73,9 +74,17 @@ module ElasticAPM
73
74
  # @param context [Span::Context] Context information about the span
74
75
  # @yield [Span] Optional block encapsulating span
75
76
  # @return [Span] Unless block given
76
- def self.span(name, type = nil, context: nil, &block)
77
+ def self.span(name, type = nil, context: nil, include_stacktrace: true,
78
+ &block)
77
79
  return call_through(&block) unless agent
78
- agent.span(name, type, context: context, &block)
80
+
81
+ agent.span(
82
+ name,
83
+ type,
84
+ context: context,
85
+ backtrace: include_stacktrace ? caller : nil,
86
+ &block
87
+ )
79
88
  end
80
89
 
81
90
  # Build a [Context] from a Rack `env`. The context may include information
@@ -125,6 +134,28 @@ module ElasticAPM
125
134
  agent && agent.set_custom_context(custom)
126
135
  end
127
136
 
137
+ # Provide a user to the current transaction
138
+ #
139
+ # @param user [Object] An object representing a user
140
+ # @return [Object] Given user
141
+ def self.set_user(user)
142
+ agent && agent.set_user(user)
143
+ end
144
+
145
+ # Provide a filter to transform payloads before sending them off
146
+ #
147
+ # @param key [Symbol] Unique filter key
148
+ # @param callback [Object, Proc] A filter that responds to #call(payload)
149
+ # @yield [Hash] A filter. Will be used if provided. Otherwise using `callback`
150
+ # @return [Bool] true
151
+ def self.add_filter(key, callback = nil, &block)
152
+ if callback.nil? && !block_given?
153
+ raise ArgumentError, '#add_filter needs either `callback\' or a block'
154
+ end
155
+
156
+ agent && agent.add_filter(key, block || callback)
157
+ end
158
+
128
159
  class << self
129
160
  private
130
161
 
@@ -28,7 +28,8 @@ module ElasticAPM
28
28
 
29
29
  LOCK.synchronize do
30
30
  return @instance if @instance
31
- @instance = new(config).start
31
+
32
+ @instance = new(config.freeze).start
32
33
  end
33
34
  end
34
35
 
@@ -45,11 +46,13 @@ module ElasticAPM
45
46
  !!@instance
46
47
  end
47
48
 
49
+ # rubocop:disable Metrics/MethodLength
48
50
  def initialize(config)
49
51
  config = Config.new(config) if config.is_a?(Hash)
50
52
 
51
53
  @config = config
52
54
 
55
+ @http = Http.new(config)
53
56
  @queue = Queue.new
54
57
 
55
58
  @instrumenter = Instrumenter.new(config, self)
@@ -61,8 +64,9 @@ module ElasticAPM
61
64
  Serializers::Errors.new(config)
62
65
  )
63
66
  end
67
+ # rubocop:enable Metrics/MethodLength
64
68
 
65
- attr_reader :config, :queue, :instrumenter, :context_builder
69
+ attr_reader :config, :queue, :instrumenter, :context_builder, :http
66
70
 
67
71
  def start
68
72
  debug 'Starting agent reporting to %s', config.server_url
@@ -150,6 +154,14 @@ module ElasticAPM
150
154
  instrumenter.set_custom_context(*args)
151
155
  end
152
156
 
157
+ def set_user(*args)
158
+ instrumenter.set_user(*args)
159
+ end
160
+
161
+ def add_filter(key, callback)
162
+ @http.filters.add(key, callback)
163
+ end
164
+
153
165
  def inspect
154
166
  '<ElasticAPM::Agent>'
155
167
  end
@@ -160,7 +172,7 @@ module ElasticAPM
160
172
  debug 'Booting worker in thread'
161
173
 
162
174
  @worker_thread = Thread.new do
163
- Worker.new(@config, @queue).run_forever
175
+ Worker.new(@config, @queue, @http).run_forever
164
176
  end
165
177
  end
166
178
 
@@ -3,6 +3,7 @@
3
3
  require 'logger'
4
4
 
5
5
  module ElasticAPM
6
+ # rubocop:disable Metrics/ClassLength
6
7
  # @api private
7
8
  class Config
8
9
  DEFAULTS = {
@@ -16,50 +17,67 @@ module ElasticAPM
16
17
 
17
18
  log_path: '-',
18
19
  log_level: Logger::INFO,
20
+ logger: nil,
19
21
 
20
- timeout: 10,
21
- open_timeout: 10,
22
+ http_timeout: 10,
23
+ http_open_timeout: 10,
22
24
  transaction_send_interval: 10,
23
25
  debug_transactions: false,
24
26
  debug_http: false,
25
27
 
26
- enabled_injectors: %w[net_http],
28
+ enabled_injectors: %w[net_http json],
27
29
 
28
- current_user_method: :current_user,
29
30
  current_user_id_method: :id,
30
31
  current_user_email_method: :email,
31
32
  current_user_username_method: :username,
32
33
 
33
- view_paths: []
34
+ view_paths: [],
35
+ root_path: Dir.pwd
34
36
  }.freeze
35
37
 
36
- LOCK = Mutex.new
38
+ ENV_TO_KEY = {
39
+ 'ELASTIC_APM_APP_NAME' => 'app_name',
40
+ 'ELASTIC_APM_SERVER_URL' => 'server_url',
41
+ 'ELASTIC_APM_SECRET_TOKEN' => 'secret_token'
42
+ }.freeze
37
43
 
44
+ # rubocop:disable Metrics/MethodLength
38
45
  def initialize(options = nil)
39
46
  options = {} if options.nil?
40
47
 
41
- DEFAULTS.merge(options).each do |key, value|
48
+ # Start with the defaults
49
+ DEFAULTS.each do |key, value|
42
50
  send("#{key}=", value)
43
51
  end
44
52
 
45
- return unless block_given?
53
+ # Set options from ENV
54
+ ENV_TO_KEY.each do |env_key, key|
55
+ next unless (value = ENV[env_key])
56
+ send("#{key}=", value)
57
+ end
58
+
59
+ # Set options from arguments
60
+ options.each do |key, value|
61
+ send("#{key}=", value)
62
+ end
46
63
 
47
- yield self
64
+ yield self if block_given?
48
65
  end
66
+ # rubocop:enable Metrics/MethodLength
49
67
 
50
68
  attr_accessor :server_url
51
69
  attr_accessor :secret_token
52
70
 
53
71
  attr_accessor :app_name
54
- attr_writer :environment
72
+ attr_reader :environment
55
73
  attr_accessor :framework_name
56
74
  attr_accessor :framework_version
57
75
 
58
76
  attr_accessor :log_path
59
77
  attr_accessor :log_level
60
78
 
61
- attr_accessor :timeout
62
- attr_accessor :open_timeout
79
+ attr_accessor :http_timeout
80
+ attr_accessor :http_open_timeout
63
81
  attr_accessor :transaction_send_interval
64
82
  attr_accessor :debug_transactions
65
83
  attr_accessor :debug_http
@@ -67,13 +85,14 @@ module ElasticAPM
67
85
  attr_accessor :enabled_injectors
68
86
 
69
87
  attr_accessor :view_paths
88
+ attr_accessor :root_path
70
89
 
71
90
  attr_accessor :current_user_method
72
91
  attr_accessor :current_user_id_method
73
92
  attr_accessor :current_user_email_method
74
93
  attr_accessor :current_user_username_method
75
94
 
76
- attr_writer :logger
95
+ attr_reader :logger
77
96
 
78
97
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
79
98
  def app=(app)
@@ -82,11 +101,14 @@ module ElasticAPM
82
101
  self.app_name = format_name(app_name || app.to_s)
83
102
  self.framework_name = 'Sinatra'
84
103
  self.framework_version = Sinatra::VERSION
104
+ self.enabled_injectors += %w[sinatra]
105
+ self.root_path = Dir.pwd
85
106
  when :rails
86
107
  self.app_name = format_name(app_name || app.class.parent_name)
87
108
  self.framework_name = 'Ruby on Rails'
88
109
  self.framework_version = Rails::VERSION::STRING
89
110
  self.logger = Rails.logger
111
+ self.root_path = Rails.root.to_s
90
112
  self.view_paths = app.config.paths['app/views'].existent
91
113
  else
92
114
  # TODO: define custom?
@@ -107,19 +129,16 @@ module ElasticAPM
107
129
  nil
108
130
  end
109
131
 
110
- def environment
111
- @environment ||= ENV['RAILS_ENV'] || ENV['RACK_ENV']
132
+ def use_ssl?
133
+ server_url.start_with?('https')
112
134
  end
113
135
 
114
- def logger
115
- @logger ||=
116
- LOCK.synchronize do
117
- build_logger(log_path, log_level)
118
- end
136
+ def environment=(env)
137
+ @environment = env || ENV['RAILS_ENV'] || ENV['RACK_ENV']
119
138
  end
120
139
 
121
- def use_ssl?
122
- server_url.start_with?('https')
140
+ def logger=(logger)
141
+ @logger = logger || build_logger(log_path, log_level)
123
142
  end
124
143
 
125
144
  private
@@ -134,4 +153,5 @@ module ElasticAPM
134
153
  str.gsub('::', '_')
135
154
  end
136
155
  end
156
+ # rubocop:enable Metrics/ClassLength
137
157
  end
@@ -3,20 +3,11 @@
3
3
  module ElasticAPM
4
4
  # @api private
5
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
6
+ def initialize(_config); end
13
7
 
14
8
  def build(rack_env)
15
9
  context = Context.new
16
-
17
10
  apply_to_request(context, rack_env)
18
- apply_to_user(context, rack_env)
19
-
20
11
  context
21
12
  end
22
13
 
@@ -40,18 +31,6 @@ module ElasticAPM
40
31
  end
41
32
  # rubocop:enable Metrics/AbcSize
42
33
 
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
34
  def get_body(req)
56
35
  return req.POST if req.form_data?
57
36
 
@@ -7,8 +7,6 @@ module ElasticAPM
7
7
  @config = config
8
8
  end
9
9
 
10
- attr_reader :config
11
-
12
10
  def build_exception(exception, handled: true)
13
11
  error = Error.new
14
12
  error.exception = Error::Exception.new(exception, handled: handled)
@@ -36,7 +34,7 @@ module ElasticAPM
36
34
  private
37
35
 
38
36
  def add_stacktrace(error, kind, backtrace)
39
- return unless (stacktrace = Stacktrace.build(config, backtrace))
37
+ return unless (stacktrace = Stacktrace.build(@config, backtrace))
40
38
 
41
39
  case kind
42
40
  when :exception
@@ -45,7 +43,7 @@ module ElasticAPM
45
43
  error.log.stacktrace = stacktrace
46
44
  end
47
45
 
48
- error.culprit = stacktrace.frames.last.function
46
+ error.culprit = stacktrace.frames.first.function
49
47
  end
50
48
 
51
49
  def add_transaction_id(error)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elastic_apm/filters/request_body_filter'
4
+ require 'elastic_apm/filters/secrets_filter'
5
+
6
+ module ElasticAPM
7
+ # @api private
8
+ module Filters
9
+ def self.new(config)
10
+ Container.new(config)
11
+ end
12
+
13
+ # @api private
14
+ class Container
15
+ def initialize(config)
16
+ @config = config
17
+ @filters = {
18
+ request_body: RequestBodyFilter.new(config),
19
+ secrets: SecretsFilter.new(config)
20
+ }
21
+ end
22
+
23
+ attr_reader :config
24
+
25
+ def add(key, filter)
26
+ @filters[key] = filter
27
+ end
28
+
29
+ def remove(key)
30
+ @filters.delete(key)
31
+ end
32
+
33
+ def apply(payload)
34
+ @filters.reduce(payload) do |result, (_key, filter)|
35
+ filter.call(result)
36
+ end
37
+ end
38
+
39
+ def length
40
+ @filters.length
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ module Filters
5
+ # @api private
6
+ class RequestBodyFilter
7
+ FILTERED = '[FILTERED]'.freeze
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ def call(payload)
14
+ strip_body_from payload[:transactions]
15
+ strip_body_from payload[:errors]
16
+
17
+ payload
18
+ end
19
+
20
+ private
21
+
22
+ def strip_body_from(arr)
23
+ return unless arr
24
+
25
+ arr.each do |entity|
26
+ next unless (request = entity.dig(:context, :request))
27
+
28
+ request[:body] = FILTERED
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ module Filters
5
+ # @api private
6
+ class SecretsFilter
7
+ FILTERED = '[FILTERED]'.freeze
8
+
9
+ KEY_FILTERS = [
10
+ /passw(or)?d/i,
11
+ /^pw$/,
12
+ /secret/i,
13
+ /token/i,
14
+ /api[-._]?key/i,
15
+ /session[-._]?id/i
16
+ ].freeze
17
+
18
+ VALUE_FILTERS = [
19
+ /^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/ # (probably) credit card number
20
+ ].freeze
21
+
22
+ def initialize(config)
23
+ @config = config
24
+ end
25
+
26
+ def call(payload)
27
+ strip_from payload[:transactions], :context, :request, :headers
28
+ strip_from payload[:transactions], :context, :response, :headers
29
+ strip_from payload[:errors], :context, :request, :headers
30
+ strip_from payload[:errors], :context, :response, :headers
31
+
32
+ payload
33
+ end
34
+
35
+ def strip_from(events, *path)
36
+ return unless events
37
+
38
+ events.each do |event|
39
+ next unless (headers = event.dig(*path))
40
+
41
+ headers.each do |k, v|
42
+ if filter_key?(k) || filter_value?(v)
43
+ headers[k] = FILTERED
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def filter_key?(key)
50
+ KEY_FILTERS.any? { |regex| key =~ regex }
51
+ end
52
+
53
+ def filter_value?(value)
54
+ VALUE_FILTERS.any? { |regex| value =~ regex }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -5,6 +5,7 @@ require 'net/http'
5
5
  require 'elastic_apm/service_info'
6
6
  require 'elastic_apm/system_info'
7
7
  require 'elastic_apm/process_info'
8
+ require 'elastic_apm/filters'
8
9
 
9
10
  module ElasticAPM
10
11
  # @api private
@@ -23,12 +24,14 @@ module ElasticAPM
23
24
  process: ProcessInfo.build(config),
24
25
  system: SystemInfo.build(config)
25
26
  }
27
+ @filters = Filters.new(config)
26
28
  end
27
29
 
28
- attr_reader :config
30
+ attr_reader :filters
29
31
 
30
32
  def post(path, payload = {})
31
33
  payload.merge! @base_payload
34
+ filters.apply(payload)
32
35
  request = prepare_request path, payload.to_json
33
36
  response = @adapter.perform request
34
37
 
@@ -50,7 +53,7 @@ module ElasticAPM
50
53
  req['User-Agent'] = USER_AGENT
51
54
  req['Content-Length'] = data.bytesize.to_s
52
55
 
53
- if (token = config.secret_token)
56
+ if (token = @config.secret_token)
54
57
  req['Authorization'] = "Bearer #{token}"
55
58
  end
56
59
 
@@ -88,8 +91,8 @@ module ElasticAPM
88
91
 
89
92
  http = Net::HTTP.new server_uri.host, server_uri.port
90
93
  http.use_ssl = @config.use_ssl?
91
- http.read_timeout = @config.timeout
92
- http.open_timeout = @config.open_timeout
94
+ http.read_timeout = @config.http_timeout
95
+ http.open_timeout = @config.http_open_timeout
93
96
 
94
97
  if @config.debug_http
95
98
  http.set_debug_output(@config.logger)
@@ -32,7 +32,7 @@ module ElasticAPM
32
32
 
33
33
  @transaction_info = TransactionInfo.new
34
34
 
35
- @subscriber = subscriber_class.new(self)
35
+ @subscriber = subscriber_class.new(config)
36
36
 
37
37
  @pending_transactions = []
38
38
  @last_sent_transactions = Time.now.utc
@@ -92,6 +92,10 @@ module ElasticAPM
92
92
  transaction.context.custom.merge!(context)
93
93
  end
94
94
 
95
+ def set_user(user)
96
+ transaction.context.user = Context::User.new(config, user)
97
+ end
98
+
95
99
  def submit_transaction(transaction)
96
100
  @pending_transactions << transaction
97
101
 
@@ -26,12 +26,14 @@ module ElasticAPM
26
26
  end
27
27
 
28
28
  def log(lvl, msg, *args)
29
+ return unless logger
30
+
29
31
  formatted_msg = prepend_prefix(format(msg.to_s, *args))
30
32
 
31
- return config.logger.send(lvl, formatted_msg) unless block_given?
33
+ return logger.send(lvl, formatted_msg) unless block_given?
32
34
 
33
35
  # TODO: dont evaluate block if level is higher
34
- config.logger.send(lvl, "#{formatted_msg}\n#{yield}")
36
+ logger.send(lvl, "#{formatted_msg}\n#{yield}")
35
37
  end
36
38
 
37
39
  private
@@ -40,8 +42,9 @@ module ElasticAPM
40
42
  "#{PREFIX}#{str}"
41
43
  end
42
44
 
43
- def has_logger?
44
- respond_to?(:config) && config.logger
45
+ def logger
46
+ return false unless (config = instance_variable_get(:@config))
47
+ config.logger
45
48
  end
46
49
  end
47
50
  end
@@ -10,7 +10,7 @@ module ElasticAPM
10
10
  # rubocop:disable Metrics/MethodLength
11
11
  def call(env)
12
12
  begin
13
- transaction = ElasticAPM.transaction 'Rack', type_for(env),
13
+ transaction = ElasticAPM.transaction 'Rack', 'app',
14
14
  context: ElasticAPM.build_context(env)
15
15
 
16
16
  resp = @app.call env
@@ -29,14 +29,5 @@ module ElasticAPM
29
29
  resp
30
30
  end
31
31
  # rubocop:enable Metrics/MethodLength
32
-
33
- private
34
-
35
- def type_for(env)
36
- format(
37
- 'request.%s'.freeze,
38
- env.fetch('REQUEST_METHOD'.freeze, 'unknown'.freeze)
39
- )
40
- end
41
32
  end
42
33
  end
@@ -13,17 +13,7 @@ module ElasticAPM
13
13
  result: transaction.result.to_s,
14
14
  duration: ms(transaction.duration),
15
15
  timestamp: micros_to_time(transaction.timestamp).utc.iso8601,
16
- spans: transaction.spans.map do |span|
17
- {
18
- id: span.id,
19
- parent: span.parent && span.parent.id,
20
- name: span.name,
21
- type: span.type,
22
- start: ms(span.relative_start),
23
- duration: ms(span.duration),
24
- context: span.context && { db: span.context.to_h }
25
- }
26
- end,
16
+ spans: transaction.spans.map(&method(:build_span)),
27
17
  sampled: transaction.sampled,
28
18
  context: transaction.context.to_h
29
19
  }
@@ -33,6 +23,23 @@ module ElasticAPM
33
23
  def build_all(transactions)
34
24
  { transactions: Array(transactions).map(&method(:build)) }
35
25
  end
26
+
27
+ private
28
+
29
+ # rubocop:disable Metrics/AbcSize
30
+ def build_span(span)
31
+ {
32
+ id: span.id,
33
+ parent: span.parent && span.parent.id,
34
+ name: span.name,
35
+ type: span.type,
36
+ start: ms(span.relative_start),
37
+ duration: ms(span.duration),
38
+ context: span.context && { db: span.context.to_h },
39
+ stacktrace: span.stacktrace.to_a
40
+ }
41
+ end
42
+ # rubocop:enable Metrics/AbcSize
36
43
  end
37
44
  end
38
45
  end
@@ -7,13 +7,11 @@ module ElasticAPM
7
7
  @config = config
8
8
  end
9
9
 
10
- attr_reader :config
11
-
12
10
  # rubocop:disable Metrics/MethodLength
13
11
  def build
14
12
  base = {
15
- name: config.app_name,
16
- environment: config.environment,
13
+ name: @config.app_name,
14
+ environment: @config.environment,
17
15
  agent: {
18
16
  name: 'ruby',
19
17
  version: VERSION
@@ -27,10 +25,10 @@ module ElasticAPM
27
25
  version: git_sha
28
26
  }
29
27
 
30
- if config.framework_name
28
+ if @config.framework_name
31
29
  base[:framework] = {
32
- name: config.framework_name,
33
- version: config.framework_version
30
+ name: @config.framework_name,
31
+ version: @config.framework_version
34
32
  }
35
33
  end
36
34
 
@@ -22,10 +22,11 @@ module ElasticAPM
22
22
  @type = type
23
23
  @parent = parent
24
24
  @context = context
25
+ @stacktrace = nil
25
26
  end
26
27
  # rubocop:enable Metrics/ParameterLists
27
28
 
28
- attr_accessor :name, :context, :type
29
+ attr_accessor :name, :context, :type, :stacktrace
29
30
  attr_reader :id, :duration, :parent, :relative_start
30
31
 
31
32
  def start
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'elastic_apm/util/lru_cache'
4
+
3
5
  module ElasticAPM
4
6
  # @api private
5
7
  class SqlSummarizer
@@ -10,15 +12,17 @@ module ElasticAPM
10
12
  /^DELETE FROM ([^ ]+)/i => 'DELETE FROM '
11
13
  }.freeze
12
14
 
15
+ FORMAT = '%s%s'.freeze
16
+
13
17
  def self.cache
14
- @cache ||= {}
18
+ @cache ||= Util::LruCache.new
15
19
  end
16
20
 
17
21
  def summarize(sql)
18
22
  self.class.cache[sql] ||=
19
23
  REGEXES.find do |regex, sig|
20
24
  if (match = sql.match(regex))
21
- break format("#{sig}#{match[1]}")
25
+ break format(FORMAT, sig, match[1].gsub(/["']/, ''))
22
26
  end
23
27
  end
24
28
  end
@@ -1,27 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'elastic_apm/stacktrace/frame'
4
+ require 'elastic_apm/stacktrace/line_cache'
4
5
 
5
6
  module ElasticAPM
6
7
  # @api private
7
8
  class Stacktrace
9
+ GEMS_REGEX = %r{/gems/}
10
+
8
11
  def initialize(backtrace)
9
12
  @backtrace = backtrace
10
13
  end
11
14
 
12
15
  attr_reader :frames
13
16
 
14
- def self.build(builder, backtrace)
17
+ def self.build(config, backtrace)
15
18
  return nil unless backtrace
16
19
 
17
20
  stack = new(backtrace)
18
- stack.build_frames(builder)
21
+ stack.build_frames(config)
19
22
  stack
20
23
  end
21
24
 
22
- def build_frames(builder)
23
- @frames = @backtrace.reverse.map do |line|
24
- build_frame(builder, line)
25
+ def build_frames(config)
26
+ @frames = @backtrace.map do |line|
27
+ build_frame(config, line)
25
28
  end
26
29
  end
27
30
 
@@ -53,7 +56,7 @@ module ElasticAPM
53
56
  [file, number, method, module_name]
54
57
  end
55
58
 
56
- def build_frame(_builder, line)
59
+ def build_frame(config, line)
57
60
  abs_path, lineno, function, _module_name = parse_line(line)
58
61
 
59
62
  frame = Frame.new
@@ -62,6 +65,8 @@ module ElasticAPM
62
65
  frame.function = function
63
66
  frame.lineno = lineno.to_i
64
67
  frame.build_context 3
68
+ frame.library_frame =
69
+ !(abs_path && abs_path.start_with?(config.root_path))
65
70
 
66
71
  frame
67
72
  end
@@ -14,30 +14,32 @@ module ElasticAPM
14
14
  :pre_context,
15
15
  :context_line,
16
16
  :post_context,
17
- :in_app,
17
+ :library_frame,
18
18
  :lineno,
19
19
  :module,
20
20
  :colno
21
21
  )
22
22
 
23
- # rubocop:disable Metrics/AbcSize
24
23
  def build_context(context_line_count)
25
24
  return unless abs_path
26
25
 
27
- file_lines = [nil] + read_lines(abs_path)
26
+ from = (lineno - context_line_count - 1)
27
+ to = (lineno + context_line_count)
28
+ file_lines = read_lines(abs_path, from..to)
28
29
 
29
- self.context_line = file_lines[lineno]
30
- self.pre_context =
31
- file_lines[(lineno - context_line_count - 1)...lineno]
32
- self.post_context =
33
- file_lines[(lineno + 1)..(lineno + context_line_count)]
30
+ self.context_line = file_lines[context_line_count]
31
+ self.pre_context = file_lines.first(context_line_count)
32
+ self.post_context = file_lines.last(context_line_count)
34
33
  end
35
- # rubocop:enable Metrics/AbcSize
36
34
 
37
35
  private
38
36
 
39
- def read_lines(path)
40
- File.readlines(path)
37
+ def read_lines(path, range)
38
+ if (cached = LineCache.get(path, range))
39
+ return cached
40
+ end
41
+
42
+ LineCache.set(path, range, File.readlines(path)[range])
41
43
  rescue Errno::ENOENT
42
44
  []
43
45
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elastic_apm/util/lru_cache'
4
+
5
+ module ElasticAPM
6
+ class Stacktrace
7
+ # A basic LRU Cache
8
+ # @api private
9
+ class LineCache
10
+ class << self
11
+ def cache
12
+ @cache ||= Util::LruCache.new
13
+ end
14
+
15
+ def get(*key)
16
+ cache[key]
17
+ end
18
+
19
+ def set(*key, value)
20
+ cache[key] = value
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -8,14 +8,12 @@ module ElasticAPM
8
8
  class Subscriber
9
9
  include Log
10
10
 
11
- def initialize(agent)
11
+ def initialize(config, agent: ElasticAPM)
12
+ @config = config
12
13
  @agent = agent
13
- @config = agent.config
14
14
  @normalizers = Normalizers.build(config)
15
15
  end
16
16
 
17
- attr_reader :config
18
-
19
17
  def register!
20
18
  unregister! if @subscription
21
19
 
@@ -43,7 +41,7 @@ module ElasticAPM
43
41
  nil
44
42
  else
45
43
  name, type, context = normalized
46
- transaction.span(name, type, context: context)
44
+ @agent.span(name, type, context: context)
47
45
  end
48
46
 
49
47
  transaction.notifications << Notification.new(id, span)
@@ -3,11 +3,7 @@
3
3
  module ElasticAPM
4
4
  # @api private
5
5
  class SystemInfo
6
- def initialize(config)
7
- @config = config
8
- end
9
-
10
- attr_reader :config
6
+ def initialize(_config); end
11
7
 
12
8
  def build
13
9
  {
@@ -28,7 +28,7 @@ module ElasticAPM
28
28
 
29
29
  attr_accessor :id, :name, :result, :type
30
30
  attr_reader :context, :duration, :root_span, :timestamp, :spans,
31
- :notifications, :sampled
31
+ :notifications, :sampled, :instrumenter
32
32
 
33
33
  def release
34
34
  @instrumenter.current_transaction = nil
@@ -61,9 +61,14 @@ module ElasticAPM
61
61
  spans.select(&:running?)
62
62
  end
63
63
 
64
- def span(name, type = nil, context: nil)
64
+ # rubocop:disable Metrics/MethodLength
65
+ def span(name, type = nil, backtrace: nil, context: nil)
65
66
  span = next_span(name, type, context)
66
67
  spans << span
68
+
69
+ span.stacktrace =
70
+ backtrace && Stacktrace.build(@instrumenter.config, backtrace)
71
+
67
72
  span.start
68
73
 
69
74
  return span unless block_given?
@@ -76,6 +81,7 @@ module ElasticAPM
76
81
 
77
82
  result
78
83
  end
84
+ # rubocop:enable Metrics/MethodLength
79
85
 
80
86
  def current_span
81
87
  spans.reverse.lazy.find(&:running?)
@@ -0,0 +1,31 @@
1
+ # Monkeypatch/backport/polyfill Enumerable#dig to Ruby < 2.3
2
+ #
3
+ # Implementation from
4
+ # https://github.com/Invoca/ruby_dig/blob/master/lib/ruby_dig.rb
5
+
6
+ # @api private
7
+ module RubyDig
8
+ def dig(key, *rest)
9
+ value = self[key]
10
+
11
+ if value.nil? || rest.empty?
12
+ value
13
+ elsif value.respond_to?(:dig)
14
+ value.dig(*rest)
15
+ else
16
+ raise TypeError, "#{value.class} does not respond to `#dig'"
17
+ end
18
+ end
19
+ end
20
+
21
+ if RUBY_VERSION < '2.3'
22
+ # @api private
23
+ class Array
24
+ include RubyDig
25
+ end
26
+
27
+ # @api private
28
+ class Hash
29
+ include RubyDig
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ module Util
5
+ # @api private
6
+ class LruCache
7
+ def initialize(max_size = 512)
8
+ @max_size = max_size
9
+ @data = {}
10
+ end
11
+
12
+ def [](key)
13
+ found = true
14
+ value = @data.delete(key) { found = false }
15
+
16
+ found ? @data[key] = value : nil
17
+ end
18
+
19
+ def []=(key, val)
20
+ @data.delete(key)
21
+ @data[key] = val
22
+
23
+ return unless @data.length > @max_size
24
+
25
+ @data.delete(@data.first[0])
26
+ end
27
+
28
+ def length
29
+ @data.length
30
+ end
31
+
32
+ def to_a
33
+ @data.to_a
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ElasticAPM
4
- VERSION = '0.2.0'.freeze
4
+ VERSION = '0.3.0'.freeze
5
5
  end
@@ -16,14 +16,12 @@ module ElasticAPM
16
16
  end
17
17
  end
18
18
 
19
- def initialize(config, queue, http: Http)
19
+ def initialize(config, queue, adapter)
20
20
  @config = config
21
- @adapter = http.new(config)
21
+ @adapter = adapter
22
22
  @queue = queue
23
23
  end
24
24
 
25
- attr_reader :config
26
-
27
25
  def run_forever
28
26
  loop do
29
27
  while (item = @queue.pop)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic-apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikkel Malmberg
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-10 00:00:00.000000000 Z
11
+ date: 2018-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -66,6 +66,9 @@ files:
66
66
  - lib/elastic_apm/error/exception.rb
67
67
  - lib/elastic_apm/error/log.rb
68
68
  - lib/elastic_apm/error_builder.rb
69
+ - lib/elastic_apm/filters.rb
70
+ - lib/elastic_apm/filters/request_body_filter.rb
71
+ - lib/elastic_apm/filters/secrets_filter.rb
69
72
  - lib/elastic_apm/http.rb
70
73
  - lib/elastic_apm/injectors.rb
71
74
  - lib/elastic_apm/injectors/action_dispatch.rb
@@ -96,11 +99,14 @@ files:
96
99
  - lib/elastic_apm/sql_summarizer.rb
97
100
  - lib/elastic_apm/stacktrace.rb
98
101
  - lib/elastic_apm/stacktrace/frame.rb
102
+ - lib/elastic_apm/stacktrace/line_cache.rb
99
103
  - lib/elastic_apm/subscriber.rb
100
104
  - lib/elastic_apm/system_info.rb
101
105
  - lib/elastic_apm/transaction.rb
102
106
  - lib/elastic_apm/util.rb
107
+ - lib/elastic_apm/util/dig.rb
103
108
  - lib/elastic_apm/util/inspector.rb
109
+ - lib/elastic_apm/util/lru_cache.rb
104
110
  - lib/elastic_apm/version.rb
105
111
  - lib/elastic_apm/worker.rb
106
112
  - vendor/.gitkeep