#! /usr/bin/env ruby # frozen_string_literal: true # # 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. # 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