sensu-plugins/sensu-plugins-haproxy

View on GitHub
bin/check-haproxy.rb

Summary

Maintainability
D
2 days
Test Coverage
#! /usr/bin/env ruby
# frozen_string_literal: false

#
#   check-haproxy.rb
#
# DESCRIPTION:
#   Defaults to checking if ALL services in the given group are up; with
#   -1, checks if ANY service is up. with -A, checks all groups.
#
# OUTPUT:
#   plain text
#
# PLATFORMS:
#   Linux
#
# DEPENDENCIES:
#   gem: sensu-plugin
#
# USAGE:
#
# LICENSE:
#   Copyright 2011 Sonian, Inc. and contributors. <support@sensuapp.org>
#   Released under the same terms as Sensu (the MIT license); see LICENSE
#   for details.
#

require 'sensu-plugin/check/cli'
require 'net/http'
require 'socket'
require 'csv'
require 'uri'

#
# Check HA Proxy
#
class CheckHAProxy < Sensu::Plugin::Check::CLI
  option :stats_source,
         short: '-S HOSTNAME|SOCKETPATH',
         long: '--stats HOSTNAME|SOCKETPATH',
         description: 'HAproxy web stats hostname or path to stats socket',
         required: true
  option :port,
         short: '-P PORT',
         long: '--port PORT',
         description: 'HAproxy web stats port',
         default: '80'
  option :path,
         short: '-q STATUSPATH',
         long: '--statspath STATUSPATH',
         description: 'HAproxy web stats path',
         default: '/'
  option :username,
         short: '-u USERNAME',
         long: '--user USERNAME',
         description: 'HAproxy web stats username'
  option :password,
         short: '-p PASSWORD',
         long: '--pass PASSWORD',
         description: 'HAproxy web stats password'
  option :use_ssl,
         description: 'Use SSL to connect to HAproxy web stats',
         long: '--use-ssl',
         boolean: true,
         default: false
  option :warn_percent,
         short: '-w PERCENT',
         boolean: true,
         default: 50,
         proc: proc(&:to_i),
         description: 'Warning Percent, default: 50'
  option :crit_percent,
         short: '-c PERCENT',
         boolean: true,
         default: 25,
         proc: proc(&:to_i),
         description: 'Critical Percent, default: 25'
  option :session_warn_percent,
         short: '-W PERCENT',
         boolean: true,
         default: 75,
         proc: proc(&:to_i),
         description: 'Session Limit Warning Percent, default: 75'
  option :session_crit_percent,
         short: '-C PERCENT',
         boolean: true,
         default: 90,
         proc: proc(&:to_i),
         description: 'Session Limit Critical Percent, default: 90'
  option :backend_session_warn_percent,
         short: '-b PERCENT',
         proc: proc(&:to_i),
         description: 'Per Backend Session Limit Warning Percent'
  option :backend_session_crit_percent,
         short: '-B PERCENT',
         proc: proc(&:to_i),
         description: 'Per Backend Session Limit Critical Percent'
  option :min_warn_count,
         short: '-M COUNT',
         default: 0,
         proc: proc(&:to_i),
         description: 'Minimum Server Warn Count, default: 0'
  option :min_crit_count,
         short: '-X COUNT',
         default: 0,
         proc: proc(&:to_i),
         description: 'Minimum Server Critical Count, default: 0'
  option :all_services,
         short: '-A',
         boolean: true,
         description: 'Check ALL Services, flag enables'
  option :include_maint,
         long: '--include-maint',
         boolean: false,
         description: 'Include servers in maintanance mode while checking (as DOWN)'
  option :missing_ok,
         short: '-m',
         boolean: true,
         description: 'Missing OK, flag enables'
  option :service,
         short: '-s SVC',
         description: 'Service Name to Check'
  option :exact_match,
         short: '-e',
         boolean: false,
         description: 'Whether service name specified with -s should be exact match or not'
  option :missing_fail,
         short: '-f',
         boolean: false,
         description: 'fail on missing service'

  def service_up?(svc)
    svc[:status].start_with?('UP') ||
      svc[:status] == 'OPEN' ||
      svc[:status] == 'no check' ||
      svc[:status].start_with?('DRAIN')
  end

  def run #rubocop:disable all
    if config[:service] || config[:all_services]
      services = acquire_services
    else
      unknown 'No service specified'
    end

    if services.empty?
      message "No services matching /#{config[:service]}/"
      if config[:missing_fail]
        critical
      elsif config[:missing_ok]
        ok
      else
        warning
      end
    else
      percent_up = 100 * services.count { |svc| service_up? svc } / services.size
      failed_names = services.reject { |svc| service_up? svc }.map do |svc|
        "#{svc[:pxname]}/#{svc[:svname]}#{svc[:check_status].to_s.empty? ? '' : '[' + svc[:check_status] + ']'}"
      end
      critical_sessions = services.select { |svc| svc[:slim].to_i > 0 && (100 * svc[:scur].to_f / svc[:slim].to_f) > config[:session_crit_percent] } # rubocop:disable Style/NumericPredicate
      warning_sessions = services.select { |svc| svc[:slim].to_i > 0 && (100 * svc[:scur].to_f / svc[:slim].to_f) > config[:session_warn_percent] } # rubocop:disable Style/NumericPredicate

      critical_backends = services.select do |svc|
        config[:backend_session_crit_percent] &&
          svc[:svname] == 'BACKEND' &&
          svc[:slim].to_i > 0 && # rubocop:disable Style/NumericPredicate
          (100 * svc[:scur].to_f / svc[:slim].to_f) > config[:backend_session_crit_percent]
      end

      warning_backends = services.select do |svc|
        config[:backend_session_warn_percent] &&
          svc[:svname] == 'BACKEND' &&
          svc[:slim].to_i > 0 && # rubocop:disable Style/NumericPredicate
          (100 * svc[:scur].to_f / svc[:slim].to_f) > config[:backend_session_warn_percent]
      end

      status = "UP: #{percent_up}% of #{services.size} /#{config[:service]}/ services" + (failed_names.empty? ? '' : ", DOWN: #{failed_names.join(', ')}")
      if services.size < config[:min_crit_count]
        critical status
      elsif percent_up < config[:crit_percent]
        critical status
      elsif !critical_sessions.empty? && config[:backend_session_crit_percent].nil?
        critical status + '; Active sessions critical: ' + critical_sessions.map { |s| "#{s[:scur]} of #{s[:slim]} #{s[:pxname]}.#{s[:svname]}" }.join(', ')
      elsif config[:backend_session_crit_percent] && !critical_backends.empty?
        critical status + '; Active backends critical: ' +
                 critical_backends.map { |s| "current sessions: #{s[:scur]}, maximum sessions: #{s[:smax]} for #{s[:pxname]} backend." }.join(', ')
      elsif services.size < config[:min_warn_count]
        warning status
      elsif percent_up < config[:warn_percent]
        warning status
      elsif !warning_sessions.empty? && config[:backend_session_warn_percent].nil?
        warning status + '; Active sessions warning: ' + warning_sessions.map { |s| "#{s[:scur]} of #{s[:slim]} #{s[:pxname]}.#{s[:svname]}" }.join(', ')
      elsif config[:backend_session_warn_percent] && !warning_backends.empty?
        critical status + '; Active backends warning: ' +
                 warning_backends.map { |s| "current sessions: #{s[:scur]}, maximum sessions: #{s[:smax]} for #{s[:pxname]} backend." }.join(', ')
      else
        ok status
      end
    end
  end

  def acquire_services #rubocop:disable all
    uri = URI.parse(config[:stats_source])

    if uri.is_a?(URI::Generic) && File.socket?(uri.path)
      srv = UNIXSocket.open(config[:stats_source])
      srv.write("show stat\n")
      out = srv.read
      srv.close
    else
      res = Net::HTTP.start(config[:stats_source], config[:port], use_ssl: config[:use_ssl]) do |http|
        req = Net::HTTP::Get.new("/#{config[:path]};csv;norefresh")
        unless config[:username].nil?
          req.basic_auth config[:username], config[:password]
        end
        http.request(req)
      end
      unless res.code.to_i == 200
        unknown "Failed to fetch from #{config[:stats_source]}:#{config[:port]}/#{config[:path]}: #{res.code}"
      end

      out = res.body
    end

    parsed = CSV.parse(out, skip_blanks: true)
    keys = parsed.shift.reject(&:nil?).map { |k| k.match(/([(\-)?\w]+)/)[0].to_sym }
    haproxy_stats = parsed.map { |line| Hash[keys.zip(line)] }

    if config[:all_services]
      haproxy_stats
    else
      regexp = config[:exact_match] ? Regexp.new("^#{config[:service]}$") : Regexp.new(config[:service].to_s)
      haproxy_stats.select do |svc|
        svc[:pxname] =~ regexp
        # #YELLOW
      end.reject do |svc| # rubocop: disable Style/MultilineBlockChain
        %w[FRONTEND BACKEND].include?(svc[:svname])
      end
    end.select do |svc|
      config[:include_maint] || !svc[:status].start_with?('MAINT')
    end
  end
end