appsignal 1.2.5 → 1.3.0.beta.1

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/circle.yml +4 -0
  4. data/ext/agent.yml +11 -11
  5. data/ext/appsignal_extension.c +105 -40
  6. data/lib/appsignal.rb +18 -6
  7. data/lib/appsignal/cli/install.rb +3 -3
  8. data/lib/appsignal/config.rb +19 -5
  9. data/lib/appsignal/event_formatter.rb +3 -2
  10. data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +1 -1
  11. data/lib/appsignal/event_formatter/mongo_ruby_driver/query_formatter.rb +1 -1
  12. data/lib/appsignal/event_formatter/moped/query_formatter.rb +13 -6
  13. data/lib/appsignal/hooks/mongo_ruby_driver.rb +0 -1
  14. data/lib/appsignal/hooks/net_http.rb +2 -5
  15. data/lib/appsignal/hooks/redis.rb +1 -1
  16. data/lib/appsignal/hooks/sequel.rb +8 -4
  17. data/lib/appsignal/integrations/mongo_ruby_driver.rb +1 -1
  18. data/lib/appsignal/integrations/object.rb +35 -0
  19. data/lib/appsignal/integrations/resque.rb +5 -0
  20. data/lib/appsignal/integrations/sinatra.rb +2 -2
  21. data/lib/appsignal/minutely.rb +41 -0
  22. data/lib/appsignal/params_sanitizer.rb +4 -105
  23. data/lib/appsignal/rack/sinatra_instrumentation.rb +25 -2
  24. data/lib/appsignal/transaction.rb +41 -15
  25. data/lib/appsignal/transmitter.rb +1 -1
  26. data/lib/appsignal/utils.rb +42 -47
  27. data/lib/appsignal/utils/params_sanitizer.rb +58 -0
  28. data/lib/appsignal/utils/query_params_sanitizer.rb +54 -0
  29. data/lib/appsignal/version.rb +1 -1
  30. data/spec/lib/appsignal/config_spec.rb +12 -2
  31. data/spec/lib/appsignal/extension_spec.rb +4 -0
  32. data/spec/lib/appsignal/hooks/net_http_spec.rb +20 -28
  33. data/spec/lib/appsignal/hooks/redis_spec.rb +9 -11
  34. data/spec/lib/appsignal/integrations/object_spec.rb +211 -0
  35. data/spec/lib/appsignal/integrations/sinatra_spec.rb +2 -2
  36. data/spec/lib/appsignal/minutely_spec.rb +54 -0
  37. data/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb +50 -10
  38. data/spec/lib/appsignal/subscriber_spec.rb +5 -6
  39. data/spec/lib/appsignal/transaction_spec.rb +102 -23
  40. data/spec/lib/appsignal/transmitter_spec.rb +1 -1
  41. data/spec/lib/appsignal/utils/params_sanitizer_spec.rb +122 -0
  42. data/spec/lib/appsignal/utils/query_params_sanitizer_spec.rb +194 -0
  43. data/spec/lib/appsignal/utils_spec.rb +13 -76
  44. data/spec/lib/appsignal_spec.rb +82 -13
  45. metadata +15 -11
  46. data/lib/appsignal/event_formatter/net_http/request_formatter.rb +0 -13
  47. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +0 -13
  48. data/spec/lib/appsignal/event_formatter/net_http/request_formatter_spec.rb +0 -26
  49. data/spec/lib/appsignal/event_formatter/sequel/sql_formatter_spec.rb +0 -22
  50. data/spec/lib/appsignal/params_sanitizer_spec.rb +0 -200
@@ -2,7 +2,28 @@ require 'rack'
2
2
 
3
3
  module Appsignal
4
4
  module Rack
5
+ # Stub old middleware. Prevents Sinatra middleware being loaded twice.
6
+ # This can happen when users use the old method of including
7
+ # `use Appsignal::Rack::SinatraInstrumentation` in their modular Sinatra
8
+ # applications. This is no longer needed. Instead Appsignal now includes
9
+ # `use Appsignal::Rack::SinatraBaseInstrumentation` automatically.
5
10
  class SinatraInstrumentation
11
+ def initialize(app, options = {})
12
+ @app, @options = app, options
13
+ Appsignal.logger.warn 'Please remove Appsignal::Rack::SinatraInstrumentation '\
14
+ 'from your Sinatra::Base class. This is no longer needed.'
15
+ end
16
+
17
+ def call(env)
18
+ @app.call(env)
19
+ end
20
+
21
+ def settings
22
+ @app.settings
23
+ end
24
+ end
25
+
26
+ class SinatraBaseInstrumentation
6
27
  attr_reader :raise_errors_on
7
28
 
8
29
  def initialize(app, options = {})
@@ -51,11 +72,13 @@ module Appsignal
51
72
  end
52
73
  end
53
74
 
54
-
55
75
  def action_name(env)
56
76
  if @options.fetch(:mounted_at, nil)
57
77
  method, route = env['sinatra.route'].split(" ")
58
- "#{method} #{@options.fetch(:mounted_at, nil)}#{route}"
78
+ "#{method} #{@options[:mounted_at]}#{route}"
79
+ elsif env['SCRIPT_NAME']
80
+ method, route = env['sinatra.route'].split(" ")
81
+ "#{method} #{env['SCRIPT_NAME']}#{route}"
59
82
  else
60
83
  env['sinatra.route']
61
84
  end
@@ -181,15 +181,32 @@ module Appsignal
181
181
  @ext.start_event
182
182
  end
183
183
 
184
- def finish_event(name, title, body, body_format)
184
+ def finish_event(name, title, body, body_format=Appsignal::EventFormatter::DEFAULT)
185
185
  @ext.finish_event(
186
186
  name,
187
187
  title || BLANK,
188
188
  body || BLANK,
189
- body_format || 0
189
+ body_format || Appsignal::EventFormatter::DEFAULT
190
190
  )
191
191
  end
192
192
 
193
+ def record_event(name, title, body, duration, body_format=Appsignal::EventFormatter::DEFAULT)
194
+ @ext.record_event(
195
+ name,
196
+ title || BLANK,
197
+ body || BLANK,
198
+ duration,
199
+ body_format || Appsignal::EventFormatter::DEFAULT
200
+ )
201
+ end
202
+
203
+ def instrument(name, title=nil, body=nil, body_format=Appsignal::EventFormatter::DEFAULT)
204
+ start_event
205
+ r = yield
206
+ finish_event(name, title, body, body_format)
207
+ r
208
+ end
209
+
193
210
  class GenericRequest
194
211
  attr_reader :env
195
212
 
@@ -231,18 +248,22 @@ module Appsignal
231
248
  def sanitized_params
232
249
  return unless Appsignal.config[:send_params]
233
250
  return unless request.respond_to?(options[:params_method])
234
- begin
235
- return unless params = request.send(options[:params_method])
236
- rescue Exception => ex
237
- # Getting params from the request has been know to fail.
238
- Appsignal.logger.debug "Exception while getting params: #{ex}"
239
- return
240
- end
241
- if params.is_a?(Hash)
242
- Appsignal::ParamsSanitizer.sanitize(params)
243
- elsif params.is_a?(Array)
244
- params
251
+
252
+ params =
253
+ begin
254
+ request.send options[:params_method]
255
+ rescue Exception => ex
256
+ # Getting params from the request has been know to fail.
257
+ Appsignal.logger.debug "Exception while getting params: #{ex}"
258
+ nil
259
+ end
260
+ return unless params
261
+
262
+ options = {}
263
+ if Appsignal.config[:filter_parameters]
264
+ options[:filter_parameters] = Appsignal.config[:filter_parameters]
245
265
  end
266
+ Appsignal::Utils::ParamsSanitizer.sanitize params, options
246
267
  end
247
268
 
248
269
  def sanitized_environment
@@ -257,7 +278,7 @@ module Appsignal
257
278
  def sanitized_session_data
258
279
  return if Appsignal.config[:skip_session_data] || !request.respond_to?(:session)
259
280
  return unless session = request.session
260
- Appsignal::ParamsSanitizer.sanitize(session.to_hash)
281
+ Appsignal::Utils::ParamsSanitizer.sanitize(session.to_hash)
261
282
  end
262
283
 
263
284
  def metadata
@@ -272,7 +293,7 @@ module Appsignal
272
293
  def sanitized_tags
273
294
  @tags.select do |k, v|
274
295
  (k.is_a?(Symbol) || k.is_a?(String) && k.length <= 100) &&
275
- (((v.is_a?(Symbol) || v.is_a?(String)) && v.length <= 100) || (v.is_a?(Integer)))
296
+ (((v.is_a?(Symbol) || v.is_a?(String)) && v.length <= 100) || (v.is_a?(Integer)))
276
297
  end
277
298
  end
278
299
 
@@ -290,6 +311,11 @@ module Appsignal
290
311
  def method_missing(m, *args, &block)
291
312
  end
292
313
 
314
+ # Instrument should still yield
315
+ def instrument(*args)
316
+ yield
317
+ end
318
+
293
319
  def nil_transaction?
294
320
  true
295
321
  end
@@ -35,7 +35,7 @@ module Appsignal
35
35
  :api_key => config[:push_api_key],
36
36
  :name => config[:name],
37
37
  :environment => config.env,
38
- :hostname => Socket.gethostname,
38
+ :hostname => config[:hostname],
39
39
  :gem_version => Appsignal::VERSION
40
40
  })
41
41
  end
@@ -1,61 +1,56 @@
1
+ require 'appsignal/utils/params_sanitizer'
2
+ require 'appsignal/utils/query_params_sanitizer'
3
+
1
4
  module Appsignal
2
5
  module Utils
3
- def self.sanitize(params, only_top_level=false, key_sanitizer=nil)
4
- if params.is_a?(Hash)
5
- {}.tap do |hsh|
6
- params.each do |key, val|
7
- hsh[self.sanitize_key(key, key_sanitizer)] = if only_top_level
8
- '?'
9
- else
10
- sanitize(val, only_top_level, key_sanitizer=nil)
11
- end
12
- end
13
- end
14
- elsif params.is_a?(Array)
15
- if only_top_level
16
- sanitize(params[0], only_top_level, key_sanitizer=nil)
17
- else
18
- params.map do |item|
19
- sanitize(item, only_top_level, key_sanitizer=nil)
20
- end.uniq
21
- end
22
- else
23
- '?'
24
- end
25
- end
6
+ module ClassMethods
7
+ extend Gem::Deprecate
26
8
 
27
- def self.sanitize_key(key, sanitizer)
28
- case sanitizer
29
- when :mongodb then key.to_s.gsub(/(\..+)/, '.?')
30
- else key
9
+ def sanitize(params, only_top_level = false, key_sanitizer = nil)
10
+ QueryParamsSanitizer.sanitize(params, only_top_level, key_sanitizer)
31
11
  end
12
+
13
+ deprecate :sanitize, "AppSignal::Utils::QueryParamsSanitizer.sanitize", 2016, 9
32
14
  end
15
+ extend ClassMethods
33
16
 
34
17
  def self.json_generate(body)
35
- JSON.generate(jsonify(body))
18
+ JSON.generate(body)
36
19
  end
37
20
 
38
- def self.jsonify(value)
39
- case value
40
- when String
41
- encode_utf8(value)
42
- when Numeric, NilClass, TrueClass, FalseClass
43
- value
44
- when Hash
45
- Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
46
- when Array
47
- value.map { |v| jsonify(v) }
48
- else
49
- jsonify(value.to_s)
21
+ class JSON
22
+ module ClassMethods
23
+ def generate(body)
24
+ ::JSON.generate(jsonify(body))
25
+ end
26
+
27
+ private
28
+
29
+ def jsonify(value)
30
+ case value
31
+ when String
32
+ encode_utf8(value)
33
+ when Numeric, NilClass, TrueClass, FalseClass
34
+ value
35
+ when Hash
36
+ Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
37
+ when Array
38
+ value.map { |v| jsonify(v) }
39
+ else
40
+ jsonify(value.to_s)
41
+ end
42
+ end
43
+
44
+ def encode_utf8(value)
45
+ value.encode(
46
+ 'utf-8'.freeze,
47
+ :invalid => :replace,
48
+ :undef => :replace
49
+ )
50
+ end
50
51
  end
51
- end
52
52
 
53
- def self.encode_utf8(value)
54
- value.encode(
55
- 'utf-8'.freeze,
56
- :invalid => :replace,
57
- :undef => :replace
58
- )
53
+ extend ClassMethods
59
54
  end
60
55
  end
61
56
  end
@@ -0,0 +1,58 @@
1
+ module Appsignal
2
+ module Utils
3
+ class ParamsSanitizer
4
+ FILTERED = '[FILTERED]'.freeze
5
+
6
+ class << self
7
+ def sanitize(params, options = {})
8
+ sanitize_value(params, options)
9
+ end
10
+
11
+ private
12
+
13
+ def sanitize_value(value, options = {})
14
+ case value
15
+ when Hash
16
+ sanitize_hash(value, options)
17
+ when Array
18
+ sanitize_array(value, options)
19
+ when TrueClass, FalseClass, NilClass, Fixnum, String, Symbol, Float
20
+ unmodified(value)
21
+ else
22
+ inspected(value)
23
+ end
24
+ end
25
+
26
+ def sanitize_hash(source, options)
27
+ filter_keys = options.fetch(:filter_parameters, [])
28
+ {}.tap do |hash|
29
+ source.each_pair do |key, value|
30
+ hash[key] =
31
+ if filter_keys.include?(key.to_s)
32
+ FILTERED
33
+ else
34
+ sanitize_value(value, options)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def sanitize_array(source, options)
41
+ [].tap do |array|
42
+ source.each_with_index do |item, index|
43
+ array[index] = sanitize_value(item, options)
44
+ end
45
+ end
46
+ end
47
+
48
+ def unmodified(value)
49
+ value
50
+ end
51
+
52
+ def inspected(value)
53
+ "#<#{value.class}>"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,54 @@
1
+ module Appsignal
2
+ module Utils
3
+ class QueryParamsSanitizer
4
+ REPLACEMENT_KEY = '?'.freeze
5
+
6
+ module ClassMethods
7
+ def sanitize(params, only_top_level = false, key_sanitizer = nil)
8
+ case params
9
+ when Hash
10
+ sanitize_hash params, only_top_level, key_sanitizer
11
+ when Array
12
+ sanitize_array params, only_top_level, key_sanitizer
13
+ else
14
+ REPLACEMENT_KEY
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def sanitize_hash(hash, only_top_level, key_sanitizer)
21
+ {}.tap do |h|
22
+ hash.each do |key, value|
23
+ h[sanitize_key(key, key_sanitizer)] =
24
+ if only_top_level
25
+ REPLACEMENT_KEY
26
+ else
27
+ sanitize(value, only_top_level, key_sanitizer)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def sanitize_array(array, only_top_level, key_sanitizer)
34
+ if only_top_level
35
+ sanitize(array[0], only_top_level, key_sanitizer)
36
+ else
37
+ array.map do |value|
38
+ sanitize(value, only_top_level, key_sanitizer)
39
+ end.uniq
40
+ end
41
+ end
42
+
43
+ def sanitize_key(key, sanitizer)
44
+ case sanitizer
45
+ when :mongodb then key.to_s.gsub(/(\..+)/, ".#{REPLACEMENT_KEY}")
46
+ else key
47
+ end
48
+ end
49
+ end
50
+
51
+ extend ClassMethods
52
+ end
53
+ end
54
+ end
@@ -1,5 +1,5 @@
1
1
  require 'yaml'
2
2
 
3
3
  module Appsignal
4
- VERSION = '1.2.5'
4
+ VERSION = '1.3.0.beta.1'
5
5
  end
@@ -20,6 +20,7 @@ describe Appsignal::Config do
20
20
  :debug => false,
21
21
  :ignore_errors => [],
22
22
  :ignore_actions => [],
23
+ :filter_parameters => [],
23
24
  :instrument_net_http => true,
24
25
  :instrument_redis => true,
25
26
  :instrument_sequel => true,
@@ -34,7 +35,9 @@ describe Appsignal::Config do
34
35
  :enable_allocation_tracking => true,
35
36
  :enable_gc_instrumentation => false,
36
37
  :running_in_container => false,
37
- :enable_host_metrics => false
38
+ :enable_host_metrics => true,
39
+ :enable_minutely_probes => false,
40
+ :hostname => Socket.gethostname
38
41
  }
39
42
  end
40
43
 
@@ -102,6 +105,8 @@ describe Appsignal::Config do
102
105
  subject.config_hash[:http_proxy] = 'http://localhost'
103
106
  subject.config_hash[:ignore_actions] = ['action1', 'action2']
104
107
  subject.config_hash[:log_path] = '/tmp'
108
+ subject.config_hash[:hostname] = 'app1.local'
109
+ subject.config_hash[:filter_parameters] = %w(password confirm_password)
105
110
  subject.write_to_environment
106
111
  end
107
112
 
@@ -119,9 +124,14 @@ describe Appsignal::Config do
119
124
  ENV['APPSIGNAL_LANGUAGE_INTEGRATION_VERSION'].should == Appsignal::VERSION
120
125
  ENV['APPSIGNAL_HTTP_PROXY'].should == 'http://localhost'
121
126
  ENV['APPSIGNAL_IGNORE_ACTIONS'].should == 'action1,action2'
127
+ ENV['APPSIGNAL_FILTER_PARAMETERS'].should == 'password,confirm_password'
128
+ ENV['APPSIGNAL_SEND_PARAMS'].should == 'true'
122
129
  ENV['APPSIGNAL_RUNNING_IN_CONTAINER'].should == 'false'
123
130
  ENV['APPSIGNAL_WORKING_DIR_PATH'].should be_nil
124
- ENV['APPSIGNAL_ENABLE_HOST_METRICS'].should == 'false'
131
+ ENV['APPSIGNAL_ENABLE_HOST_METRICS'].should == 'true'
132
+ ENV['APPSIGNAL_ENABLE_MINUTELY_PROBES'].should == 'false'
133
+ ENV['APPSIGNAL_HOSTNAME'].should == 'app1.local'
134
+ ENV['APPSIGNAL_PROCESS_NAME'].should include 'rspec'
125
135
  end
126
136
 
127
137
  context "if working_dir_path is set" do
@@ -48,6 +48,10 @@ describe "extension loading and operation" do
48
48
  subject.finish_event('name', 'title', 'body', 0)
49
49
  end
50
50
 
51
+ it "should have a record_event method" do
52
+ subject.record_event('name', 'title', 'body', 0, 1000)
53
+ end
54
+
51
55
  it "should have a set_error method" do
52
56
  subject.set_error('name', 'message', '[backtrace]')
53
57
  end
@@ -2,54 +2,46 @@ require 'spec_helper'
2
2
 
3
3
  describe Appsignal::Hooks::NetHttpHook do
4
4
  before :all do
5
- Appsignal.config = project_fixture_config
5
+ start_agent
6
6
  end
7
7
 
8
8
  context "with Net::HTTP instrumentation enabled" do
9
- let(:events) { [] }
10
- before :all do
11
- Appsignal.config.config_hash[:instrument_net_http] = true
12
- end
13
- before do
14
- ActiveSupport::Notifications.subscribe(/^[^!]/) do |*args|
15
- events << ActiveSupport::Notifications::Event.new(*args)
16
- end
17
- end
18
- after(:all) { Appsignal.config.config_hash[:instrument_net_http] = false }
19
-
20
9
  its(:dependencies_present?) { should be_true }
21
10
 
22
- it "should generate an event for a http request" do
11
+ it "should instrument a http request" do
12
+ Appsignal::Transaction.create('uuid', Appsignal::Transaction::HTTP_REQUEST, 'test')
13
+ expect( Appsignal::Transaction.current ).to receive(:start_event)
14
+ .at_least(:once)
15
+ expect( Appsignal::Transaction.current ).to receive(:finish_event)
16
+ .at_least(:once)
17
+ .with("request.net_http", "GET http://www.google.com", nil, 0)
18
+
23
19
  stub_request(:any, 'http://www.google.com/')
24
20
 
25
21
  Net::HTTP.get_response(URI.parse('http://www.google.com'))
26
-
27
- event = events.last
28
- event.name.should == 'request.net_http'
29
- event.payload[:protocol].should == 'http'
30
- event.payload[:domain].should == 'www.google.com'
31
- event.payload[:path].should == '/'
32
- event.payload[:method].should == 'GET'
33
22
  end
34
23
 
35
- it "should generate an event for a https request" do
24
+ it "should instrument a https request" do
25
+ Appsignal::Transaction.create('uuid', Appsignal::Transaction::HTTP_REQUEST, 'test')
26
+ expect( Appsignal::Transaction.current ).to receive(:start_event)
27
+ .at_least(:once)
28
+ expect( Appsignal::Transaction.current ).to receive(:finish_event)
29
+ .at_least(:once)
30
+ .with("request.net_http", "GET https://www.google.com", nil, 0)
31
+
36
32
  stub_request(:any, 'https://www.google.com/')
37
33
 
38
34
  uri = URI.parse('https://www.google.com')
39
35
  http = Net::HTTP.new(uri.host, uri.port)
40
36
  http.use_ssl = true
41
37
  http.get(uri.request_uri)
42
-
43
- event = events.last
44
- event.name.should == 'request.net_http'
45
- event.payload[:protocol].should == 'https'
46
- event.payload[:domain].should == 'www.google.com'
47
- event.payload[:path].should == '/'
48
- event.payload[:method].should == 'GET'
49
38
  end
50
39
  end
51
40
 
52
41
  context "with Net::HTTP instrumentation disabled" do
42
+ before { Appsignal.config.config_hash[:instrument_net_http] = false }
43
+ after { Appsignal.config.config_hash[:instrument_net_http] = true }
44
+
53
45
  its(:dependencies_present?) { should be_false }
54
46
  end
55
47
  end