#!/usr/bin/perl -w

# $Id$

=head1 NAME

	mysql-monitor - мониторинг инстансов mysql

=head1 DESCRIPTION

    Читает конфиги из /etc/mysql-monitor.d/*, получает из mysql различные метрики,
    пишет их в /var/spool/mysql-monitor/*

    Конфиг заббикса определяет такие параметры:
     - mysql-monitor.alive[*] -- 1 - mysql жив, проверки работают, 0 - проверки падают
     - mysql-monitor.check_age[*] -- возраст проверки в секундах, >130 - плохо...

     - mysql-monitor.slave_behind[*]
     - mysql-monitor.slave_running[*] -- 1 - репликация работает, 0 - не работает

     - mysql-monitor.queries_cnt[*] -- количество сессий
     - mysql-monitor.queries_active[*] -- количество активных сессий
     - mysql-monitor.queries_connect[*] -- количество сессий в состоянии connect
     - mysql-monitor.queries_sleep[*] -- количество неактивных сессий
     - mysql-monitor.queries_slow[*] -- количество запросов, работающих дольше 60-ти сек
     - mysql-monitor.queries_qps[*] -- количество запросов в секунду (с момента старта сервера)

     - mysql-monitor.checksum_errors[*] -- количество ошибок, обнаруженных последней проверкой консистентности
     - mysql-monitor.checksum_active[*] -- 1 - проверка консистентности жива, 0 - наверное мертва
     Внимание: эти мониторинги зажигаются только после 10 утра

=cut

use strict;
use Getopt::Long;

use YAML;
use Carp;
use DBI;
use File::Path qw/mkpath/;
use File::Slurp;
use List::Util qw/max/;
use Time::Local;
use Pid::File::Flock qw/:auto/;

use utf8;
use open ':std' => ':utf8';

# максимальный возраст проверки консистентности - 2.5 дня
my $MAX_CHECKSUM_AGE = 2.5 * 24 * 3600;

my $CONF_DIR = '/etc/mysql-monitor.d';
my $STATUS_DIR = '/var/spool/mysql-monitor';
my $TIMEOUT = 10;

# порог срабатывания на медленные запросы
my $SLOW_THRESHOLD = 60;

GetOptions(
    "help" => sub {system("podselect -section NAME -section DESCRIPTION $0 | pod2text-utf8 >&2"); exit 0;},
    "conf-dir=s" => \$CONF_DIR,
    "status-dir=s" => \$STATUS_DIR,
    "timeout=i" => \$TIMEOUT,
    ) || die "Error occured";

if (!-d $STATUS_DIR) {
    mkpath $STATUS_DIR, 0, 0755;
}

# цикл по всем конфигам
opendir(my $dh, $CONF_DIR) || die "Can't open dir $CONF_DIR: $!";
for my $file (sort grep {!/\./} readdir($dh)) {
    my $status = eval {
        local $SIG{ALRM} = sub {die "TIMEOUT"};
        alarm($TIMEOUT);
        process_config($file);
    };
    alarm(0);
    if ($@) {
        $status = {error => "$@"};
    }
    my $status_file = "$STATUS_DIR/$file";
    # считываем предыдущее состояние
    my $prev_status;
    if (-f $status_file) {
        $prev_status = eval { YAML::LoadFile($status_file); };
        carp $@ if !$prev_status;
    };
    # ночью не зажигаем checksum warning
    #my $hour = (localtime)[2];
    #if (exists $status->{checksum_active} && $prev_status && $hour < 10) {
    #    $status->{checksum_active} = $prev_status->{checksum_active} if !$status->{checksum_active};
    #    $status->{checksum_errors} = $prev_status->{checksum_errors} if $status->{checksum_errors};
    #}
    write_file($status_file, {atomic => 1}, YAML::Dump($status));
}
closedir($dh) || die "Can't close $CONF_DIR: $!";

sub process_config {
    my ($config_name) = @_;

    # читаем, проверяем конфиг
    my $conf = read_config("$CONF_DIR/$config_name");

    # читаем пароль, если есть
    $conf->{password} = '';
    eval {
        open(my $pwd_file, '<', "/etc/mysql/${config_name}-root") || die "Can't open file";
        chomp($conf->{password} = <$pwd_file> || '');
    };

    # коннектимся в базу
    my $dbh = DBI->connect("DBI:mysql:;mysql_socket=$conf->{socket};mysql_enable_utf8=1", "root", $conf->{password}, {RaiseError => 1, PrintError => 0});

    my %status;

    # данные о сессиях в бд
    my $processes = $dbh->selectall_arrayref("show processlist", {Slice => {}});
    $status{queries_cnt} = @$processes;
    $status{queries_active} = grep {$_->{Command} eq 'Query'} @$processes;
    $status{queries_sleep} = grep {$_->{Command} eq 'Sleep'} @$processes;
    $status{queries_connect} = grep {$_->{Command} eq 'Connect'} @$processes;
    $status{slaves_num} = grep {$_->{Command} eq 'Binlog Dump'} @$processes;
    $status{queries_slow} = grep {$_->{Command} eq 'Query' && $_->{Time} >= $SLOW_THRESHOLD} @$processes;    

    # статистическая информация о работе бд
    my $mysql_status = $dbh->selectall_hashref("show status", "Variable_name");
    $status{queries_qps} = sprintf('%.2f', $mysql_status->{Queries}->{Value} / $mysql_status->{Uptime}->{Value});

    # Percona Xtradb Cluster specific
    if (exists $mysql_status->{wsrep_cluster_status})
    {
        $status{wsrep_cluster_status}= $mysql_status->{wsrep_cluster_status}->{Value};
        $status{wsrep_connected} = $mysql_status->{wsrep_connected}->{Value};
        $status{wsrep_local_state} = $mysql_status->{wsrep_local_state}->{Value};
        $status{wsrep_local_state_comment} = $mysql_status->{wsrep_local_state_comment}->{Value};
        $status{wsrep_cluster_size} = $mysql_status->{wsrep_cluster_size}->{Value};
    }

    # информация о репликации
    my $slave_status = $dbh->selectrow_hashref("show slave status");
    my $wsrep_status = $dbh->selectrow_hashref("show variables like 'wsrep_on'");
    $status{has_slave} = 1;
    $status{has_slave} = 0 if not defined $slave_status && not defined $wsrep_status;
    $status{slave_running} = $slave_status
        && $slave_status->{Slave_IO_Running} && $slave_status->{Slave_IO_Running} eq 'Yes'
        && $slave_status->{Slave_SQL_Running} && $slave_status->{Slave_SQL_Running} eq 'Yes'
        ? 1 : 0
        ;
    $status{slave_behind} = $slave_status ? $slave_status->{Seconds_Behind_Master} || 0 : 0;

    # проверка чексуммы (при использовании mk-table-checksum)
    if ($conf->{checksum_result_table}) {
        $status{checksum_active} = undef;
        $status{checksum_errors} = undef;
        eval {
           my $checksum_result = $dbh->selectall_hashref("SELECT name, value FROM $conf->{checksum_result_table}", "name");
            my $check_ts = ($checksum_result->{check_time}->{value}||'') =~ /^(\d{4})-?(\d{2})-?(\d{2}) ?(\d{2}):(\d{2}):(\d{2})$/
                ? timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900)
                : 0;
            $status{checksum_active} = $checksum_result->{system_error}->{value} || time - $check_ts > $MAX_CHECKSUM_AGE ? 0 : 1;
            $status{checksum_errors} = $checksum_result->{errors_cnt}->{value} || 0;
        }
    }

    # проверка возраста самой долгой транзакции
    my $innodb_trx_exists = !! @{$dbh->selectcol_arrayref("DESC INFORMATION_SCHEMA.INNODB_TRX")};
    if ($innodb_trx_exists) {
        # Вернуть старый вариант после остановки Transfer Manage
        # ($status{trx_max_age}) = $dbh->selectrow_array("SELECT ifnull(max(unix_timestamp() - unix_timestamp(trx_started)), 0) trx_max_age FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_lock_structs > 0 OR trx_query IS NOT NULL");

        ($status{trx_max_age}) = $dbh->selectrow_array("SELECT ifnull(max(unix_timestamp() - unix_timestamp(trx_started)), 0) trx_max_age FROM INFORMATION_SCHEMA.INNODB_TRX trx LEFT JOIN INFORMATION_SCHEMA.PROCESSLIST proc ON trx.trx_mysql_thread_id = proc.id  WHERE (proc.user is null or proc.user != 'loaddb') AND (trx_lock_structs > 0 OR trx_query IS NOT NULL)");
    } else {
        $status{trx_max_age} = 0;
    }

    # проверка возраста самого долгого запроса в rbac
    # trx_max_age не годится, потому что в rbac по какой-то причине таблица INFORMATION_SCHEMA.INNODB_TRX всегда пустая: https://st.yandex-team.ru/DIRECTADMIN-6132#1524067494000
    # "пишущие запросы" для начала определяем как "не SELECT", условие можно по необходимости уточнить.
    # на остальных mysql-ях в таком мониторинге пока не ощущается необходимости: есть мониторинг trx_max_age и прибивалки запросов в ppcdata
    my $is_rbac2 = grep {/^rbac2/} `/usr/local/bin/lm --complete`;
    if ($is_rbac2) {
        my @write_processes = grep {
            $_->{Info} &&
            $_->{Info} !~ /^SELECT/i &&
            ($_->{db} // '') eq 'rbac2'
        } @$processes;
        $status{rbac2_write_query_max_time} = max(map {int($_->{Time})} @write_processes) // 0;
    } else {
        $status{rbac2_write_query_max_time} = 0;
    }
    
    # он жив!
    $status{alive} = 1;

    return \%status;
}

sub read_config {
    my ($file) = @_;
    my $conf = YAML::LoadFile($file);
    if (!exists $conf->{socket}) {
        die "Error in $file: socket is not defined";
    }
    return $conf;
}
