diff --git a/files/nrpe/check_mysql_longqueries b/files/nrpe/check_mysql_longqueries index 1a4458a..e72176d 100755 --- a/files/nrpe/check_mysql_longqueries +++ b/files/nrpe/check_mysql_longqueries @@ -1,237 +1,372 @@ -#!/usr/bin/perl -# $Id$ -# -# check_mysql_longqueries plugin for Nagios -# -# Copyright (C) 2009 Vincent Rivellino -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# -# Checks MySQL's processlist to see if there are queries running longer than -# defined thresholds. -# -# Requires the following modules: -# DBI -# Monitoring::Plugin -# -# Copyright Notice: GPLv2 -# -# CHANGES -# -# 30 Jan 2009 - Vincent Rivellino -# Initial version released. -# -# 02 Mar 2020 - Ludovic Cartier -# Replace Nagios::Plugin by Monitoring::Plugin -# need debian package libmonitoring-plugin-perl -# +#!/bin/bash -use warnings; -use strict; -use DBI; -use Monitoring::Plugin; +# MySQL Long Running Query Monitoring Script +# Converted from Perl to Bash +# Original: Vincent Rivellino +# Copyright (C) 2009 +# This plugin comes with ABSOLUTELY NO WARRANTY. This is free software. +set -u -## setup Monitoring::Plugin -my $np = Monitoring::Plugin->new( - usage => "Usage: %s [-v|--verbose] [-H ] [-P ] [-S ] [-u ] [-p ] -w -c ", - version => "1.0", - license => "Copyright (C) 2009 Vincent Rivellino \n" . - "This plugin comes with ABSOLUTELY NO WARRANTY. This is free software, and you\n" . - "are welcome to redistribute it under the conditions of version 2 of the GPL." -); +# Default values +VERBOSE=0 +HOST="" +PORT="" +SOCKET="" +USER="" +PASSWORD="" +WARN="" +CRIT="" +DB_FILTER="" +SKIP_DB="" +CLIENTUSER_FILTER="" +SKIP_CLIENTUSER="" +CLIENTHOST_FILTER="" +SKIP_CLIENTHOST="" +TIMEOUT=10 -## add command line arguments -$np->add_arg( - spec => 'host|H=s', - help => "-H, --host\n MySQL server host" -); -$np->add_arg( - spec => 'port|P=i', - help => "-P, --port\n MySQL server port" -); -$np->add_arg( - spec => 'socket|S=s', - help => "-S, --socket\n MySQL server socket" -); -$np->add_arg( - spec => 'user|u=s', - help => "-u, --user\n database user (must have privilege to SHOW PROCESSLIST)" -); -$np->add_arg( - spec => 'password|p=s', - help => "-p, --password\n database password" -); -$np->add_arg( - spec => 'warn|w=i', - help => "-w, --warn\n Query time in seconds to generate a WARNING", - required => 1 -); -$np->add_arg( - spec => 'crit|c=i', - help => "-c, --crit\n Query time in seconds to generate a CRITICAL", - required => 1 -); -$np->add_arg( - spec => 'db=s', - help => "--db\n Only check queries running on this database\n To specify more than one, separate with commas." -); -$np->add_arg( - spec => 'skip_db=s', - help => "--skip_db\n Don't check queries running on this database\n To specify more than one, separate with commas." -); -$np->add_arg( - spec => 'clientuser=s', - help => "--clientuser\n Only check queries running by this MySQL user\n To specify more than one, separate with commas." -); -$np->add_arg( - spec => 'skip_clientuser=s', - help => "--skip_clientuser\n Don't check queries running by this MySQL user\n To specify more than one, separate with commas." -); -$np->add_arg( - spec => 'clienthost=s', - help => "--clienthost\n Only check queries running from this client host\n To specify more than one, separate with commas." -); -$np->add_arg( - spec => 'skip_clienthost=s', - help => "--skip_clienthost\n Don't check queries running from this client host\n To specify more than one, separate with commas." -); +# Nagios exit codes +OK=0 +WARNING=1 +CRITICAL=2 +UNKNOWN=3 +# Function to print usage +usage() { + cat <] [-P ] [-S ] [-u ] [-p ] -w -c -## parse the command line arguments -$np->getopts; -my $verbose = $np->opts->verbose || 0; +Options: + -H, --host MySQL server host + -P, --port MySQL server port + -S, --socket MySQL server socket + -u, --user Database user (must have privilege to SHOW PROCESSLIST) + -p, --password Database password + -w, --warn Query time in seconds to generate a WARNING (required) + -c, --crit Query time in seconds to generate a CRITICAL (required) + --db Only check queries running on this database (comma-separated) + --skip_db Don't check queries on this database (comma-separated) + --clientuser Only check queries by this MySQL user (comma-separated) + --skip_clientuser Don't check queries by this MySQL user (comma-separated) + --clienthost Only check queries from this client host (comma-separated) + --skip_clienthost Don't check queries from this client host (comma-separated) + -v, --verbose Increase verbosity + --version Show version + -h, --help Show this help message -if ( $verbose >= 2 ) { - print "Plugin options:\n"; - printf " %-23s %d\n", "verbose:", $verbose; - printf " %-23s %s\n", "host:", $np->opts->host || ''; - printf " %-23s %s\n", "port:", $np->opts->port || ''; - printf " %-23s %s\n", "socket:", $np->opts->socket || ''; - printf " %-23s %s\n", "user:", $np->opts->user || ''; - printf " %-23s %s\n", "password:", $np->opts->password || ''; - printf " %-23s %d\n", "warn:", $np->opts->warn; - printf " %-23s %d\n", "crit:", $np->opts->crit; - printf " %-23s %s\n", "db:", $np->opts->db || ''; - printf " %-23s %s\n", "skip_db:", $np->opts->skip_db || ''; - printf " %-23s %s\n", "clientuser:", $np->opts->clientuser || ''; - printf " %-23s %s\n", "skip_clientuser:", $np->opts->skip_clientuser || ''; - printf " %-23s %s\n", "clienthost:", $np->opts->clienthost || ''; - printf " %-23s %s\n", "skip_clienthost:", $np->opts->skip_clienthost || ''; +EOF + exit $UNKNOWN } -# extract restrictions from args - will grep() these lists -my @db = split( '/,/', $np->opts->db || '' ); -my @skipdb = split( '/,/', $np->opts->skip_db || '' ); -my @clientuser = split( '/,/', $np->opts->clientuser || '' ); -my @skipclientuser = split( '/,/', $np->opts->skip_clientuser || '' ); -my @clienthost = split( '/,/', $np->opts->clienthost || '' ); -my @skipclienthost = split( '/,/', $np->opts->skip_clienthost || '' ); - -alarm $np->opts->timeout; - -## setup the dsn - no need to specify a database -my $dsn = 'DBI:mysql:'; - -## if we're connecting to localhost (by name) or the host isn't defined ... -if ( ! $np->opts->host || $np->opts->host eq 'localhost' ) { - # connect via a local socket (if it's defined) - $dsn .= ';mysql_socket=' . $np->opts->socket - if $np->opts->socket; - -## otherwise, attempt to connect via host and/or port (if they're defined) -} else { - $dsn .= ';host=' . $np->opts->host - if $np->opts->host; - $dsn .= ';port=' . $np->opts->port - if $np->opts->port; +# Function to exit with Nagios status +nagios_exit() { + local status=$1 + local message=$2 + + case $status in + $OK) echo "OK - $message"; exit $OK ;; + $WARNING) echo "WARNING - $message"; exit $WARNING ;; + $CRITICAL) echo "CRITICAL - $message"; exit $CRITICAL ;; + $UNKNOWN) echo "UNKNOWN - $message"; exit $UNKNOWN ;; + esac } -## print dsn if really verbose -print "DSN: '$dsn' USER: '", $np->opts->user || '', "' PASS: '", $np->opts->password || '', "'\n" - if $verbose >= 2; +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -H|--host) + HOST="$2" + shift 2 + ;; + -P|--port) + PORT="$2" + shift 2 + ;; + -S|--socket) + SOCKET="$2" + shift 2 + ;; + -u|--user) + USER="$2" + shift 2 + ;; + -p|--password) + PASSWORD="$2" + shift 2 + ;; + -w|--warn) + WARN="$2" + shift 2 + ;; + -c|--crit) + CRIT="$2" + shift 2 + ;; + --db) + DB_FILTER="$2" + shift 2 + ;; + --skip_db) + SKIP_DB="$2" + shift 2 + ;; + --clientuser) + CLIENTUSER_FILTER="$2" + shift 2 + ;; + --skip_clientuser) + SKIP_CLIENTUSER="$2" + shift 2 + ;; + --clienthost) + CLIENTHOST_FILTER="$2" + shift 2 + ;; + --skip_clienthost) + SKIP_CLIENTHOST="$2" + shift 2 + ;; + -v|--verbose) + ((VERBOSE++)) + shift + ;; + --version) + echo "Version 1.0" + exit $OK + ;; + -h|--help) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done -## connect to the database server -my $dbh = DBI->connect( $dsn, $np->opts->user || '', $np->opts->password || '', - { RaiseError => 0, PrintError => 0, AutoCommit => 1 } ) - or $np->nagios_exit( UNKNOWN, "Could not connect to database: $DBI::errstr" ); +# Validate required arguments +if [[ -z "$WARN" || -z "$CRIT" ]]; then + echo "ERROR: -w (warn) and -c (crit) are required" + usage +fi -## get the list of running queries -my $sth = $dbh->prepare( 'SHOW FULL PROCESSLIST' ); -$sth->execute(); -$np->nagios_exit( UNKNOWN, $sth->errstr ) if $sth->err; +# Print verbose options +if [[ $VERBOSE -ge 2 ]]; then + echo "Plugin options:" + printf " %-23s %d\n" "verbose:" "$VERBOSE" + printf " %-23s %s\n" "host:" "${HOST:-}" + printf " %-23s %s\n" "port:" "${PORT:-}" + printf " %-23s %s\n" "socket:" "${SOCKET:-}" + printf " %-23s %s\n" "user:" "${USER:-}" + printf " %-23s %s\n" "password:" "${PASSWORD:-}" + printf " %-23s %d\n" "warn:" "$WARN" + printf " %-23s %d\n" "crit:" "$CRIT" + printf " %-23s %s\n" "db:" "${DB_FILTER:-}" + printf " %-23s %s\n" "skip_db:" "${SKIP_DB:-}" + printf " %-23s %s\n" "clientuser:" "${CLIENTUSER_FILTER:-}" + printf " %-23s %s\n" "skip_clientuser:" "${SKIP_CLIENTUSER:-}" + printf " %-23s %s\n" "clienthost:" "${CLIENTHOST_FILTER:-}" + printf " %-23s %s\n" "skip_clienthost:" "${SKIP_CLIENTHOST:-}" +fi -## bind each row result to a hash -my %row; -$sth->bind_columns( \( @row{ @{$sth->{NAME_lc} } } )); +# Convert comma-separated filters to arrays +IFS=',' read -ra DB_ARRAY <<< "${DB_FILTER:-}" +IFS=',' read -ra SKIP_DB_ARRAY <<< "${SKIP_DB:-}" +IFS=',' read -ra CLIENTUSER_ARRAY <<< "${CLIENTUSER_FILTER:-}" +IFS=',' read -ra SKIP_CLIENTUSER_ARRAY <<< "${SKIP_CLIENTUSER:-}" +IFS=',' read -ra CLIENTHOST_ARRAY <<< "${CLIENTHOST_FILTER:-}" +IFS=',' read -ra SKIP_CLIENTHOST_ARRAY <<< "${SKIP_CLIENTHOST:-}" +# Set alarm timeout +trap 'nagios_exit $UNKNOWN "Plugin timed out"' ALRM +alarm $TIMEOUT 2>/dev/null || true -## use these to keep track of the longest-running query -my $longquery_info = ''; -my $longquery_time = 0; +# Build MySQL connection string +MYSQL_OPTS=() -## process the results -my $count = 0; -while ( $sth->fetch ) { - $count++; +if [[ -z "$HOST" || "$HOST" == "localhost" ]]; then + # Connect via socket if specified + if [[ -n "$SOCKET" ]]; then + MYSQL_OPTS+=(--socket="$SOCKET") + fi +else + # Connect via host and port + if [[ -n "$HOST" ]]; then + MYSQL_OPTS+=(--host="$HOST") + fi + if [[ -n "$PORT" ]]; then + MYSQL_OPTS+=(--port="$PORT") + fi +fi - # skip if time is zero or NULL - next unless $row{'time'}; +# Add credentials +if [[ -n "$USER" ]]; then + MYSQL_OPTS+=(--user="$USER") +fi +if [[ -n "$PASSWORD" ]]; then + MYSQL_OPTS+=(--password="$PASSWORD") +fi - # skip ignorable results - next if $row{'user'} eq 'system user'; - next if $row{'command'} =~ m/(Sleep|Binlog Dump|Ping|Processlist)/io; +# Print connection info if really verbose +if [[ $VERBOSE -ge 2 ]]; then + echo "MySQL Options: ${MYSQL_OPTS[*]}" +fi - # extract connection info - my $db = $row{'db'} || ''; - my $user = $row{'user'} || ''; - my $host = $row{'host'} || ''; - $host =~ s/:\d+$//o; - - # skip if connection info does or doest match criteria - next if $np->opts->db and grep !/^$db$/, @db; - next if $np->opts->skip_db and grep /^$db$/, @skipdb; - - next if $np->opts->clientuser and grep !/^$user$/, @clientuser; - next if $np->opts->skip_clientuser and grep /^$user$/, @skipclientuser; - - next if $np->opts->clienthost and grep !/^$host$/, @clienthost; - next if $np->opts->skip_clienthost and grep /^$host$/, @skipclienthost; - - # only save the longest running query - if ( $row{'time'} > $longquery_time ) { - $longquery_time = $row{'time'}; - $longquery_info = "TIME: $row{'time'}"; - foreach my $k ( sort keys %row ) { - next if $k eq 'time' or $k eq 'info'; - $longquery_info .= " $k=" . ( $row{$k} || 'NULL' ); - } - $longquery_info .= " INFO=" . ( $row{'info'} || 'NULL' ); - } +# Function to check if value matches any item in array +array_contains() { + local value=$1 + shift + local array=("$@") + + for item in "${array[@]}"; do + if [[ "$value" == "$item" ]]; then + return 0 + fi + done + return 1 } -# we're done with the db handle -$dbh->disconnect; +# Function to check if value does NOT match any item in array +array_not_contains() { + local value=$1 + shift + local array=("$@") + + [[ ${#array[@]} -eq 0 ]] && return 0 + + for item in "${array[@]}"; do + if [[ "$value" == "$item" ]]; then + return 1 + fi + done + return 0 +} -# OK if no long queries were found -$np->nagios_exit( OK, "No long running queries found ($count threads checked)" ) unless $longquery_info; +# Query the MySQL processlist +QUERY_OUTPUT=$(mysql "${MYSQL_OPTS[@]}" -e "SHOW FULL PROCESSLIST\G" 2>&1) -# check for crit -$np->nagios_exit( CRITICAL, $longquery_info ) if $longquery_time >= $np->opts->crit; -$np->nagios_exit( WARNING, $longquery_info ) if $longquery_time >= $np->opts->warn; +if [[ $? -ne 0 ]]; then + nagios_exit $UNKNOWN "Could not connect to database: $QUERY_OUTPUT" +fi -# OK if if the longest query didn't match crit & warn -$np->nagios_exit( OK, "No long running queries found ($count threads checked)" ); +# Parse the processlist output +LONGQUERY_INFO="" +LONGQUERY_TIME=0 +COUNT=0 +CURRENT_QUERY="" + +while IFS= read -r line; do + # Skip empty lines + [[ -z "$line" ]] && continue + + # Check for start of new record + if [[ $line =~ ^[[:space:]]*\*\*\*\*\*\*\*\*\*\* ]]; then + # Process previous query if any + if [[ -n "$CURRENT_QUERY" ]]; then + ((COUNT++)) + eval "$CURRENT_QUERY" + + # Skip if time is 0 or NULL + [[ -z "$TIME" || "$TIME" -eq 0 ]] && CURRENT_QUERY="" && continue + + # Skip system queries + [[ "$USER_VAL" == "system user" ]] && CURRENT_QUERY="" && continue + [[ "$COMMAND" =~ (Sleep|Binlog\ Dump|Ping|Processlist) ]] && CURRENT_QUERY="" && continue + + # Extract host without port + HOST_VAL="${HOST_VAL%:*}" + + # Apply filters + if [[ -n "$DB_FILTER" ]]; then + array_contains "$DB_VAL" "${DB_ARRAY[@]}" || { + CURRENT_QUERY="" + continue + } + fi + + if [[ -n "$SKIP_DB" ]]; then + array_not_contains "$DB_VAL" "${SKIP_DB_ARRAY[@]}" || { + CURRENT_QUERY="" + continue + } + fi + + if [[ -n "$CLIENTUSER_FILTER" ]]; then + array_contains "$USER_VAL" "${CLIENTUSER_ARRAY[@]}" || { + CURRENT_QUERY="" + continue + } + fi + + if [[ -n "$SKIP_CLIENTUSER" ]]; then + array_not_contains "$USER_VAL" "${SKIP_CLIENTUSER_ARRAY[@]}" || { + CURRENT_QUERY="" + continue + } + fi + + if [[ -n "$CLIENTHOST_FILTER" ]]; then + array_contains "$HOST_VAL" "${CLIENTHOST_ARRAY[@]}" || { + CURRENT_QUERY="" + continue + } + fi + + if [[ -n "$SKIP_CLIENTHOST" ]]; then + array_not_contains "$HOST_VAL" "${SKIP_CLIENTHOST_ARRAY[@]}" || { + CURRENT_QUERY="" + continue + } + fi + + # Check if this is the longest running query + if [[ "$TIME" -gt "$LONGQUERY_TIME" ]]; then + LONGQUERY_TIME=$TIME + LONGQUERY_INFO="TIME: $TIME ID=$ID USER=$USER_VAL HOST=$HOST_VAL DB=$DB_VAL COMMAND=$COMMAND" + [[ -n "$INFO" ]] && LONGQUERY_INFO="$LONGQUERY_INFO INFO=$INFO" + fi + fi + CURRENT_QUERY="" + else + # Parse key-value pairs + if [[ $line =~ ^[[:space:]]*([^:]+):[[:space:]]*(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + value="${BASH_REMATCH[2]}" + + # Map field names + case "$key" in + "Id") CURRENT_QUERY="${CURRENT_QUERY}ID='$value'; " ;; + "User") CURRENT_QUERY="${CURRENT_QUERY}USER_VAL='$value'; " ;; + "Host") CURRENT_QUERY="${CURRENT_QUERY}HOST_VAL='$value'; " ;; + "db") CURRENT_QUERY="${CURRENT_QUERY}DB_VAL='$value'; " ;; + "Command") CURRENT_QUERY="${CURRENT_QUERY}COMMAND='$value'; " ;; + "Time") CURRENT_QUERY="${CURRENT_QUERY}TIME='${value%% *}'; " ;; + "Info") CURRENT_QUERY="${CURRENT_QUERY}INFO='$value'; " ;; + esac + fi + fi +done <<< "$QUERY_OUTPUT" + +# Process last query +if [[ -n "$CURRENT_QUERY" ]]; then + ((COUNT++)) + eval "$CURRENT_QUERY" + + if [[ -n "$TIME" && "$TIME" -gt 0 ]]; then + [[ "$TIME" -gt "$LONGQUERY_TIME" ]] && LONGQUERY_TIME=$TIME && LONGQUERY_INFO="TIME: $TIME" + fi +fi + +# Check results +if [[ -z "$LONGQUERY_INFO" ]]; then + nagios_exit $OK "No long running queries found ($COUNT threads checked)" +fi + +# Check thresholds +if [[ "$LONGQUERY_TIME" -ge "$CRIT" ]]; then + nagios_exit $CRITICAL "$LONGQUERY_INFO" +elif [[ "$LONGQUERY_TIME" -ge "$WARN" ]]; then + nagios_exit $WARNING "$LONGQUERY_INFO" +else + nagios_exit $OK "No long running queries found ($COUNT threads checked)" +fi \ No newline at end of file diff --git a/tasks/main.yml b/tasks/main.yml index d4842c2..bc9972f 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -14,7 +14,6 @@ - monitoring-plugins-basic - monitoring-plugins-common - monitoring-plugins-standard - - libmonitoring-plugin-perl - name: nrpe | copy nrpe configuration template: diff --git a/templates/nrpe.j2 b/templates/nrpe.j2 index 044ee24..eb2b6a4 100644 --- a/templates/nrpe.j2 +++ b/templates/nrpe.j2 @@ -32,7 +32,7 @@ command[check_rw_root]=/usr/lib/nagios/plugins/check_rofs / {% if nrpe_mysql is defined %} # mysql command[check_mysql]=/usr/lib/nagios/plugins/check_mysql -u {{ nrpe_mysql_user }} -p{{ nrpe_mysql_password }} -H {{ nrpe_mysql_host }} -d mysql -command[check_mysql_longqueries]=/usr/lib/nagios/plugins/check_mysql_longqueries -u {{ nrpe_mysql_user }} -p{{ nrpe_mysql_password }} -H {{ nrpe_mysql_host }} -w 600 -c 1200 +command[check_mysql_longqueries]=/usr/lib/nagios/plugins/check_mysql_longqueries -u {{ nrpe_mysql_user }} -p {{ nrpe_mysql_password }} -H {{ nrpe_mysql_host }} -w 600 -c 1200 {% endif %} {% if nrpe_postgresql is defined %}