newrelic_rpm 3.3.5 → 3.4.0.beta1

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

Potentially problematic release.


This version of newrelic_rpm might be problematic. Click here for more details.

Files changed (41) hide show
  1. data/CHANGELOG +8 -5
  2. data/lib/new_relic/agent.rb +11 -3
  3. data/lib/new_relic/agent/agent.rb +68 -223
  4. data/lib/new_relic/agent/error_collector.rb +1 -1
  5. data/lib/new_relic/agent/instrumentation/resque.rb +80 -0
  6. data/lib/new_relic/agent/instrumentation/sinatra.rb +2 -0
  7. data/lib/new_relic/agent/new_relic_service.rb +221 -0
  8. data/lib/new_relic/agent/pipe_channel_manager.rb +151 -0
  9. data/lib/new_relic/agent/pipe_service.rb +57 -0
  10. data/lib/new_relic/agent/samplers/delayed_job_sampler.rb +4 -34
  11. data/lib/new_relic/agent/sql_sampler.rb +1 -5
  12. data/lib/new_relic/agent/stats_engine/transactions.rb +2 -2
  13. data/lib/new_relic/agent/transaction_sampler.rb +0 -2
  14. data/lib/new_relic/control/configuration.rb +1 -1
  15. data/lib/new_relic/control/instance_methods.rb +2 -1
  16. data/lib/new_relic/language_support.rb +26 -0
  17. data/lib/new_relic/transaction_sample.rb +2 -6
  18. data/lib/new_relic/version.rb +3 -3
  19. data/newrelic_rpm.gemspec +16 -5
  20. data/test/new_relic/agent/agent/connect_test.rb +39 -23
  21. data/test/new_relic/agent/agent_test.rb +25 -4
  22. data/test/new_relic/agent/database_test.rb +12 -0
  23. data/test/new_relic/agent/new_relic_service_test.rb +151 -0
  24. data/test/new_relic/agent/pipe_channel_manager_test.rb +114 -0
  25. data/test/new_relic/agent/pipe_service_test.rb +113 -0
  26. data/test/new_relic/agent/rpm_agent_test.rb +3 -30
  27. data/test/new_relic/agent/transaction_sample_builder_test.rb +0 -1
  28. data/test/new_relic/agent/transaction_sampler_test.rb +6 -6
  29. data/test/new_relic/agent/worker_loop_test.rb +2 -2
  30. data/test/new_relic/agent_test.rb +73 -3
  31. data/test/new_relic/control/configuration_test.rb +0 -7
  32. data/test/new_relic/control_test.rb +3 -3
  33. data/test/new_relic/delayed_job_injection_test.rb +6 -1
  34. data/test/new_relic/fake_collector.rb +210 -0
  35. data/test/new_relic/fake_service.rb +44 -0
  36. data/test/script/ci.sh +6 -6
  37. data/test/script/ci_agent-tests_runner.sh +82 -0
  38. data/test/script/ci_multiverse_runner.sh +35 -0
  39. data/test/test_contexts.rb +1 -0
  40. data/test/test_helper.rb +3 -1
  41. metadata +359 -324
@@ -16,7 +16,7 @@ module NewRelic
16
16
  # memory and data retention
17
17
  MAX_ERROR_QUEUE_LENGTH = 20 unless defined? MAX_ERROR_QUEUE_LENGTH
18
18
 
19
- attr_accessor :enabled
19
+ attr_accessor :enabled, :errors
20
20
  attr_reader :config_enabled
21
21
 
22
22
  # Returns a new error collector
@@ -0,0 +1,80 @@
1
+ DependencyDetection.defer do
2
+ @name = :resque
3
+
4
+ depends_on do
5
+ defined?(::Resque::Job) && !NewRelic::Control.instance['disable_resque'] &&
6
+ !NewRelic::LanguageSupport.using_version?('1.9.1')
7
+ end
8
+
9
+ executes do
10
+ NewRelic::Agent.logger.debug 'Installing Resque instrumentation'
11
+ end
12
+
13
+ executes do
14
+ # == Resque Instrumentation
15
+ #
16
+ # Installs a hook to ensure the agent starts manually when the worker
17
+ # starts and also adds the tracer to the process method which executes
18
+ # in the forked task.
19
+
20
+ module Resque
21
+ module Plugins
22
+ module NewRelicInstrumentation
23
+ include NewRelic::Agent::Instrumentation::ControllerInstrumentation
24
+
25
+ def around_perform_with_monitoring(*args)
26
+ begin
27
+ perform_action_with_newrelic_trace(:name => 'perform',
28
+ :class_name => self.name,
29
+ :category => 'OtherTransaction/ResqueJob') do
30
+ yield(*args)
31
+ end
32
+ ensure
33
+ NewRelic::Agent.shutdown if NewRelic::LanguageSupport.can_fork?
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ module NewRelic
41
+ module Agent
42
+ module Instrumentation
43
+ module ResqueInstrumentationInstaller
44
+ def payload_class
45
+ klass = super
46
+ klass.instance_eval do
47
+ extend ::Resque::Plugins::NewRelicInstrumentation
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ ::Resque::Job.class_eval do
56
+ def self.new(*args)
57
+ super(*args).extend NewRelic::Agent::Instrumentation::ResqueInstrumentationInstaller
58
+ end
59
+ end
60
+
61
+ if NewRelic::LanguageSupport.can_fork?
62
+ ::Resque.before_first_fork do
63
+ NewRelic::Agent.manual_start(:dispatcher => :resque,
64
+ :sync_startup => true,
65
+ :start_channel_listener => true)
66
+ end
67
+
68
+ ::Resque.before_fork do |job|
69
+ NewRelic::Agent.register_report_channel(job.object_id)
70
+ end
71
+
72
+ ::Resque.after_fork do |job|
73
+ NewRelic::Agent.after_fork(:report_to_channel => job.object_id)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ # call this now so it is memoized before potentially forking worker processes
80
+ NewRelic::LanguageSupport.can_fork?
@@ -1,6 +1,8 @@
1
1
  require 'new_relic/agent/instrumentation/controller_instrumentation'
2
2
 
3
3
  DependencyDetection.defer do
4
+ @name = :sinatra
5
+
4
6
  depends_on do
5
7
  defined?(::Sinatra) && defined?(::Sinatra::Base) &&
6
8
  Sinatra::Base.private_method_defined?(:dispatch!)
@@ -0,0 +1,221 @@
1
+ module NewRelic
2
+ module Agent
3
+ class NewRelicService
4
+ # Specifies the version of the agent's communication protocol with
5
+ # the NewRelic hosted site.
6
+
7
+ PROTOCOL_VERSION = 8
8
+ # 14105: v8 (tag 2.10.3)
9
+ # (no v7)
10
+ # 10379: v6 (not tagged)
11
+ # 4078: v5 (tag 2.5.4)
12
+ # 2292: v4 (tag 2.3.6)
13
+ # 1754: v3 (tag 2.3.0)
14
+ # 534: v2 (shows up in 2.1.0, our first tag)
15
+
16
+ attr_accessor :request_timeout
17
+ attr_reader :collector
18
+ attr_accessor :agent_id
19
+
20
+ def initialize(license_key=control.license_key, collector=control.server)
21
+ @license_key = license_key
22
+ @collector = collector
23
+ @request_timeout = NewRelic::Control.instance.fetch('timeout', 2 * 60)
24
+ end
25
+
26
+ def connect(settings={})
27
+ host = get_redirect_host
28
+ @collector.name = host if host
29
+ response = invoke_remote(:connect, settings)
30
+ @agent_id = response['agent_run_id']
31
+ response
32
+ end
33
+
34
+ def get_redirect_host
35
+ invoke_remote(:get_redirect_host)
36
+ end
37
+
38
+ def shutdown(time)
39
+ invoke_remote(:shutdown, @agent_id, time) if @agent_id
40
+ end
41
+
42
+ def metric_data(last_harvest_time, now, unsent_timeslice_data)
43
+ invoke_remote(:metric_data, @agent_id, last_harvest_time, now,
44
+ unsent_timeslice_data)
45
+ end
46
+
47
+ def error_data(unsent_errors)
48
+ invoke_remote(:error_data, @agent_id, unsent_errors)
49
+ end
50
+
51
+ def transaction_sample_data(traces)
52
+ invoke_remote(:transaction_sample_data, @agent_id, traces)
53
+ end
54
+
55
+ def sql_trace_data(sql_traces)
56
+ invoke_remote(:sql_trace_data, sql_traces)
57
+ end
58
+
59
+ private
60
+
61
+ # A shorthand for NewRelic::Control.instance
62
+ def control
63
+ NewRelic::Control.instance
64
+ end
65
+
66
+ # Shorthand to the NewRelic::Agent.logger method
67
+ def log
68
+ NewRelic::Agent.logger
69
+ end
70
+
71
+ # The path on the server that we should post our data to
72
+ def remote_method_uri(method)
73
+ uri = "/agent_listener/#{PROTOCOL_VERSION}/#{@license_key}/#{method}"
74
+ uri << "?run_id=#{@agent_id}" if @agent_id
75
+ uri
76
+ end
77
+
78
+ # send a message via post to the actual server. This attempts
79
+ # to automatically compress the data via zlib if it is large
80
+ # enough to be worth compressing, and handles any errors the
81
+ # server may return
82
+ def invoke_remote(method, *args)
83
+ now = Time.now
84
+ #determines whether to zip the data or send plain
85
+ post_data, encoding = compress_data(args)
86
+
87
+ response = send_request(:uri => remote_method_uri(method),
88
+ :encoding => encoding,
89
+ :collector => @collector,
90
+ :data => post_data)
91
+
92
+ # raises the right exception if the remote server tells it to die
93
+ return check_for_exception(response)
94
+ rescue NewRelic::Agent::ForceRestartException => e
95
+ log.info e.message
96
+ raise
97
+ rescue SystemCallError, SocketError => e
98
+ # These include Errno connection errors
99
+ raise NewRelic::Agent::ServerConnectionException, "Recoverable error connecting to the server: #{e}"
100
+ ensure
101
+ NewRelic::Agent.instance.stats_engine.get_stats_no_scope('Supportability/invoke_remote').record_data_point((Time.now - now).to_f)
102
+ NewRelic::Agent.instance.stats_engine.get_stats_no_scope('Supportability/invoke_remote/' + method.to_s).record_data_point((Time.now - now).to_f)
103
+ end
104
+
105
+ # This method handles the compression of the request body that
106
+ # we are going to send to the server
107
+ #
108
+ # We currently optimize for CPU here since we get roughly a 10x
109
+ # reduction in message size with this, and CPU overhead is at a
110
+ # premium. For extra-large posts, we use the higher compression
111
+ # since otherwise it actually errors out.
112
+ #
113
+ # We do not compress if content is smaller than 64kb. There are
114
+ # problems with bugs in Ruby in some versions that expose us
115
+ # to a risk of segfaults if we compress aggressively.
116
+ #
117
+ # medium payloads get fast compression, to save CPU
118
+ # big payloads get all the compression possible, to stay under
119
+ # the 2,000,000 byte post threshold
120
+ def compress_data(object)
121
+ dump = Marshal.dump(object)
122
+
123
+ dump_size = dump.size
124
+
125
+ return [dump, 'identity'] if dump_size < (64*1024)
126
+
127
+ compressed_dump = Zlib::Deflate.deflate(dump, Zlib::DEFAULT_COMPRESSION)
128
+
129
+ # this checks to make sure mongrel won't choke on big uploads
130
+ check_post_size(compressed_dump)
131
+
132
+ [compressed_dump, 'deflate']
133
+ end
134
+
135
+ # Raises a PostTooBigException if the post_string is longer
136
+ # than the limit configured in the control object
137
+ def check_post_size(post_string)
138
+ # TODO: define this as a config option on the server side
139
+ return if post_string.size < control.post_size_limit
140
+ log.warn "Tried to send too much data: #{post_string.size} bytes"
141
+ raise PostTooBigException
142
+ end
143
+
144
+ # Posts to the specified server
145
+ #
146
+ # Options:
147
+ # - :uri => the path to request on the server (a misnomer of
148
+ # course)
149
+ # - :encoding => the encoding to pass to the server
150
+ # - :collector => a URI object that responds to the 'name' method
151
+ # and returns the name of the collector to
152
+ # contact
153
+ # - :data => the data to send as the body of the request
154
+ def send_request(opts)
155
+ request = Net::HTTP::Post.new(opts[:uri], 'CONTENT-ENCODING' => opts[:encoding], 'HOST' => opts[:collector].name)
156
+ request['user-agent'] = user_agent
157
+ request.content_type = "application/octet-stream"
158
+ request.body = opts[:data]
159
+
160
+ log.debug "Connect to #{opts[:collector]}#{opts[:uri]}"
161
+
162
+ response = nil
163
+ http = control.http_connection(@collector)
164
+ http.read_timeout = nil
165
+ begin
166
+ NewRelic::TimerLib.timeout(@request_timeout) do
167
+ response = http.request(request)
168
+ end
169
+ rescue Timeout::Error
170
+ log.warn "Timed out trying to post data to New Relic (timeout = #{@request_timeout} seconds)" unless @service.request_timeout < 30
171
+ raise
172
+ end
173
+ if response.is_a? Net::HTTPServiceUnavailable
174
+ raise NewRelic::Agent::ServerConnectionException, "Service unavailable (#{response.code}): #{response.message}"
175
+ elsif response.is_a? Net::HTTPGatewayTimeOut
176
+ log.debug("Timed out getting response: #{response.message}")
177
+ raise Timeout::Error, response.message
178
+ elsif response.is_a? Net::HTTPRequestEntityTooLarge
179
+ raise PostTooBigException
180
+ elsif !(response.is_a? Net::HTTPSuccess)
181
+ raise NewRelic::Agent::ServerConnectionException, "Unexpected response from server (#{response.code}): #{response.message}"
182
+ end
183
+ response
184
+ end
185
+
186
+ # Decompresses the response from the server, if it is gzip
187
+ # encoded, otherwise returns it verbatim
188
+ def decompress_response(response)
189
+ if response['content-encoding'] != 'gzip'
190
+ log.debug "Uncompressed content returned"
191
+ return response.body
192
+ end
193
+ log.debug "Decompressing return value"
194
+ i = Zlib::GzipReader.new(StringIO.new(response.body))
195
+ i.read
196
+ end
197
+
198
+ # unmarshals the response and raises it if it is an exception,
199
+ # so we can handle it in nonlocally
200
+ def check_for_exception(response)
201
+ dump = decompress_response(response)
202
+ value = Marshal.load(dump)
203
+ raise value if value.is_a? Exception
204
+ value
205
+ end
206
+
207
+ # Sets the user agent for connections to the server, to
208
+ # conform with the HTTP spec and allow for debugging. Includes
209
+ # the ruby version and also zlib version if available since
210
+ # that may cause corrupt compression if there is a problem.
211
+ def user_agent
212
+ ruby_description = ''
213
+ # note the trailing space!
214
+ ruby_description << "(ruby #{::RUBY_VERSION} #{::RUBY_PLATFORM}) " if defined?(::RUBY_VERSION) && defined?(::RUBY_PLATFORM)
215
+ zlib_version = ''
216
+ zlib_version << "zlib/#{Zlib.zlib_version}" if defined?(::Zlib) && Zlib.respond_to?(:zlib_version)
217
+ "NewRelic-RubyAgent/#{NewRelic::VERSION::STRING} #{ruby_description}#{zlib_version}"
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,151 @@
1
+ module NewRelic
2
+ module Agent
3
+ module PipeChannelManager
4
+ extend self
5
+
6
+ def register_report_channel(id)
7
+ listener.register_pipe(id)
8
+ end
9
+
10
+ def channels
11
+ listener.pipes
12
+ end
13
+
14
+ def listener
15
+ @listener ||= Listener.new
16
+ end
17
+
18
+ class Pipe
19
+ attr_accessor :in, :out
20
+ attr_reader :last_read
21
+
22
+ def initialize
23
+ @out, @in = IO.pipe
24
+ if defined?(::Encoding::ASCII_8BIT)
25
+ @in.set_encoding(::Encoding::ASCII_8BIT)
26
+ end
27
+ @last_read = Time.now
28
+ end
29
+
30
+ def close
31
+ @out.close unless @out.closed?
32
+ @in.close unless @in.closed?
33
+ end
34
+
35
+ def write(data)
36
+ @out.close unless @out.closed?
37
+ @in << Marshal.dump(data) + "\n\n"
38
+ end
39
+
40
+ def read
41
+ @in.close unless @in.closed?
42
+ @last_read = Time.now
43
+ @out.gets("\n\n")
44
+ end
45
+
46
+ def closed?
47
+ @out.closed? && @in.closed?
48
+ end
49
+ end
50
+
51
+ class Listener
52
+ attr_reader :thread
53
+ attr_accessor :pipes, :timeout, :select_timeout
54
+
55
+ def initialize
56
+ @pipes = {}
57
+ @timeout = 360
58
+ @select_timeout = 60
59
+ end
60
+
61
+ def register_pipe(id)
62
+ @pipes[id] = Pipe.new
63
+ wake.in << '.'
64
+ end
65
+
66
+ def start
67
+ return if @started == true
68
+ @started = true
69
+ @thread = Thread.new do
70
+ loop do
71
+ clean_up_pipes
72
+ pipes_to_listen_to = @pipes.values.map{|pipe| pipe.out} + [wake.out]
73
+ if ready = IO.select(pipes_to_listen_to, [], [], @select_timeout)
74
+ pipe = ready[0][0]
75
+ if pipe == wake.out
76
+ pipe.read(1)
77
+ else
78
+ merge_data_from_pipe(pipe)
79
+ end
80
+ end
81
+
82
+ break if !should_keep_listening?
83
+ end
84
+ end
85
+ @thread #.abort_on_exception = true
86
+ sleep 0.001 # give time for the thread to spawn
87
+ end
88
+
89
+ def stop
90
+ return unless @started == true
91
+ @started = false
92
+ wake.in << '.' unless wake.in.closed?
93
+ @thread.join # make sure we wait for the thread to exit
94
+ close_all_pipes
95
+ @wake.close
96
+ @wake = nil
97
+ end
98
+
99
+ def close_all_pipes
100
+ @pipes.each do |id, pipe|
101
+ pipe.close if pipe
102
+ end
103
+ @pipes = {}
104
+ end
105
+
106
+ def wake
107
+ @wake ||= Pipe.new
108
+ end
109
+
110
+ def started?
111
+ @started
112
+ end
113
+
114
+ protected
115
+
116
+ def merge_data_from_pipe(pipe_handle)
117
+ pipe = find_pipe_for_handle(pipe_handle)
118
+ got = pipe.read
119
+
120
+ if got && !got.empty?
121
+ payload = Marshal.load(got)
122
+ if payload == 'EOF'
123
+ pipe.close
124
+ else
125
+ NewRelic::Agent.agent.merge_data_from([payload[:stats],
126
+ payload[:transaction_traces],
127
+ payload[:error_traces]])
128
+ end
129
+ end
130
+ end
131
+
132
+ def should_keep_listening?
133
+ @started || @pipes.values.find{|pipe| !pipe.in.closed?}
134
+ end
135
+
136
+ def clean_up_pipes
137
+ @pipes.values.each do |pipe|
138
+ if pipe.last_read.to_f + @timeout < Time.now.to_f
139
+ pipe.close unless pipe.closed?
140
+ end
141
+ end
142
+ @pipes.reject! {|id, pipe| pipe.out.closed? }
143
+ end
144
+
145
+ def find_pipe_for_handle(out_handle)
146
+ @pipes.values.find{|pipe| pipe.out == out_handle }
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end