#!/usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md

"""See the check's README for more details."""

import argparse
import sys

import lib.args
import lib.base
import lib.db_mysql
import lib.db_sqlite
import lib.human
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2026051002'

DESCRIPTION = """Checks two related InnoDB log buffer metrics in MySQL/MariaDB:
1. **Log waits** (`Innodb_log_waits` / `Innodb_log_writes`) - how often InnoDB had to wait for
   log writes to be flushed because the log buffer was full. Anything above 0% indicates that
   `innodb_log_buffer_size` should be increased.
2. **Write log efficiency** ((`Innodb_log_write_requests` - `Innodb_log_writes`) /
   `Innodb_log_write_requests` * 100) - how many log write requests were absorbed by the buffer
   without needing a physical disk write. Below 90% indicates `innodb_log_buffer_size` is too
   small for the write workload.
Alerts on either condition."""

DEFAULT_DEFAULTS_FILE = '/var/spool/icinga2/.my.cnf'
DEFAULT_DEFAULTS_GROUP = 'client'
DEFAULT_TIMEOUT = 3

SQLITE_DB = 'linuxfabrik-monitoring-plugins-mysql-innodb-log-waits.db'


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(description=DESCRIPTION)

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version=f'%(prog)s: v{__version__} by {__author__}',
    )

    parser.add_argument(
        '--always-ok',
        help=lib.args.help('--always-ok'),
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--defaults-file',
        help='MySQL/MariaDB cnf file to read user, host and password from (instead of specifying them on the command line). '
        'Example: `/var/spool/icinga2/.my.cnf`. '
        'Default: %(default)s',
        dest='DEFAULTS_FILE',
        default=DEFAULT_DEFAULTS_FILE,
    )

    parser.add_argument(
        '--defaults-group',
        help=lib.args.help('--defaults-group') + ' Default: %(default)s',
        dest='DEFAULTS_GROUP',
        default=DEFAULT_DEFAULTS_GROUP,
    )

    parser.add_argument(
        '--timeout',
        help=lib.args.help('--timeout') + ' Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    args, _ = parser.parse_known_args()
    return args


def get_vars(conn):
    # Do not implement `get_all_vars()`, just fetch the ones we need for this check.
    sql = """
        show global variables
        where variable_name like 'innodb_log_buffer_size'
            ;
          """
    return lib.base.coe(lib.db_mysql.select(conn, sql))


def get_status(conn):
    # Do not implement `get_all_vars()`, just fetch the ones we need for this check.
    sql = """
        show global status
        where variable_name like 'Innodb_log_waits'
            or variable_name like 'Innodb_log_writes'
            or variable_name like 'Innodb_log_write_requests'
            ;
          """
    return lib.base.coe(lib.db_mysql.select(conn, sql))


def main():
    """The main function. This is where the magic happens."""

    # logic taken from mysqltuner.pl:mysql_innodb(), sections "InnoDB Write Log
    # efficiency" and "InnoDB Log Waits", verified in sync with MySQLTuner
    # (the < 90% write-efficiency threshold and the "any waits at all" log-wait
    # threshold are unchanged upstream since the original port).

    # parse the command line
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    # fetch data
    mysql_connection = {
        'defaults_file': args.DEFAULTS_FILE,
        'defaults_group': args.DEFAULTS_GROUP,
        'timeout': args.TIMEOUT,
    }
    conn = lib.base.coe(lib.db_mysql.connect(mysql_connection))
    lib.base.coe(lib.db_mysql.check_privileges(conn))

    myvar = lib.db_mysql.lod2dict(get_vars(conn))
    mystat = lib.db_mysql.lod2dict(get_status(conn))
    engines = lib.db_mysql.get_engines(conn)
    lib.db_mysql.close(conn)

    # InnoDB engine availability. Mirror mysqltuner's `infoprint` semantic - missing or
    # disabled engine is a config decision, not an unknown state.
    if (
        not engines.get('have_innodb', '')
        or engines['have_innodb'] != 'YES'
    ):
        lib.base.oao(
            'InnoDB Storage Engine not available or disabled.',
            STATE_OK,
            always_ok=args.ALWAYS_OK,
        )

    # init some vars
    # cache int conversions; older MySQL might not expose `Innodb_log_write_requests`
    log_buffer_size = int(myvar['innodb_log_buffer_size'])
    log_waits = int(mystat.get('Innodb_log_waits', 0))
    log_writes = int(mystat.get('Innodb_log_writes', 0))
    log_write_requests_raw = mystat.get('Innodb_log_write_requests')
    log_write_requests = (
        int(log_write_requests_raw) if log_write_requests_raw is not None else None
    )

    state = STATE_OK
    sections = []
    # All recommendations from all WARN paths land here and render once at the
    # end as a `Recommendations:\n* ...` bulleted block, regardless of which
    # combinations of WARN paths fire.
    recommendations = []

    # analyze data
    # 1. InnoDB Log Waits: any wait at all earns a WARN. mysqltuner uses the same
    #    "any wait" threshold (`> 0.000001` in their float math).
    if log_writes > 0:
        pct_log_waits = round(log_waits / log_writes * 100, 4)
    else:
        pct_log_waits = 0.0

    log_waits_msg = (
        f'InnoDB log waits: {pct_log_waits}%'
        f' ({lib.human.number2human(log_waits)} waits /'
        f' {lib.human.number2human(log_writes)} writes)'
    )
    if log_waits > 0:
        state = lib.base.get_worst(state, STATE_WARN)
        sections.append(
            f'{log_waits_msg}{lib.base.state2str(STATE_WARN, prefix=" ")}.'
        )
        recommendations.append(
            f'Set `innodb_log_buffer_size` >'
            f' {lib.human.bytes2human(log_buffer_size)}'
        )
    else:
        sections.append(f'{log_waits_msg}.')

    # 2. InnoDB Write Log efficiency: only computable when the server exposes
    #    `Innodb_log_write_requests` (older MySQL did not). mysqltuner emits a
    #    "metrics not reliable" infoprint when `writes > write_requests` (which
    #    cannot happen physically); we mirror that as an info line.
    if log_write_requests is not None:
        if log_write_requests == 0:
            sections.append(
                'InnoDB Write Log efficiency: no log write requests yet.'
            )
        elif log_writes > log_write_requests:
            sections.append(
                f'InnoDB Write Log efficiency: metrics are not reliable'
                f' (Innodb_log_writes {log_writes} > Innodb_log_write_requests'
                f' {log_write_requests}).'
            )
        else:
            pct_write_eff = round(
                (log_write_requests - log_writes) / log_write_requests * 100, 1
            )
            absorbed = log_write_requests - log_writes
            eff_msg = (
                f'InnoDB Write Log efficiency: {pct_write_eff}%'
                f' ({lib.human.number2human(absorbed)} log buffer hits /'
                f' {lib.human.number2human(log_write_requests)} total)'
            )
            if pct_write_eff < 90:
                state = lib.base.get_worst(state, STATE_WARN)
                sections.append(
                    f'{eff_msg}{lib.base.state2str(STATE_WARN, prefix=" ")}.'
                )
                rec = (
                    f'Set `innodb_log_buffer_size` >'
                    f' {lib.human.bytes2human(log_buffer_size)}'
                )
                if rec not in recommendations:
                    recommendations.append(rec)
            else:
                sections.append(f'{eff_msg}.')

    # build the message
    if recommendations:
        sections.append(
            'Recommendations:\n' + '\n'.join(f'* {r}' for r in recommendations)
        )

    msg = '\n\n'.join(sections)

    # Per-CONTRIBUTING: counters are emitted as in-plugin per-second deltas instead
    # of `uom='c'`. The percentages above are ratios of cumulative values and stay
    # correct even with the cumulative source readings.
    rates = lib.db_sqlite.per_second_deltas(
        SQLITE_DB,
        'mysql-innodb-log-waits',
        {
            'innodb_log_waits': log_waits,
            'innodb_log_writes': log_writes,
            'innodb_log_write_requests': log_write_requests or 0,
        },
    )

    perfdata = ''
    perfdata += lib.base.get_perfdata(
        'mysql_innodb_log_buffer_size',
        log_buffer_size,
        uom='B',
        _min=0,
    )
    if rates is not None:
        perfdata += lib.base.get_perfdata(
            'mysql_innodb_log_waits_per_second',
            int(rates['innodb_log_waits']),
            _min=0,
        )
        perfdata += lib.base.get_perfdata(
            'mysql_innodb_log_writes_per_second',
            int(rates['innodb_log_writes']),
            _min=0,
        )
        if log_write_requests is not None:
            perfdata += lib.base.get_perfdata(
                'mysql_innodb_log_write_requests_per_second',
                int(rates['innodb_log_write_requests']),
                _min=0,
            )
    perfdata += lib.base.get_perfdata(
        'mysql_innodb_log_waits_pct',
        pct_log_waits,
        uom='%',
        _min=0,
        _max=100,
    )
    if log_write_requests is not None and log_write_requests > 0 and log_writes <= log_write_requests:
        perfdata += lib.base.get_perfdata(
            'mysql_innodb_write_log_efficiency_pct',
            round(
                (log_write_requests - log_writes) / log_write_requests * 100, 1
            ),
            uom='%',
            _min=0,
            _max=100,
        )

    # over and out
    lib.base.oao(msg, state, perfdata, always_ok=args.ALWAYS_OK)


if __name__ == '__main__':
    try:
        main()
    except Exception:
        lib.base.cu()
