appsignal 1.2.5 → 1.3.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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