lib/instana/agent.rb in instana-0.8.1 vs lib/instana/agent.rb in instana-0.8.2

- old
+ new

@@ -1,75 +1,89 @@ require 'net/http' require 'uri' require 'json' +require 'timers' require 'sys/proctable' include Sys module Instana class Agent - attr_accessor :last_entity_response + attr_accessor :state def initialize # Host agent defaults. Can be configured via Instana.config @request_timeout = 5000 @host = '127.0.0.1' @port = 42699 @server_header = 'Instana Agent' + # Supported two states (unannounced & announced) + @state = :unannounced + # Snapshot data is collected once per process but resent # every 10 minutes along side process metrics. @snapshot = take_snapshot # Set last snapshot to 10 minutes ago # so we send a snapshot on first report @last_snapshot = Time.now - 601 + + # Timestamp of the last successful response from + # entity data reporting. + @entity_last_seen = Time.now + + # Two timers, one for each state (unannounced & announced) + @timers = ::Timers::Group.new + @announce_timer = nil + @collect_timer = nil end ## - # take_snapshot + # start # - # Method to collect up process info for snapshots. This - # is generally used once per process. # - def take_snapshot - data = {} + def start + # The announce timer + # We attempt to announce this ruby sensor to the host agent. + # In case of failure, we try again in 30 seconds. + @announce_timer = @timers.now_and_every(30) do + if host_agent_ready? && announce_sensor + ::Instana.logger.debug "Announce successful. Switching to metrics collection." + transition_to(:announced) + end + end - data[:sensorVersion] = ::Instana::VERSION - data[:pid] = Process.pid - data[:ruby_version] = RUBY_VERSION - - process = ProcTable.ps(Process.pid) - arguments = process.cmdline.split(' ') - data[:name] = arguments.shift - data[:exec_args] = arguments - - # Since a snapshot is only taken on process boot, - # this is ok here. - data[:start_time] = Time.now.to_s - - # Framework Detection - if defined?(::RailsLts::VERSION) - data[:framework] = "Rails on Rails LTS-#{::RailsLts::VERSION}" - - elsif defined?(::Rails.version) - data[:framework] = "Ruby on Rails #{::Rails.version}" - - elsif defined?(::Grape::VERSION) - data[:framework] = "Grape #{::Grape::VERSION}" - - elsif defined?(::Padrino::VERSION) - data[:framework] = "Padrino #{::Padrino::VERSION}" - - elsif defined?(::Sinatra::VERSION) - data[:framework] = "Sinatra #{::Sinatra::VERSION}" + # The collect timer + # If we are in announced state, send metric data (only delta reporting) + # every ::Instana::Collector.interval seconds. + @collect_timer = @timers.every(::Instana::Collector.interval) do + if @state == :announced + unless ::Instana::Collector.collect_and_report + # If report has been failing for more than 1 minute, + # fall back to unannounced state + if (Time.now - @entity_last_seen) > 60 + ::Instana.logger.debug "Metrics reporting failed for >1 min. Falling back to unannounced state." + transition_to(:unannounced) + end + end + end end - data - rescue => e - ::Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}" - ::Instana.logger.debug e.backtrace.join("\r\n") - return data + # Start the background ruby sensor thread. It works off of timers and + # is sleeping otherwise + Thread.new do + loop { + if @state == :unannounced + @collect_timer.pause + @announce_timer.resume + else + @announce_timer.pause + @collect_timer.resume + end + @timers.wait + } + end end ## # announce_sensor # @@ -86,65 +100,52 @@ announce_payload[:args] = arguments path = 'com.instana.plugin.ruby.discovery' uri = URI.parse("http://#{@host}:#{@port}/#{path}") req = Net::HTTP::Put.new(uri) - - req['Accept'] = 'application/json' - req['Content-Type'] = 'application/json' req.body = announce_payload.to_json - ::Instana.logger.debug "Announcing sensor to #{path} for pid #{Process.pid}: #{announce_payload.to_json}" - - response = nil - Net::HTTP.start(uri.hostname, uri.port) do |http| - response = http.request(req) - end - Instana.logger.debug response.code + response = make_host_agent_request(req) + response && (response.code.to_i == 200) ? true : false rescue => e Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}" Instana.logger.debug e.backtrace.join("\r\n") end ## # report_entity_data # - # Method to report metrics data to the host agent. Every 10 minutes, this - # method will also send a process snapshot data. + # Method to report metrics data to the host agent. # def report_entity_data(payload) + with_snapshot = false path = "com.instana.plugin.ruby.#{Process.pid}" uri = URI.parse("http://#{@host}:#{@port}/#{path}") req = Net::HTTP::Post.new(uri) # Every 5 minutes, send snapshot data as well if (Time.now - @last_snapshot) > 600 + with_snapshot = true payload.merge!(@snapshot) - @last_snapshot = Time.now end - req['Accept'] = 'application/json' - req['Content-Type'] = 'application/json' req.body = payload.to_json + response = make_host_agent_request(req) - #Instana.logger.debug "Posting metrics to #{path}: #{payload.to_json}" + if response + last_entity_response = response.code.to_i - response = nil - Net::HTTP.start(uri.hostname, uri.port) do |http| - response = http.request(req) - end + if last_entity_response == 200 + @entity_last_seen = Time.now + @last_snapshot = Time.now if with_snapshot - # If snapshot data is in the payload and last response - # was ok then delete the snapshot data. Otherwise let it - # ride for another run. - if response.code.to_i == 200 - @snapshot.each do |k, v| - payload.delete(k) + #::Instana.logger.debug "entity response #{last_entity_response}: #{payload.to_json}" + return true end + #::Instana.logger.debug "entity response #{last_entity_response}: #{payload.to_json}" end - Instana.logger.debug response.code unless response.code.to_i == 200 - @last_entity_response = response.code.to_i + false rescue => e Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}" Instana.logger.debug e.backtrace.join("\r\n") end @@ -155,31 +156,113 @@ # def host_agent_ready? uri = URI.parse("http://#{@host}:#{@port}/") req = Net::HTTP::Get.new(uri) + response = make_host_agent_request(req) + + (response && response.code.to_i == 200) ? true : false + rescue => e + Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}" + Instana.logger.debug e.backtrace.join("\r\n") + return false + end + + private + + ## + # transition_to + # + # Handles any/all steps required in the transtion + # between states. + # + def transition_to(state) + case state + when :announced + # announce successful; set state + @state = :announced + + # Reset the entity timer + @entity_last_seen = Time.now + + # Set last snapshot to 10 minutes ago + # so we send a snapshot on first report + @last_snapshot = Time.now - 601 + when :unannounced + @state = :unannounced + else + ::Instana.logger.warn "Uknown agent state: #{state}" + end + end + + ## + # make host_agent_request + # + # Centralization of the net/http communications + # with the host agent. Pass in a prepared <req> + # of type Net::HTTP::Get|Put|Head + # + def make_host_agent_request(req) req['Accept'] = 'application/json' req['Content-Type'] = 'application/json' - ::Instana.logger.debug "Checking agent availability...." - response = nil - Net::HTTP.start(uri.hostname, uri.port) do |http| + Net::HTTP.start(req.uri.hostname, req.uri.port, :open_timeout => 1, :read_timeout => 1) do |http| response = http.request(req) end + response + rescue Errno::ECONNREFUSED => e + Instana.logger.debug "Agent not responding. Connection refused." + return nil + rescue => e + Instana.logger.debug "Host agent request error: #{e.inspect}" + return nil + end - if response.code.to_i != 200 - Instana.logger.debug "Host agent returned #{response.code}" - false - else - true + private + ## + # take_snapshot + # + # Method to collect up process info for snapshots. This + # is generally used once per process. + # + def take_snapshot + data = {} + + data[:sensorVersion] = ::Instana::VERSION + data[:pid] = ::Process.pid + data[:ruby_version] = RUBY_VERSION + + process = ::ProcTable.ps(Process.pid) + arguments = process.cmdline.split(' ') + data[:name] = arguments.shift + data[:exec_args] = arguments + + # Since a snapshot is only taken on process boot, + # this is ok here. + data[:start_time] = Time.now.to_s + + # Framework Detection + if defined?(::RailsLts::VERSION) + data[:framework] = "Rails on Rails LTS-#{::RailsLts::VERSION}" + + elsif defined?(::Rails.version) + data[:framework] = "Ruby on Rails #{::Rails.version}" + + elsif defined?(::Grape::VERSION) + data[:framework] = "Grape #{::Grape::VERSION}" + + elsif defined?(::Padrino::VERSION) + data[:framework] = "Padrino #{::Padrino::VERSION}" + + elsif defined?(::Sinatra::VERSION) + data[:framework] = "Sinatra #{::Sinatra::VERSION}" end - rescue Errno::ECONNREFUSED => e - Instana.logger.debug "Agent not responding: #{e.inspect}" - return false + + data rescue => e - Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}" - Instana.logger.debug e.backtrace.join("\r\n") - return false + ::Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}" + ::Instana.logger.debug e.backtrace.join("\r\n") + return data end end end