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

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

DESCRIPTION = """Monitors Apache httpd performance via the mod_status endpoint (server-status?auto).
Alerts when worker usage exceeds the configured thresholds. Reports busy and idle
workers, request rates, bytes served, CPU load, connection states, and system load
averages. Requires "ExtendedStatus On" in the Apache configuration for full metrics.
Uses a local SQLite database to calculate per-second rates from cumulative counters."""

DEFAULT_CRIT = 95  # %
DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT = 8
DEFAULT_URL = 'http://localhost/server-status'
DEFAULT_WARN = 80  # %


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(
        '-c',
        '--critical',
        help='CRIT threshold for the percentage of workers processing requests. '
        'Default: >= %(default)s',
        dest='CRIT',
        type=int,
        default=DEFAULT_CRIT,
    )

    parser.add_argument(
        '--insecure',
        help=lib.args.help('--insecure'),
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--no-proxy',
        help=lib.args.help('--no-proxy'),
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

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

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

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

    parser.add_argument(
        '-w',
        '--warning',
        help='WARN threshold for the percentage of workers processing requests. '
        'Default: >= %(default)s',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    args, _ = parser.parse_known_args()
    return args


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:
        URL = args.URL
        if URL[0:4] != 'http':
            lib.base.oao(
                '--url parameter has to start with "http://" or https://".',
                STATE_UNKNOWN,
            )
        URL = URL + '?auto'

        # fetch the URL
        result = lib.base.coe(
            lib.url.fetch(
                URL,
                insecure=args.INSECURE,
                no_proxy=args.NO_PROXY,
                timeout=args.TIMEOUT,
            )
        )
        if 'Scoreboard: ' not in result:
            lib.base.cu('Malformed Apache server info.')
    else:
        # do not call the command, put in test data
        stdout, _stderr, _retc = lib.lftest.test(args.TEST)
        result = stdout
    result = result.strip().split('\n')

    # init some vars
    state = STATE_OK
    perfdata = ''
    apache = {}
    workers = {}

    apache['ServerName'] = 'N/A'

    # analyze data
    # Apache mod_status Source:
    # https://github.com/apache/httpd/blob/trunk/modules/generators/mod_status.c
    # We just want "raw" values that provide a real-time / just-in-time measurement. Values
    # which are calculated by Apache, for example over its service uptime and therefore
    # returning an average over time will be ignored in perfdata.
    # See https://github.com/Linuxfabrik/monitoring-plugins/issues//310.
    # Some continous counters have to be re-calculated (Value_Now - Value_Previous = CurrentValue)
    for item in result:
        if ': ' not in item:
            if item != 'TLSSessionCacheStatus':
                apache['ServerName'] = item
            continue

        try:
            key, value = item.split(': ')
        except Exception:
            continue

        if key == 'ServerUptimeSeconds':
            apache['ServerUptimeSeconds'] = int(value)  # continous counter
            continue
        if key == 'Total Accesses':
            apache['Total Accesses'] = int(value)
            continue
        if key == 'Total kBytes':
            apache['Total Bytes'] = int(value) * 1024
            continue
        if key == 'Total Duration':
            apache['Total Duration'] = float(value) / 1000  # convert msec to sec
            continue

        apache[key] = value

    # === The Bascis ===============================================================================
    # first analyze all the items we get even if "Extended Status" is "Off"

    # parse the "scoreboard" and count the elements
    apache['TotalWorkers'] = len(apache['Scoreboard'])
    if apache['TotalWorkers'] == 0:
        lib.base.cu('Malformed Apache server info.')

        # from mod_status.c:
    workers['closing'] = apache['Scoreboard'].count('C')  # SERVER_CLOSING
    workers['dns'] = apache['Scoreboard'].count('D')  # SERVER_BUSY_DNS
    workers['finishing'] = apache['Scoreboard'].count('G')  # SERVER_GRACEFUL
    workers['free'] = apache['Scoreboard'].count('.')  # SERVER_DEAD
    workers['idle'] = apache['Scoreboard'].count('I')  # SERVER_IDLE_KILL
    workers['keepalive'] = apache['Scoreboard'].count('K')  # SERVER_BUSY_KEEPALIVE
    workers['logging'] = apache['Scoreboard'].count('L')  # SERVER_BUSY_LOG
    workers['reading'] = apache['Scoreboard'].count('R')  # SERVER_BUSY_READ
    workers['replying'] = apache['Scoreboard'].count('W')  # SERVER_BUSY_WRITE
    workers['starting'] = apache['Scoreboard'].count('S')  # SERVER_STARTING
    workers['waiting'] = apache['Scoreboard'].count('_')  # SERVER_READY

    # currently the only value we warn about
    apache['WorkerUsagePercentage'] = round(
        int(apache['BusyWorkers']) / int(apache['TotalWorkers']) * 100.0, 1
    )
    state = lib.base.get_state(apache['WorkerUsagePercentage'], args.WARN, args.CRIT)

    perfdata += lib.base.get_perfdata(
        'WorkerUsagePercentage',
        apache['WorkerUsagePercentage'],
        uom='%',
        warn=args.WARN,
        crit=args.CRIT,
        _min=0,
        _max=100,
    )

    perfdata += lib.base.get_perfdata(
        'BusyWorkers',
        apache['BusyWorkers'],
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'IdleWorkers',
        apache['IdleWorkers'],
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'TotalWorkers',
        apache['TotalWorkers'],
        _min=0,
    )

    perfdata += lib.base.get_perfdata(
        'workers_closing',
        workers['closing'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_dns',
        workers['dns'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_finishing',
        workers['finishing'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_free',
        workers['free'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_idle',
        workers['idle'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_keepalive',
        workers['keepalive'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_logging',
        workers['logging'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_reading',
        workers['reading'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_replying',
        workers['replying'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_starting',
        workers['starting'],
        _min=0,
        _max=apache['TotalWorkers'],
    )
    perfdata += lib.base.get_perfdata(
        'workers_waiting',
        workers['waiting'],
        _min=0,
        _max=apache['TotalWorkers'],
    )

    # If ExtendedStatus is off:
    if 'Uptime' not in apache:
        # "ExtendedStatus off" just reports BusyWorkers, IdleWorkers and Scoreboard
        msg = (
            f'{apache["BusyWorkers"]}/{apache["TotalWorkers"]}'
            f' workers busy'
            f' ({apache["WorkerUsagePercentage"]}%'
            f'{lib.base.state2str(state, prefix=" ")}'
            f'; {workers["finishing"]} "G")'
            f', {apache["IdleWorkers"]} idle'
            f', {workers["free"]} free'
        )

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

    # === Second Stage==============================================================================
    # ExtendedStatus is on, but not all values are always present due to different versions of
    # mod_status

    # get previous values
    prev_uptime = lib.cache.get(
        f'apache-httpd-status-{args.URL}-uptime',
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    prev_count = lib.cache.get(
        f'apache-httpd-status-{args.URL}-total-accesses',
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    prev_bcount = lib.cache.get(
        f'apache-httpd-status-{args.URL}-total-bytes',
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )

    # save current values
    lib.cache.set(
        f'apache-httpd-status-{args.URL}-uptime',
        apache['Uptime'],
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    lib.cache.set(
        f'apache-httpd-status-{args.URL}-total-accesses',
        apache['Total Accesses'],
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    lib.cache.set(
        f'apache-httpd-status-{args.URL}-total-bytes',
        apache['Total Bytes'],
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    if prev_uptime is False or prev_count is False or prev_bcount is False:
        lib.base.oao('Waiting for more data (1).', STATE_OK)

    # calculate "raw" values for the current interval from total values (continous counters)
    uptime_diff = int(apache['Uptime']) - int(prev_uptime)
    apache['Total Accesses'] = apache['Total Accesses'] - int(prev_count)
    apache['Total Bytes'] = apache['Total Bytes'] - int(prev_bcount)
    if uptime_diff <= 0 or apache['Total Accesses'] <= 0 or apache['Total Bytes'] < 0:
        lib.base.oao('Waiting for more data (2).', STATE_OK)

    perfdata += lib.base.get_perfdata(
        'Accesses',
        apache['Total Accesses'],
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'Bytes',
        apache['Total Bytes'],
        uom='B',
        _min=0,
    )
    perfdata += (
        lib.base.get_perfdata(
            'CPULoad',
            apache['CPULoad'],
            _min=0,
        )
        if 'CPULoad' in apache
        else ''
    )
    perfdata += lib.base.get_perfdata(
        'Uptime',
        apache['Uptime'],
        uom='s',
        _min=0,
    )

    if 'Total Duration' not in apache:
        msg = (
            f'{apache["BusyWorkers"]}/{apache["TotalWorkers"]}'
            f' workers busy'
            f' ({apache["WorkerUsagePercentage"]}%'
            f'{lib.base.state2str(state, prefix=" ")}'
            f'; {workers["finishing"]} "G")'
            f', {apache["IdleWorkers"]} idle'
            f', {workers["free"]} free'
            f'; {lib.human.number2human(apache["Total Accesses"])} accesses'
            f', {lib.human.bytes2human(apache["Total Bytes"])} traffic'
            f'; Up {lib.human.seconds2human(apache["Uptime"])}'
        )

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

    # === Third Stage===============================================================================
    # Some more variables in newer versions of mod_status

    prev_duration_global = lib.cache.get(
        f'apache-httpd-status-{args.URL}-total-duration',
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    lib.cache.set(
        f'apache-httpd-status-{args.URL}-total-duration',
        apache['Total Duration'],
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    if prev_duration_global is False:
        lib.base.oao('Waiting for more data (3).', STATE_OK)

    # calculate "raw" values for the current interval from total values (continous counters)
    duration_diff = apache['Total Duration'] - float(prev_duration_global)
    if duration_diff <= 0 or apache['Total Accesses'] <= 0 or apache['Total Bytes'] < 0:
        lib.base.oao('Waiting for more data (4).', STATE_OK)

    perfdata += (
        lib.base.get_perfdata(
            'ConnsAsyncClosing',
            apache['ConnsAsyncClosing'],
            _min=0,
        )
        if 'ConnsAsyncClosing' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'ConnsAsyncKeepAlive',
            apache['ConnsAsyncKeepAlive'],
            _min=0,
        )
        if 'ConnsAsyncKeepAlive' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'ConnsAsyncWriting',
            apache['ConnsAsyncWriting'],
            _min=0,
        )
        if 'ConnsAsyncWriting' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'ConnsTotal',
            apache['ConnsTotal'],
            _min=0,
        )
        if 'ConnsTotal' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'Load1',
            apache['Load1'],
            _min=0,
        )
        if 'Load1' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'Load5',
            apache['Load5'],
            _min=0,
        )
        if 'Load5' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'Load15',
            apache['Load15'],
            _min=0,
        )
        if 'Load15' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'ParentServerConfigGeneration',
            apache['ParentServerConfigGeneration'],
            _min=0,
        )
        if 'ParentServerConfigGeneration' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'ParentServerMPMGeneration',
            apache['ParentServerMPMGeneration'],
            _min=0,
        )
        if 'ParentServerMPMGeneration' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'Processes',
            apache['Processes'],
            _min=0,
        )
        if 'Processes' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'Stopping',
            apache['Stopping'],
            _min=0,
        )
        if 'Stopping' in apache
        else ''
    )
    perfdata += (
        lib.base.get_perfdata(
            'Total Duration',
            duration_diff,
            uom='s',
            _min=0,
        )
        if 'Total Duration' in apache
        else ''
    )

    msg = (
        f'{apache["ServerName"]}:'
        f' {apache["BusyWorkers"]}/{apache["TotalWorkers"]}'
        f' workers busy'
        f' ({apache["WorkerUsagePercentage"]}%'
        f'{lib.base.state2str(state, prefix=" ")}'
        f'; {workers["finishing"]} "G")'
        f', {apache["IdleWorkers"]} idle'
        f', {workers["free"]} free'
        f'; {lib.human.number2human(apache["Total Accesses"])} accesses'
        f', {lib.human.bytes2human(apache["Total Bytes"])} traffic'
        f'; Up {lib.human.seconds2human(apache["Uptime"])}'
    )

    table_values = []
    table_values.append(
        {
            'key': 'Current Time',
            'value': apache['CurrentTime'],
        }
    )
    table_values.append(
        {
            'key': 'Restart Time',
            'value': apache['RestartTime'],
        }
    )
    table_values.append(
        {
            'key': 'Check Interval',
            'value': lib.human.seconds2human(uptime_diff),
        }
    )
    table_values.append(
        {
            'key': 'Uptime',
            'value': lib.human.seconds2human(apache['Uptime']),
        }
    )
    if 'ConnsTotal' in apache:
        table_values.append(
            {
                'key': 'Connections',
                'value': apache['ConnsTotal'],
            }
        )
    if 'ConnsAsyncWriting' in apache:
        table_values.append(
            {
                'key': '  Async Writing',
                'value': apache['ConnsAsyncWriting'],
            }
        )
    if 'ConnsAsyncKeepAlive' in apache:
        table_values.append(
            {
                'key': '  Async KeepAlive',
                'value': apache['ConnsAsyncKeepAlive'],
            }
        )
    if 'ConnsAsyncClosing' in apache:
        table_values.append(
            {
                'key': '  Async Closing',
                'value': apache['ConnsAsyncClosing'],
            }
        )
    table_values.append(
        {
            'key': 'Requests',
            'value': lib.human.number2human(apache['Total Accesses']),
        }
    )
    table_values.append(
        {
            'key': 'Bytes',
            'value': lib.human.bytes2human(apache['Total Bytes']),
        }
    )
    table_values.append(
        {
            'key': 'Request Duration',
            'value': lib.human.seconds2human(duration_diff),
        }
    )
    if 'Load1' in apache:
        table_values.append(
            {
                'key': 'Load1',
                'value': apache['Load1'],
            }
        )
    if 'Load15' in apache:
        table_values.append(
            {
                'key': 'Load5',
                'value': apache['Load5'],
            }
        )
    if 'Load15' in apache:
        table_values.append(
            {
                'key': 'Load15',
                'value': apache['Load15'],
            }
        )
    if 'Processes' in apache:
        table_values.append(
            {
                'key': 'Processes',
                'value': apache['Processes'],
            }
        )
    if 'Stopping' in apache:
        table_values.append(
            {
                'key': '  Stopping',
                'value': apache['Stopping'],
            }
        )
    table_values.append(
        {
            'key': 'Workers Total',
            'value': apache['TotalWorkers'],
        }
    )
    table_values.append(
        {
            'key': '  Busy',
            'value': apache['BusyWorkers'],
        }
    )
    table_values.append(
        {
            'key': '  Idle',
            'value': apache['IdleWorkers'],
        }
    )
    table_values.append(
        {
            'key': '  Usage (%)',
            'value': str(apache['WorkerUsagePercentage'])
            + lib.base.state2str(state, prefix=' '),
        }
    )
    if 'ParentServerConfigGeneration' in apache:
        table_values.append(
            {
                'key': 'Parent Server ConfigGeneration',
                'value': apache['ParentServerConfigGeneration'],
            }
        )
    if 'ParentServerMPMGeneration' in apache:
        table_values.append(
            {
                'key': 'Parent Server MPMGeneration',
                'value': apache['ParentServerMPMGeneration'],
            }
        )
    table_values.append(
        {
            'key': 'Server Name',
            'value': apache['ServerName'],
        }
    )
    table_values.append(
        {
            'key': 'Server MPM',
            'value': apache['ServerMPM'],
        }
    )
    table_values.append(
        {
            'key': 'Server Version',
            'value': apache['ServerVersion'],
        }
    )
    table_values.append(
        {
            'key': 'Server Built',
            'value': apache['Server Built'],
        }
    )

    msg = msg + '\n\n'

    # build the message
    msg += lib.base.get_table(
        table_values,
        ['key', 'value'],
        header=['Key', 'Value'],
        strip=False,
    )

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


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