#!/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  # pylint: disable=C0413
import json  # pylint: disable=C0413
import sys  # pylint: disable=C0413

import lib.args  # pylint: disable=C0413
import lib.base  # pylint: disable=C0413
import lib.human  # pylint: disable=C0413
import lib.lftest  # pylint: disable=C0413
import lib.time  # pylint: disable=C0413
import lib.txt  # pylint: disable=C0413
import lib.url  # pylint: disable=C0413
from lib.globals import (STATE_CRIT, STATE_OK,  # pylint: disable=C0413
                          STATE_UNKNOWN, STATE_WARN)

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

DESCRIPTION = """This check collects information from the PHP-FPM status page and alerts on certain
                overuse. In addition, a table is printed which contains each pool process in the
                status "Running" (information relates to the current request that is being
                served)."""

DEFAULT_CRIT = 90   # %
DEFAULT_CRIT_SLOW_REQUESTS = 100
DEFAULT_INSECURE = False
DEFAULT_LENGTHY = True
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT = 8
DEFAULT_URL = 'http://localhost/fpm-status' # ?json&full
DEFAULT_WARN = 80   # %
DEFAULT_WARN_SLOW_REQUESTS = 1


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

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

    parser.add_argument(
        '--always-ok',
        help='Always returns OK.',
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '-c', '--critical',
        help='Set the CRIT threshold for queue usage as a percentage. '
             'Default: >= %(default)s',
        dest='CRIT',
        type=int,
        default=DEFAULT_CRIT,
    )

    parser.add_argument(
        '--critical-slowreq',
        help='Set the CRIT threshold for slow requests. '
             'Default: >= %(default)s',
        dest='CRIT_SLOW_REQUESTS',
        type=int,
        default=DEFAULT_CRIT_SLOW_REQUESTS,
    )

    parser.add_argument(
        '--insecure',
        help='This option explicitly allows to perform "insecure" SSL connections. '
             'Default: %(default)s',
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--lengthy',
        help='Extended reporting.',
        dest='LENGTHY',
        action='store_true',
        default=DEFAULT_LENGTHY,
    )

    parser.add_argument(
        '--no-proxy',
        help='Do not use a proxy. '
             'Default: %(default)s',
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    parser.add_argument(
        '--test',
        help='For unit tests. Needs "path-to-stdout-file,path-to-stderr-file,expected-retc".',
        dest='TEST',
        type=lib.args.csv,
    )

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

    parser.add_argument(
        '-u', '--url',
        help='PHP-FPM Status URL. '
             'Default: %(default)s',
        dest='URL',
        default=DEFAULT_URL,
        )

    parser.add_argument(
        '-w', '--warning',
        help='Set the WARN threshold for queue usage as a percentage. '
             'Default: >= %(default)s',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    parser.add_argument(
        '--warning-slowreq',
        help='Set the WARN threshold for slow requests. '
             'Default: >= %(default)s',
        dest='WARN_SLOW_REQUESTS',
        type=int,
        default=DEFAULT_WARN_SLOW_REQUESTS,
    )

    return parser.parse_args()


def main():
    """The main function. Hier spielt die Musik.
    """

    # parse the command line, exit with UNKNOWN if it fails
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    # fetch data
    if args.TEST is None:
        jsonst = lib.base.coe(lib.url.fetch(
            args.URL + '?json&full',
            insecure=args.INSECURE,
            no_proxy=args.NO_PROXY,
            timeout=args.TIMEOUT,
        ))
    else:
        # do not call the command, put in test data
        stdout, _, _ = lib.lftest.test(args.TEST)
        jsonst = stdout

    try:
        result = json.loads(jsonst)
    except:
        lib.base.cu('ValueError: No JSON object could be decoded')

    # init some vars
    msg = ''
    state = STATE_OK
    perfdata = ''
    table_values = []

    # analyze data
    if result['listen queue len']:
        queue_usage = round(
            float(result['listen queue']) / float(result['listen queue len']) * 100,
            1,
        )
    else:
        queue_usage = 0

    queue_state = lib.base.get_state(
        queue_usage,
        args.WARN,
        args.CRIT,
    )
    state = lib.base.get_worst(queue_state, state)
    slow_req_state = lib.base.get_state(
        result['slow requests'],
        args.WARN_SLOW_REQUESTS,
        args.CRIT_SLOW_REQUESTS,
    )
    state = lib.base.get_worst(slow_req_state, state)

    # build the message

    # Basic information - Always displayed on the status page Parameter
    msg += 'Pool {} ({}): '.format(result['pool'], result['process manager'])
    if result['listen queue']:
        msg += '{}/{} {} waiting for a free process in listen queue ({}%){}, '.format(
            result['listen queue'],
            result['listen queue len'],
            lib.txt.pluralize('req', result['listen queue']),
            queue_usage,
            lib.base.state2str(queue_state, prefix=' '),
        )
    if result.get('max children reached', 0):
        children_state = STATE_WARN
        state = lib.base.get_worst(children_state, state)
        msg += '"max children" was reached at least once{}, '.format(
            lib.base.state2str(children_state, prefix=' '),
        )
    if result.get('slow requests', 0):
        msg += '{} slow {} {} hit the configured `request_slowlog_timeout`{}, '.format(
            result['slow requests'],
            lib.txt.pluralize('req', result['slow requests']),
            lib.txt.pluralize('', result['slow requests'], 'has,have'),
            lib.base.state2str(slow_req_state, prefix=' '),
        )
    msg += '{} connections, '.format(
        lib.human.number2human(result['accepted conn']),
    )
    msg += '{} processes ({} active, {} idle), '.format(
        result['total processes'],
        result['active processes'],
        result['idle processes'],
    )
    msg += 'Up {} (since {}), '.format(
        lib.human.seconds2human(result['start since']),
        lib.time.epoch2iso(result['start time']),
    )

    # Per-process information - only displayed in full output mode
    for value in result.get('processes', ''):
        if not value:
            continue
        if value['state'] != 'Running':
            continue
        # do not monitor our own url used for monitoring
        if '?json&full' in value['request uri']:
            continue
        table_values.append({
            'pid': value['pid'],
            'state': value['state'],
            'start time': '{} ({} ago)'.format(
                lib.time.epoch2iso(value['start time']),
                lib.human.seconds2human(value['start since']),
            ),
            'requests': value['requests'],
            # microseconds:
            'request duration': lib.human.seconds2human(int(value['request duration']) / 1000000),
            'request method': value['request method'],
            'request uri': value['request uri'][:80] + '...' if len(value['request uri']) > 80 else value['request uri'], # pylint: disable=C0301
            'content length': lib.human.bytes2human(value['content length']) if value['content length'] else '-', # pylint: disable=C0301
            'user': value['user'],
            'script': value['script'],
            'last request cpu': value['last request cpu'],
            'last request memory': value['last request memory'],
        })

    msg = msg[:-2] + '\n\n'
    if len(table_values) > 0:
        if not args.LENGTHY:
            keys = [
                'pid',
                'requests',
                'request duration',
                'request method',
                'request uri',
                'user',
            ]
            headers = [
                'PID',
                'Reqs',
                'LastReqDur',
                'LastMthd',
                'Last Request URI',
                'AuthUser',
            ]
        else:
            keys = [
                'pid',
                'state',
                'start time',
                'requests',
                'request duration',
                'request method',
                'content length',
                # 'last request cpu',
                # 'last request memory',
                'request uri',
                'script',
                'user',
            ]
            headers = [
                'PID',
                'State',
                'Process Start',
                'Reqs',
                'LastReqDur',
                'LastMthd',
                'LastContLen',
                # 'LastCPU',  # the calculation is done when the request processing is complete
                # 'LastMem',  # the calculation is done when the request processing is complete
                'Last Request URI',
                'Script',
                'AuthUser',
            ]
        msg += lib.base.get_table(table_values, keys, headers)
        msg += '\nFor details, see https://www.php.net/manual/en/fpm.status.php'

    perfdata += lib.base.get_perfdata('accepted conn', result['accepted conn'], 'c', None, None, 0, None) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('active processes', result['active processes'], None, None, None, 0, result['total processes']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('idle processes', result['idle processes'], None, None, None, 0, result['total processes']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('listen queue', result['listen queue'], None, None, None, 0, result['listen queue len']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('listen queue len', result['listen queue len'], None, None, None, 0, None) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('max children reached', result['max children reached'], None, None, None, 0, None) # pylint: disable=C0301
    if 'slow requests' in result:
        perfdata += lib.base.get_perfdata('slow requests', result['slow requests'], None, args.WARN_SLOW_REQUESTS, args.CRIT_SLOW_REQUESTS, 0, None) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('start since', result['start since'], 's', None, None, 0, None) # pylint: disable=C0301

    perfdata += lib.base.get_perfdata('queue usage', queue_usage, '%', args.WARN, args.CRIT, 0, 100)

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


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