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

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