#!/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_sqlite
import lib.disk
import lib.human
import lib.lftest
import lib.txt
from lib.globals import STATE_OK, STATE_UNKNOWN

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

DESCRIPTION = """Reports XFS filesystem activity from /proc/fs/xfs/stat as per-second rates: read
and write system calls, inode cache hit ratio, inode reclaim activity, and directory operations,
plus the current number of active inodes. Intended for trending I/O and metadata workload on XFS
volumes. This check is informational and does not raise alerts."""


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

    parser.add_argument(
        '--test',
        help=lib.args.help('--test'),
        dest='TEST',
        type=lib.args.csv,
    )

    args, _ = parser.parse_known_args()
    return args


def field(stats, label, idx, count):
    """Return one integer value from a parsed /proc/fs/xfs/stat line, or fail
    with UNKNOWN if the line is missing or has fewer values than expected.

    The value layout of /proc/fs/xfs/stat is defined by the kernel in
    fs/xfs/xfs_stats.c; callers pass the field index according to that layout.
    """
    values = stats.get(label)
    if not values or len(values) < count:
        lib.base.cu(f'Unexpected format for XFS "{label}" statistics.')
    return int(values[idx])


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

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

    # fetch data
    if args.TEST is None:
        # /proc/fs/xfs/stat reports a size of 0 like most proc files, so
        # allow_empty=True is required to detect its presence.
        if not lib.disk.file_exists('/proc/fs/xfs/stat', allow_empty=True):
            lib.base.cu('No mounted XFS filesystem found.')
        raw = lib.base.coe(lib.disk.read_file('/proc/fs/xfs/stat'))
    else:
        raw, _, _ = lib.lftest.test(args.TEST)

    # map each /proc/fs/xfs/stat line to its label: {'rw': ['123', '456'], ...}
    stats = {row[0]: row[1:] for row in lib.txt.mltext2array(raw) if row}

    # We only read the fields that are actually meaningful to a Linux admin.
    # The rest of /proc/fs/xfs/stat (allocator, btree and block-mapping
    # internals) is XFS-developer debugging territory, and several "vnodes"
    # fields are unused legacy slots that always read zero on modern kernels.
    #
    # vnodes: xs_inodes_active is a gauge (current active inodes), not a
    # counter, and is always available, even on the first run.
    active_inodes = field(stats, 'vnodes', 0, 8)
    perfdata = lib.base.get_perfdata('active_inodes', active_inodes, uom=None, _min=0)

    # Convert the cumulative counters to per-second rates against the previous
    # run, stored in this plugin's own SQLite cache, instead of emitting
    # uom='c' continuous counters (issue #320). The field offsets follow the
    # /proc/fs/xfs/stat layout defined in the kernel (fs/xfs/xfs_stats.c):
    #   rw:  write_calls read_calls
    #   ig:  attempts found recycle missed dup reclaims attrchg
    #   dir: lookup create remove getdents
    rates = lib.db_sqlite.per_second_deltas(
        'linuxfabrik-monitoring-plugins-fs-xfs-stats.db',
        'fs-xfs-stats',
        {
            'dir_create': field(stats, 'dir', 1, 4),
            'dir_lookup': field(stats, 'dir', 0, 4),
            'dir_remove': field(stats, 'dir', 2, 4),
            'ig_attempts': field(stats, 'ig', 0, 7),
            'ig_found': field(stats, 'ig', 1, 7),
            'ig_reclaims': field(stats, 'ig', 5, 7),
            'read_calls': field(stats, 'rw', 1, 2),
            'write_calls': field(stats, 'rw', 0, 2),
        },
    )
    if rates is None:
        # first run, cache wiped, or counter reset (fresh mount): no delta yet
        lib.base.oao(
            'Waiting for more data.', STATE_OK, perfdata, no_perfdata=args.NO_PERFDATA
        )

    # inode cache hit ratio over the interval (delta-based, so it reflects the
    # current workload instead of converging to a lifetime average)
    if rates['ig_attempts'] > 0:
        inode_cache_hit_percent = round(
            rates['ig_found'] / rates['ig_attempts'] * 100, 1
        )
    else:
        inode_cache_hit_percent = 100.0

    perfdata += lib.base.get_perfdata(
        'dir_create_per_second', round(rates['dir_create'], 1), uom=None, _min=0
    )
    perfdata += lib.base.get_perfdata(
        'dir_lookup_per_second', round(rates['dir_lookup'], 1), uom=None, _min=0
    )
    perfdata += lib.base.get_perfdata(
        'dir_remove_per_second', round(rates['dir_remove'], 1), uom=None, _min=0
    )
    perfdata += lib.base.get_perfdata(
        'inode_attempts_per_second', round(rates['ig_attempts'], 1), uom=None, _min=0
    )
    perfdata += lib.base.get_perfdata(
        'inode_cache_hit_percent', inode_cache_hit_percent, uom='%', _min=0, _max=100
    )
    perfdata += lib.base.get_perfdata(
        'inode_reclaims_per_second', round(rates['ig_reclaims'], 1), uom=None, _min=0
    )
    perfdata += lib.base.get_perfdata(
        'read_calls_per_second', round(rates['read_calls'], 1), uom=None, _min=0
    )
    perfdata += lib.base.get_perfdata(
        'write_calls_per_second', round(rates['write_calls'], 1), uom=None, _min=0
    )

    # build the message
    msg = (
        f'{lib.human.number2human(rates["read_calls"])} read/s, '
        f'{lib.human.number2human(rates["write_calls"])} write/s, '
        f'inode cache hit {inode_cache_hit_percent}%, '
        f'{lib.human.number2human(active_inodes)} active inodes'
    )

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


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