require 'net/http' require 'uri' require 'json' require 'timers' require 'sys/proctable' include Sys module Instana class Agent 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 ## # start # # 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 # 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 # 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 # # Collect process ID, name and arguments to notify # the host agent. # def announce_sensor process = ProcTable.ps(Process.pid) announce_payload = {} announce_payload[:pid] = Process.pid arguments = process.cmdline.split(' ') arguments.shift announce_payload[:args] = arguments path = 'com.instana.plugin.ruby.discovery' uri = URI.parse("http://#{@host}:#{@port}/#{path}") req = Net::HTTP::Put.new(uri) req.body = announce_payload.to_json 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. # 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) end req.body = payload.to_json response = make_host_agent_request(req) if response last_entity_response = response.code.to_i if last_entity_response == 200 @entity_last_seen = Time.now @last_snapshot = Time.now if with_snapshot #::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 false rescue => e Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}" Instana.logger.debug e.backtrace.join("\r\n") end ## # host_agent_ready? # # Check that the host agent is available and can be contacted. # 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 # of type Net::HTTP::Get|Put|Head # def make_host_agent_request(req) req['Accept'] = 'application/json' req['Content-Type'] = 'application/json' response = nil 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 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 data rescue => e ::Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}" ::Instana.logger.debug e.backtrace.join("\r\n") return data end end end