sensu-plugins/sensu-plugins-process-checks

View on GitHub
bin/metrics-process-status.rb

Summary

Maintainability
A
25 mins
Test Coverage
#! /usr/bin/env ruby
#
#   proc-status-metrics
#
# DESCRIPTION:
#   For all processes owned by a user AND/OR matching a provided process
#   name substring, return selected memory metrics from /proc/[PID]/status

#
# OUTPUT:
#   metric data
#
# PLATFORMS:
#   Linux
#
# DEPENDENCIES:
#   gem: sensu-plugin
#
# USAGE:
#
# NOTES:
#   - This check names the metrics after the full process name
#     string as found in /proc/[PID]/cmdline, so data will be lost if
#     you have multiple processes with identical cmdlines.
#   - A process that changes its cmdline but still matches the search
#     query will be duplicated as multiple discovered processes.
#   - We make some assumptions in parsing /proc/[PID]/status, this will
#     mostly only work with the memory-related stats.

#
# LICENSE:
#   Copyright (c) 2014 Cozy Services Ltd. opensource@cozy.co
#   Matt Greensmith mgreensmith@cozy.co
#   Released under the same terms as Sensu (the MIT license); see LICENSE
#   for details.
#

require 'sensu-plugin/metric/cli'
require 'socket'

#
# String
# monkeypatch for helping validate PID strings
#
class String
  # if its and integer, set it to a string
  def integer?
    to_i.to_s == self
  end
end

#
# Proc Status
#
# /proc/[PID]/status memory metrics plugin
#
class ProcStatus < Sensu::Plugin::Metric::CLI::Graphite
  option :user,
         description: 'Query processes owned by a user',
         short: '-u USER',
         long: '--user USER'

  option :processname,
         description: 'Process name substring to match against, not a regex.',
         short: '-p PROCESSNAME',
         long: '--process-name PROCESSNAME'

  option :scheme,
         description: 'Metric naming scheme',
         long: '--scheme SCHEME',
         default: "#{Socket.gethostname}.proc"

  option :metrics,
         description: 'Memory metrics to collect from /proc/[PID]/status, comma-separated',
         short: '-m METRICS',
         long: '--metrics METRICS',
         default: 'VmSize,VmRSS,VmSwap'

  # Build search command
  #
  def pgrep_command
    pgrep_command = 'pgrep '
    pgrep_command << "-u #{config[:user]} " if config[:user]
    pgrep_command << "-f #{config[:processname]} " if config[:processname]
    pgrep_command << '2<&1'
  end

  # Acquire process_pids
  #
  # @param pgrep_output [String]
  #
  def acquire_valid_pids(pgrep_output)
    res = pgrep_output.split("\n").map(&:strip)
    pids = res.select(&:integer?)
    pids
  end

  # Acquire the sate for the supplied PID
  #
  # @param pid [String]
  #
  def acquire_stats_for_pid(pid)
    return nil unless ::File.exist?(::File.join('/proc', pid, 'cmdline'))

    cmdline_raw = `cat /proc/#{pid}/cmdline`
    cmdline = cmdline_raw.strip.gsub(/[^[:alnum:]]/, '_')

    metric_names = config[:metrics].split(',')
    proc_status_lines = `cat /proc/#{pid}/status`.split("\n")

    out = { cmdline.to_s => {} }

    metric_names.each do |m|
      line = proc_status_lines.find { |x| /^#{m}/.match(x) }
      val = line ? line.split("\t")[1].to_i : nil
      out[cmdline.to_s][m] = val
    end
    out
  end

  # Main functino
  #
  def run
    raise 'You must supply -u USER or -p PROCESSNAME' unless config[:user] || config[:processname]
    metrics = {}
    pgrep_output = `#{pgrep_command}`
    pids = acquire_valid_pids(pgrep_output)

    pids.each do |p|
      data = acquire_stats_for_pid(p)
      metrics.merge!(data) unless data.nil?
    end

    timestamp = Time.now.to_i

    metrics.each do |proc_name, stats|
      stats.each do |stat_name, value|
        output [config[:scheme], proc_name, stat_name].join('.'), value, timestamp
      end
    end
    ok
  end
end