Class: Instana::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/instana/agent.rb

Constant Summary collapse

LOCALHOST =
'127.0.0.1'.freeze
MIME_JSON =
'application/json'.freeze
DISCOVERY_PATH =
'com.instana.plugin.ruby.discovery'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeAgent

Returns a new instance of Agent.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/instana/agent.rb', line 19

def initialize
  # Supported two states (unannounced & announced)
  @state = :unannounced

  # Timestamp of the last successful response from
  # entity data reporting.
  @entity_last_seen = Time.now

  # Used to track the last time the collect timer was run.
  @last_collect_run = Time.now

  # Two timers, one for each state (unannounced & announced)
  @timers = ::Timers::Group.new
  @announce_timer = nil
  @collect_timer = nil

  # Detect platform flags
  @is_linux = (RUBY_PLATFORM =~ /linux/i) ? true : false
  @is_osx = (RUBY_PLATFORM =~ /darwin/i) ? true : false

  # In case we're running in Docker, have the default gateway available
  # to check in case we're running in bridged network mode
  if @is_linux
    @default_gateway = `/sbin/ip route | awk '/default/ { print $3 }'`.chomp
  else
    @default_gateway = nil
  end

  # The agent UUID returned from the host agent
  @agent_uuid = nil

  # Collect process information
  @process = ::Instana::Util.collect_process_info

  # This will hold info on the discovered agent host
  @discovered = nil
end

Instance Attribute Details

#agent_uuidObject

Returns the value of attribute agent_uuid.



12
13
14
# File 'lib/instana/agent.rb', line 12

def agent_uuid
  @agent_uuid
end

#processObject

Returns the value of attribute process.



13
14
15
# File 'lib/instana/agent.rb', line 13

def process
  @process
end

#stateObject

Returns the value of attribute state.



11
12
13
# File 'lib/instana/agent.rb', line 11

def state
  @state
end

Instance Method Details

#after_forkObject

Used post fork to re-initialize state and restart communications with the host agent.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/instana/agent.rb', line 60

def after_fork
  ::Instana.logger.agent "after_fork hook called. Falling back to unannounced state and spawning a new background agent thread."

  # Reseed the random number generator for this
  # new thread.
  srand

  # Re-collect process information post fork
  @process = ::Instana::Util.collect_process_info

  transition_to(:unannounced)
  setup
  spawn_background_thread
end

#announce_sensorObject

Collect process ID, name and arguments to notify the host agent.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/instana/agent.rb', line 167

def announce_sensor
  unless @discovered
    ::Instana.logger.agent("#{__method__} called but discovery hasn't run yet!")
    return false
  end

  announce_payload = {}
  announce_payload[:pid] = pid_namespace? ? get_real_pid : Process.pid
  announce_payload[:name] = @process[:name]
  announce_payload[:args] = @process[:arguments]

  if @is_linux && !::Instana.test?
    # We create an open socket to the host agent in case we are running in a container
    # and the real pid needs to be detected.
    socket = TCPSocket.new @discovered[:agent_host], @discovered[:agent_port]
    announce_payload[:fd] = socket.fileno
    announce_payload[:inode] = File.readlink("/proc/#{Process.pid}/fd/#{socket.fileno}")
  end

  uri = URI.parse("http://#{@discovered[:agent_host]}:#{@discovered[:agent_port]}/#{DISCOVERY_PATH}")
  req = Net::HTTP::Put.new(uri)
  req.body = announce_payload.to_json

  ::Instana.logger.agent "Announce: http://#{@discovered[:agent_host]}:#{@discovered[:agent_port]}/#{DISCOVERY_PATH} - payload: #{req.body}"

  response = make_host_agent_request(req)

  if response && (response.code.to_i == 200)
    data = JSON.parse(response.body)
    @process[:report_pid] = data['pid']
    @agent_uuid = data['agentUuid']
    true
  else
    false
  end
rescue => e
  Instana.logger.error "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}"
  Instana.logger.debug e.backtrace.join("\r\n")
  return false
ensure
  socket.close if socket
end

#handle_response(json_string) ⇒ Object

When a request is received by the host agent, it is sent here from processing and response.

Parameters:

  • json_string (String)

    the request from the host agent



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/instana/agent.rb', line 253

def handle_response(json_string)
  their_request = JSON.parse(json_string).first

  if their_request.key?("action")
    if their_request["action"] == "ruby.source"
      payload = ::Instana::Util.get_rb_source(their_request["args"]["file"])
    else
      payload = { :error => "Unrecognized action: #{their_request["action"]}. An newer Instana gem may be required for this. Current version: #{::Instana::VERSION}" }
    end
  else
    payload = { :error => "Instana Ruby: No action specified in request." }
  end

  path = "com.instana.plugin.ruby/response.#{@process[:report_pid]}?messageId=#{URI.encode(their_request['messageId'])}"
  uri = URI.parse("http://#{@discovered[:agent_host]}:#{@discovered[:agent_port]}/#{path}")
  req = Net::HTTP::Post.new(uri)
  req.body = payload.to_json
  ::Instana.logger.agent_response "Responding to agent: #{req.inspect}"
  make_host_agent_request(req)
end

#host_agent_ready?Boolean

Check that the host agent is available and can be contacted. This will first check localhost and if not, then attempt on the default gateway for docker in bridged mode.

Returns:

  • (Boolean)


311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/instana/agent.rb', line 311

def host_agent_ready?
  @discovered ||= run_discovery

  if @discovered
    # Try default location or manually configured (if so)
    uri = URI.parse("http://#{@discovered[:agent_host]}:#{@discovered[:agent_port]}/")
    req = Net::HTTP::Get.new(uri)

    response = make_host_agent_request(req)

    if response && (response.code.to_i == 200)
      return true
    end
  end
  false
rescue => e
  Instana.logger.error "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}"
  Instana.logger.debug e.backtrace.join("\r\n") unless ::Instana.test?
  return false
end

#ready?Boolean

Indicates if the agent is ready to send metrics and/or data.

Returns:

  • (Boolean)


387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/instana/agent.rb', line 387

def ready?
  # In test, we're always ready :-)
  return true if ENV['INSTANA_GEM_TEST']

  if forked?
    ::Instana.logger.agent "Instana: detected fork.  Calling after_fork"
    after_fork
  end

  @state == :announced
rescue => e
  Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}"
  Instana.logger.debug e.backtrace.join("\r\n") unless ::Instana.test?
  return false
end

#report_metrics(payload) ⇒ Boolean

Method to report metrics data to the host agent.

Parameters:

  • paylod (Hash)

    The collection of metrics to report.

Returns:

  • (Boolean)

    true on success, false otherwise



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/instana/agent.rb', line 216

def report_metrics(payload)
  unless @discovered
    ::Instana.logger.agent("#{__method__} called but discovery hasn't run yet!")
    return false
  end

  path = "com.instana.plugin.ruby.#{@process[:report_pid]}"
  uri = URI.parse("http://#{@discovered[:agent_host]}:#{@discovered[:agent_port]}/#{path}")
  req = Net::HTTP::Post.new(uri)

  req.body = payload.to_json
  response = make_host_agent_request(req)

  if response
    if response.body && response.body.length > 2
      # The host agent returned something indicating that is has a request for us that we
      # need to process.
      handle_response(response.body)
    end

    if response.code.to_i == 200
      @entity_last_seen = Time.now
      return true
    end

  end
  false
rescue => e
  Instana.logger.error "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}"
  Instana.logger.debug e.backtrace.join("\r\n")
end

#report_pidObject

Returns the PID that we are reporting to



380
381
382
# File 'lib/instana/agent.rb', line 380

def report_pid
  @process[:report_pid]
end

#report_spans(spans) ⇒ Boolean

Accept and report spans to the host agent.

Parameters:

  • traces (Array)

    An array of [Span]

Returns:

  • (Boolean)


279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/instana/agent.rb', line 279

def report_spans(spans)
  return unless @state == :announced

  unless @discovered
    ::Instana.logger.agent("#{__method__} called but discovery hasn't run yet!")
    return false
  end

  path = "com.instana.plugin.ruby/traces.#{@process[:report_pid]}"
  uri = URI.parse("http://#{@discovered[:agent_host]}:#{@discovered[:agent_port]}/#{path}")
  req = Net::HTTP::Post.new(uri)

  req.body = spans.to_json
  response = make_host_agent_request(req)

  if response
    last_trace_response = response.code.to_i

    if [200, 204].include?(last_trace_response)
      return true
    end
  end
  false
rescue => e
  Instana.logger.debug "#{__method__}:#{File.basename(__FILE__)}:#{__LINE__}: #{e.message}"
  Instana.logger.debug e.backtrace.join("\r\n")
end

#run_discoveryHash

Runs a discovery process to determine where we can contact the host agent. This is usually just localhost but in docker can be found on the default gateway. This also allows for manual configuration via ::Instana.config.

Returns:

  • (Hash)

    a hash with :agent_host, :agent_port values or empty hash



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/instana/agent.rb', line 338

def run_discovery
  discovered = {}

  ::Instana.logger.debug "#{__method__}: Running agent discovery..."

  # Try default location or manually configured (if so)
  uri = URI.parse("http://#{::Instana.config[:agent_host]}:#{::Instana.config[:agent_port]}/")
  req = Net::HTTP::Get.new(uri)

  ::Instana.logger.debug "#{__method__}: Trying #{::Instana.config[:agent_host]}:#{::Instana.config[:agent_port]}"

  response = make_host_agent_request(req)

  if response && (response.code.to_i == 200)
    discovered[:agent_host] = ::Instana.config[:agent_host]
    discovered[:agent_port] = ::Instana.config[:agent_port]
    ::Instana.logger.debug "#{__method__}: Found #{discovered[:agent_host]}:#{discovered[:agent_port]}"
    return discovered
  end

  return nil unless @is_linux

  # We are potentially running on Docker in bridged networking mode.
  # Attempt to contact default gateway
  uri = URI.parse("http://#{@default_gateway}:#{::Instana.config[:agent_port]}/")
  req = Net::HTTP::Get.new(uri)

  ::Instana.logger.debug "#{__method__}: Trying default gateway #{@default_gateway}:#{::Instana.config[:agent_port]}"

  response = make_host_agent_request(req)

  if response && (response.code.to_i == 200)
    discovered[:agent_host] = @default_gateway
    discovered[:agent_port] = ::Instana.config[:agent_port]
    ::Instana.logger.debug "#{__method__}: Found #{discovered[:agent_host]}:#{discovered[:agent_port]}"
    return discovered
  end
  nil
end

#setupObject

Sets up periodic timers and starts the agent in a background thread.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/instana/agent.rb', line 101

def setup
  # 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.warn "Host agent available. We're in business."
      transition_to(:announced)
    end
  end

  # The collect timer
  # If we are in announced state, send metric data (only delta reporting)
  # every ::Instana.config[:collector][:interval] seconds.
  @collect_timer = @timers.every(::Instana.config[:collector][:interval]) do
    # Make sure that this block doesn't get called more often than the interval.  This can
    # happen on high CPU load and a back up of timer runs.  If we are called before `interval`
    # then we just skip.
    unless (Time.now - @last_collect_run) < ::Instana.config[:collector][:interval]
      @last_collect_run = Time.now
      if @state == :announced
        if !::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.warn "Host agent offline for >1 min.  Going to sit in a corner..."
            transition_to(:unannounced)
          end
        end
        ::Instana.processor.send
      end
    end
  end
end

#spawn_background_threadObject

Spawns the background thread and calls start. This method is separated out for those who wish to control which thread the background agent will run in.

This method can be overridden with the following:

module Instana

class Agent
  def spawn_background_thread
    # start thread
    start
  end
end

end



90
91
92
93
94
95
96
97
# File 'lib/instana/agent.rb', line 90

def spawn_background_thread
  # The thread calling fork is the only thread in the created child process.
  # fork doesn’t copy other threads.
  # Restart our background thread
  Thread.new do
    start
  end
end

#startObject

Starts the timer loop for the timers that were initialized in the setup method. This is blocking and should only be called from an already initialized background thread.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/instana/agent.rb', line 140

def start
  ::Instana.logger.warn "Host agent not available.  Will retry periodically." unless host_agent_ready?
  loop do
    if @state == :unannounced
      @collect_timer.pause
      @announce_timer.resume
    else
      @announce_timer.pause
      @collect_timer.resume
    end
    @timers.wait
  end
ensure
  if @state == :announced
    # Pause the timers so they don't fire while we are
    # reporting traces
    @collect_timer.pause
    @announce_timer.pause

    ::Instana.logger.debug "Agent exiting. Reporting final #{::Instana.processor.queue_count} trace(s)."
    ::Instana.processor.send
  end
end