#!/usr/lib64/linuxfabrik-monitoring-plugins/venv/bin/python
# -*- 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__ = '2026071201'

DESCRIPTION = """Checks InnoDB redo log buffer health in MySQL/MariaDB.

Primary check - **Log waits** (`Innodb_log_waits` / `Innodb_log_writes`): how often InnoDB had to
wait because the in-memory log buffer was full before its contents could be flushed to disk. Per
the MariaDB InnoDB source this counter is the authoritative signal for an undersized log buffer
("Number of log waits due to small log buffer"). Any value above 0 means `innodb_log_buffer_size`
was too small for the write workload at some point, so the plugin alerts and recommends a larger
buffer.

Secondary informational metric - **Write log efficiency** ((`Innodb_log_write_requests` -
`Innodb_log_writes`) / `Innodb_log_write_requests` * 100): the share of in-memory log appends that
were batched into a shared physical write. This ratio is governed by group commit and
`innodb_flush_log_at_trx_commit`, not by buffer size, so the plugin reports it for trending but
never alerts on it and never recommends resizing the buffer based on it.

Deliberate deviation from MySQLTuner: MySQLTuner alerts and recommends increasing
`innodb_log_buffer_size` whenever write log efficiency drops below 90%. The MariaDB InnoDB source
does not support that link, so this plugin treats write log efficiency as informational only and
alerts solely on log waits."""

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,
        epilog=lib.args.epilog(__file__),
        formatter_class=lib.args.HelpFormatter,
    )

    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(
        '--no-perfdata',
        help=lib.args.help('--no-perfdata'),
        dest='NO_PERFDATA',
        action='store_true',
        default=False,
    )

    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 derived from mysqltuner.pl:mysql_innodb(), sections "InnoDB Write Log
    # efficiency" and "InnoDB Log Waits". Deliberate deviation from MySQLTuner:
    # only `Innodb_log_waits > 0` alerts and recommends a larger
    # `innodb_log_buffer_size`. Per the MariaDB InnoDB source, `Innodb_log_waits`
    # (`log_sys.waits`, incremented only on the WRITE_BACKOFF path when an append
    # must wait for a full buffer) is the sole counter that signals a too-small
    # log buffer - srv0mon.cc labels it "Number of log waits due to small log
    # buffer". The Write Log efficiency ratio Innodb_log_writes /
    # Innodb_log_write_requests instead measures how many in-memory log appends
    # were batched into a shared physical write (group commit /
    # `innodb_flush_log_at_trx_commit` behavior), which is unrelated to buffer
    # capacity. MySQLTuner alerts and recommends a bigger buffer when that ratio
    # drops below 90%; since the source does not support that, we emit it as an
    # informational metric only.

    # 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).
    pct_log_waits = round(log_waits / log_writes * 100, 4) if log_writes > 0 else 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` > {lib.human.bytes2human(log_buffer_size)}'
        )
    else:
        sections.append(f'{log_waits_msg}.')

    # 2. InnoDB Write Log efficiency: informational only. Unlike MySQLTuner (see
    #    the note at the top of main()) this never alerts and never recommends a
    #    larger buffer, because the ratio reflects group commit /
    #    `innodb_flush_log_at_trx_commit` batching, not buffer capacity. Only
    #    computable when the server exposes `Innodb_log_write_requests` (older
    #    MySQL did not). `writes > write_requests` cannot happen physically and
    #    briefly appears only during counter resets, so it is reported as info.
    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
            )
            batched = log_write_requests - log_writes
            sections.append(
                f'InnoDB Write Log efficiency: {pct_write_eff}%'
                f' ({lib.human.number2human(batched)} batched /'
                f' {lib.human.number2human(log_write_requests)} log write requests).'
            )

    # 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, no_perfdata=args.NO_PERFDATA
    )


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