Source code for apache_manager.cli

# Monitor and control Apache web server workers from Python.
#
# Author: Peter Odding <peter@peterodding.com>
# Last Change: February 15, 2017
# URL: https://apache-manager.readthedocs.io

"""
Usage: apache-manager [OPTIONS]

Command line interface to monitor the Apache web server and kill worker
processes that exceed resource thresholds. When no options are given the
server metrics and memory usage of workers are printed to the terminal.

Supported options:

  -w, --watch

    This option causes the Apache manager to redraw the collected metrics once
    every 10 seconds in a `top' like interface until interrupted using `q' (for
    quite) or Control-C.

  -a, --max-memory-active=SIZE

    Kill active Apache workers that are using more memory than specified by the
    SIZE argument. SIZE is expected to be a human readable memory size like 50K
    (50 kilobytes), 42M (42 megabytes), 2G (2 gigabytes), etc.

  -i, --max-memory-idle=SIZE

    Kill Apache workers that are using more memory than specified by the SIZE
    argument (see --max-memory-active for acceptable values of SIZE).

  -t, --max-ss, --max-time=TIMESPAN

    Kill Apache workers whose "time since the beginning of the most recent
    request" is greater than specified by the TIMESPAN argument. TIMESPAN is
    expected to be a human readable timespan like 2s (2 seconds), 3m (3
    minutes), 5h (5 hours), 2d (2 days), etc.

  -f, --data-file=PATH

    Change the pathname of the file where the Apache manager stores monitoring
    metrics after every run. Defaults to `/tmp/apache-manager.txt'.

  -z, --zabbix-discovery

    Generate a JSON fragment that's compatible with the low-level discovery
    support in the Zabbix monitoring system. With the right template in place
    this enables the Zabbix server to discover the names of the WSGI process
    groups that are active on any given server. This makes it possible to
    collect and analyze the memory usage of specific WSGI process groups.

  -n, --dry-run, --simulate

    Don't actually kill any Apache workers.

  -v, --verbose

    Increase verbosity (can be repeated).

  -q, --quiet

    Decrease verbosity (can be repeated).

  -h, --help

    Show this message and exit.
"""

# Standard library modules.
import getopt
import json
import logging
import sys

# External dependencies.
import coloredlogs
from humanfriendly import (
    format_size,
    format_timespan,
    parse_size,
    parse_timespan,
    pluralize,
)
from humanfriendly.terminal import (
    ansi_wrap,
    connected_to_terminal,
    HIGHLIGHT_COLOR,
    usage,
)

# Modules included in our package.
from apache_manager import ApacheManager, NATIVE_WORKERS_LABEL
from apache_manager.interactive import watch_metrics

# Initialize a logger for this program.
logger = logging.getLogger(__name__)


[docs]def main(): """Command line interface for the ``apache-manager`` program.""" # Configure logging output. coloredlogs.install(syslog=True) # Command line option defaults. data_file = '/tmp/apache-manager.txt' dry_run = False max_memory_active = None max_memory_idle = None max_ss = None watch = False zabbix_discovery = False verbosity = 0 # Parse the command line options. try: options, arguments = getopt.getopt(sys.argv[1:], 'wa:i:t:f:znvqh', [ 'watch', 'max-memory-active=', 'max-memory-idle=', 'max-ss=', 'max-time=', 'data-file=', 'zabbix-discovery', 'dry-run', 'simulate', 'verbose', 'quiet', 'help', ]) for option, value in options: if option in ('-w', '--watch'): watch = True elif option in ('-a', '--max-memory-active'): max_memory_active = parse_size(value) elif option in ('-i', '--max-memory-idle'): max_memory_idle = parse_size(value) elif option in ('-t', '--max-ss', '--max-time'): max_ss = parse_timespan(value) elif option in ('-f', '--data-file'): data_file = value elif option in ('-z', '--zabbix-discovery'): zabbix_discovery = True elif option in ('-n', '--dry-run', '--simulate'): logger.info("Performing a dry run ..") dry_run = True elif option in ('-v', '--verbose'): coloredlogs.increase_verbosity() verbosity += 1 elif option in ('-q', '--quiet'): coloredlogs.decrease_verbosity() verbosity -= 1 elif option in ('-h', '--help'): usage(__doc__) return except Exception as e: sys.stderr.write("Error: %s!\n" % e) sys.exit(1) # Execute the requested action(s). manager = ApacheManager() try: if max_memory_active or max_memory_idle or max_ss: manager.kill_workers( max_memory_active=max_memory_active, max_memory_idle=max_memory_idle, timeout=max_ss, dry_run=dry_run, ) elif watch and connected_to_terminal(sys.stdout): watch_metrics(manager) elif zabbix_discovery: report_zabbix_discovery(manager) elif data_file != '-' and verbosity >= 0: for line in report_metrics(manager): if line_is_heading(line): line = ansi_wrap(line, color=HIGHLIGHT_COLOR) print(line) finally: if (not watch) and (data_file == '-' or not dry_run): manager.save_metrics(data_file)
[docs]def report_metrics(manager): """Create a textual summary of Apache web server metrics.""" lines = ["Server metrics:"] for name, value in sorted(manager.server_metrics.items()): if name in ('total_traffic', 'bytes_per_second', 'bytes_per_request'): value = format_size(value) elif name == 'cpu_load': value = '%.1f%%' % value elif name == 'uptime': value = format_timespan(value) name = ' '.join(name.split('_')) name = name[0].upper() + name[1:] lines.append(" - %s: %s" % (name, value)) main_label = "main Apache workers" if manager.wsgi_process_groups else "Apache workers" report_memory_usage(lines, main_label, manager.memory_usage) for name, memory_usage in sorted(manager.wsgi_process_groups.items()): report_memory_usage(lines, "WSGI process group '%s'" % name, memory_usage) return lines
[docs]def report_memory_usage(lines, label, memory_usage): """Create a textual summary of Apache worker memory usage.""" lines.append("") workers = pluralize(len(memory_usage), "worker") lines.append("Memory usage of %s (%s):" % (label, workers)) lines.append(" - Minimum: %s" % format_size(memory_usage.min)) lines.append(" - Average: %s" % format_size(memory_usage.average)) lines.append(" - Maximum: %s" % format_size(memory_usage.max))
[docs]def report_zabbix_discovery(manager): """Enable Zabbix low-level discovery of WSGI application groups.""" worker_groups = [NATIVE_WORKERS_LABEL] + sorted(manager.wsgi_process_groups.keys()) print(json.dumps({'data': [{'{#NAME}': name} for name in worker_groups]}))
[docs]def line_is_heading(line): """Check whether a line of output generated by :func:`report_metrics()` should be highlighted as a heading.""" return line.endswith(':')