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.



17
18
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
56
57
58
59
60
61
# File 'lib/instana/agent.rb', line 17

def initialize
  # Host agent defaults.  Can be configured via Instana.config
  @host = LOCALHOST
  @port = 42699

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

  # Store the pid from process boot so we can detect forks
  @pid = Process.pid

  # Snapshot data is collected once per process but resent
  # every 10 minutes along side process metrics.
  @snapshot = take_snapshot

  # Set last snapshot to just under 10 minutes ago
  # so we send a snapshot sooner than later
  @last_snapshot = Time.now - 570

  # 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

  # 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_info
end

Instance Attribute Details

#agent_uuidObject

Returns the value of attribute agent_uuid.



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

def agent_uuid
  @agent_uuid
end

#stateObject

Returns the value of attribute state.



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

def state
  @state
end

Instance Method Details

#after_forkObject

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



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/instana/agent.rb', line 96

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

  # Re-collect process information post fork
  @pid = Process.pid
  collect_process_info

  transition_to(:unannounced)
  setup
  spawn_background_thread
end

#announce_sensorObject

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



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
# File 'lib/instana/agent.rb', line 219

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

  uri = URI.parse("http://#{@host}:#{@port}/#{DISCOVERY_PATH}")
  req = Net::HTTP::Put.new(uri)
  req.body = announce_payload.to_json

  ::Instana.logger.agent "Announce: http://#{@host}:#{@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
end

#collect_process_infoObject

Used in class initialization and after a fork, this method collects up process information and stores it in @process



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/instana/agent.rb', line 66

def collect_process_info
  @process = {}
  cmdline = ProcTable.ps(Process.pid).cmdline.split("\0")
  @process[:name] = cmdline.shift
  @process[:arguments] = cmdline

  if @is_osx
    # Handle OSX bug where env vars show up at the end of process name
    # such as MANPATH etc..
    @process[:name].gsub!(/[_A-Z]+=\S+/, '')
    @process[:name].rstrip!
  end

  @process[:original_pid] = @pid
  # This is usually Process.pid but in the case of docker, the host agent
  # will return to us the true host pid in which we use to report data.
  @process[:report_pid] = nil
end

#forked?Boolean

Determine whether the pid has changed since Agent start.

@ return [Boolean] true or false to indicate if forked

Returns:

  • (Boolean)


89
90
91
# File 'lib/instana/agent.rb', line 89

def forked?
  @pid != Process.pid
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. It will save where it found the host agent in @host that is used in subsequent HTTP calls.

Returns:

  • (Boolean)


321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/instana/agent.rb', line 321

def host_agent_ready?
  # Localhost
  uri = URI.parse("http://#{LOCALHOST}:#{@port}/")
  req = Net::HTTP::Get.new(uri)

  response = make_host_agent_request(req)

  if response && (response.code.to_i == 200)
    @host = LOCALHOST
    return true
  end

  return false unless @is_linux

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

  response = make_host_agent_request(req)

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

#ready?Boolean

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

Returns:

  • (Boolean)


194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/instana/agent.rb', line 194

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")
  return false
end

#report_entity_data(payload) ⇒ Object

Method to report metrics data to the host agent.

Parameters:

  • paylod (Hash)

    The collection of metrics to report.



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/instana/agent.rb', line 250

def report_entity_data(payload)
  with_snapshot = false
  path = "com.instana.plugin.ruby.#{@process[:report_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)

    # Add in process related that could have changed since
    # snapshot was taken.
    p = { :pid => @process[:report_pid] }
    p[:name] = @process[:name]
    p[:exec_args] = @process[:arguments]
    payload.merge!(p)
  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

      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



212
213
214
# File 'lib/instana/agent.rb', line 212

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)


293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/instana/agent.rb', line 293

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

  path = "com.instana.plugin.ruby/traces.#{@process[:report_pid]}"
  uri = URI.parse("http://#{@host}:#{@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

#setupObject

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



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

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.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
      ::Instana.processor.send
    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



123
124
125
126
127
128
129
130
# File 'lib/instana/agent.rb', line 123

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.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/instana/agent.rb', line 167

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