logparse / interface.pyon commit rename parsers, better journald integration (e1f7605)
   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3
   4"""
   5This module is the entrypoint of the `logparse` shell command and also contains
   6single-use functions which don't fit elsewhere. All user interaction with
   7logparse should be through this module.
   8
   9This module provides the following methods:
  10    - `main()`:         Set up arguments, config, logging, and execute parsers
  11    - `rotate()`:       Rotate logs using systemd logrotate
  12    - `rotate_sim()`:   Simulate log rotation
  13"""
  14
  15import argparse
  16from copy import copy
  17import logging
  18import logging.handlers
  19import os
  20from sys import exit, stdin, version
  21from subprocess import check_output
  22from datetime import datetime
  23
  24import logparse
  25from logparse import formatting, mail, config, load_parsers, util
  26
  27
  28def main():
  29    """
  30    Initialisation and general management of logparse functionaliy.
  31    """
  32
  33    # Get arguments
  34
  35    global argparser
  36    argparser = get_argparser()
  37
  38    # Load config
  39
  40    config.prefs = config.loadconf(argparser.parse_args().config)
  41    if argparser.parse_args().time_period:
  42        config.prefs.set("logparse", "period",
  43                argparser.parse_args().time_period)
  44    
  45    # Set up logging
  46    logger = logging.getLogger(__name__)
  47    if (argparser.parse_args().quiet
  48            or config.prefs.getboolean("logparse", "quiet")):
  49        logparse.logger.setLevel(logging.CRITICAL)
  50    elif (argparser.parse_args().verbose 
  51            or config.prefs.getboolean("logparse", "verbose")):
  52        logparse.logger.setLevel(logging.DEBUG)
  53        logger.debug("Verbose mode turned on")
  54    else:
  55        logparse.logger.setLevel(logging.INFO)
  56
  57
  58
  59    # Time analysis
  60
  61    global start
  62    start = datetime.now()
  63    logger.info("Beginning log analysis at {0} {1}".format(
  64        start.strftime(formatting.DATEFMT), start.strftime(formatting.TIMEFMT)))
  65    logger.debug("This is {0} version {1}, running on Python {2}".format(
  66        logparse.__name__, logparse.__version__, version.replace('\n', '')))
  67     
  68    # Write header
  69
  70    formatting.init_var()
  71
  72    if argparser.parse_args().plain:
  73        output = formatting.PlaintextOutput(
  74                linewidth=config.prefs.getint("plain", "linewidth"))
  75        output.append_header()
  76    else:
  77        output = formatting.HtmlOutput()
  78        output.append_header(config.prefs.get("html", "header"))
  79
  80    # Find parsers
  81    
  82    parser_names = []
  83    ignore_logs = []
  84    if argparser.parse_args().logs:
  85        parser_names = set(argparser.parse_args().logs.split())
  86    elif config.prefs.get("logparse", "parsers"):
  87        parser_names = set(config.prefs.get("logparse", "parsers").split())
  88
  89    if argparser.parse_args().ignore_logs:
  90        ignore_logs = argparser.parse_args().ignore_logs.split()
  91    elif config.prefs.get("logparse", "ignore-parsers"):
  92        ignore_logs = config.prefs.get("logparse", "ignore-parsers").split()
  93
  94    # Set up parsers
  95
  96    loader = load_parsers.ParserLoader()
  97
  98    try:
  99        loader.check_systemd()
 100    except Exception as e:
 101        logger.error("Failed to check systemd dependencies: ".format(e))
 102
 103    if parser_names:
 104        for parser_name in parser_names:
 105            if parser_name not in ignore_logs:
 106                loader.search(parser_name)
 107    else:
 108        loader.load_pkg()
 109        if ignore_logs:
 110            loader.ignore(ignore_logs)
 111
 112    # Execute parsers
 113
 114    executed_parsers = []
 115
 116    for parser in loader.parsers:
 117        if (argparser.parse_args().verbose 
 118                or config.prefs.getboolean("logparse", "verbose")):
 119            output.append_section(parser.parse_log())
 120
 121        else:
 122            try:
 123                output.append_section(parser.parse_log())
 124            except Exception as e:
 125                logger.error("Uncaught error executing logger {0}: {1}".format(
 126                    parser.name, e))
 127        executed_parsers.append(parser.name)
 128
 129    if len(executed_parsers) == 0:
 130        exit()
 131
 132    # Write footer
 133    output.append_footer()
 134
 135    # Write output
 136    if ((argparser.parse_args().destination
 137        or config.prefs.get("logparse", "output"))
 138        and not argparser.parse_args().no_write):
 139
 140        # Determine destination path
 141        if argparser.parse_args().destination:
 142            dest_path = argparser.parse_args().destination
 143        else:
 144            dest_path = config.prefs.get("logparse", "output")
 145
 146        logger.debug("Outputting to {0}".format(dest_path))
 147
 148        # Determine whether to clobber old file
 149        if (not os.path.isfile(dest_path)) \
 150        and not (argparser.parse_args().overwrite
 151                or config.prefs.getboolean("logparse", "overwrite")):
 152
 153            if (argparser.parse_args().embed_styles
 154                    or config.prefs.getboolean("html", "embed-styles")) \
 155                and not (argparser.parse_args().plain
 156                        or config.prefs.getboolean("plain", "plain")):
 157                # Embed CSS stylesheet
 158                output.embed_css(config.prefs.get("html", "css"))
 159                output.write_embedded(dest_path)
 160            else:
 161                output.write(dest_path)
 162
 163        elif logging.root.level == logging.CRITICAL:
 164
 165            # Don't write output if running in quiet mode (only stdout)
 166            pass
 167
 168        else:
 169
 170            logger.warning("Destination file already exists")
 171            if input("Would you like to overwrite {0}? (y/n) [n] "
 172                    .format(dest_path)) == 'y':
 173                if (argparser.parse_args().embed_styles
 174                        or config.prefs.getboolean("html", "embed-styles")) \
 175                    and not (argparser.parse_args().plain
 176                        or config.prefs.getboolean("plain", "plain")):
 177
 178                    output.embed_css(config.prefs.get("html", "css"))
 179                    output.write_embedded(dest_path)
 180
 181                else:
 182                    output.write(dest_path)
 183            else:
 184                logger.warning("No output written")
 185
 186    # Send email if requested
 187
 188    if (str(argparser.parse_args().to) or str(config.prefs.get("mail", "to"))) \
 189            and not argparser.parse_args().no_mail:
 190
 191        if str(argparser.parse_args().to):
 192            to = argparser.parse_args().to
 193        else:
 194            to = config.prefs.get("mail", "to")
 195
 196        mail.sendmail(
 197            mailbin=config.prefs.get("mail", "mailbin"),
 198            body=(output.embed_css(config.prefs.get("html", "css"))
 199                if isinstance(output, formatting.HtmlOutput) else output.content),
 200            recipient=to,
 201            subject=formatting.fsubject(config.prefs.get("mail", "subject")),
 202            html=isinstance(output, formatting.HtmlOutput),
 203            sender=config.prefs.get("mail", "from"))
 204
 205    # Rotate logs if requested
 206
 207    if not argparser.parse_args().no_rotate:
 208        if (argparser.parse_args().simulate
 209                or config.prefs.getboolean("logparse", "rotate")):
 210            rotate_sim()
 211        elif (config.prefs.getboolean("logparse", "rotate")
 212                or argparser.parse_args().rotate):
 213            rotate()
 214        else:
 215            logger.debug("User doesn't want to rotate logs")
 216    else:
 217        logger.debug("User doesn't want to rotate logs")
 218
 219    # Finish up
 220
 221    finish = datetime.now()
 222    logger.info("Finished parsing logs at {0} {1} (total time: {2})".format(
 223        finish.strftime(formatting.DATEFMT),
 224        finish.strftime(formatting.TIMEFMT),
 225        finish - start))
 226
 227    if argparser.parse_args().printout:
 228        if isinstance(output, formatting.HtmlOutput) \
 229                and argparser.parse_args().embed_styles \
 230                or config.prefs.getboolean("html", "embed-styles"):
 231            output.print_stdout_embedded()
 232        else:
 233            output.print_stdout()
 234
 235    return
 236
 237
 238def get_argparser():
 239    """
 240    Initialise arguments (in a separate function for documentation purposes)
 241    """
 242
 243    argparser = argparse.ArgumentParser(description=
 244            'Grab logs of some common services and send them by email')
 245    argparser.add_argument('-t','--to', required=False,
 246            help='mail recipient (\"to\" address)')
 247    argparser.add_argument('-c', '--config', required=False,
 248            default="/etc/logparse/logparse.conf",
 249            help='path to config file')
 250    argparser.add_argument('-p', '--print', required=False, dest='printout',
 251            action='store_true', default=False,
 252            help='print HTML to stdout')
 253    argparser.add_argument('-d', '--destination', required=False, 
 254            help='file to output HTML')
 255    argparser.add_argument('-f', '--overwrite', required=False,
 256            action='store_true', default=False, 
 257            help='force overwrite an existing output file')
 258    argparser.add_argument('-v', '--verbose', required=False, default=False,
 259            action='store_true',
 260            help='verbose console/syslog output (for debugging)')
 261    argparser.add_argument('-r', '--rotate', required=False, default=False,
 262            action='store_true',
 263            help='force rotate log files using systemd logrotate (overrides \
 264            --rotate and "rotate" in logparse.conf)')
 265    argparser.add_argument('-nr', '--no-rotate', required=False, default=False,
 266            action='store_true', 
 267            help='do not rotate log files (overrides config)')
 268    argparser.add_argument('-s', '--simulate', required=False, default=False,
 269            action="store_true",
 270            help="test run logrotate (do not actually change files)")
 271    argparser.add_argument('-l', '--logs', required=False, 
 272            help='services to analyse')
 273    argparser.add_argument('-nl', '--ignore-logs', required=False,
 274            help='skip these services (takes precedence over -l)')
 275    argparser.add_argument('-es', '--embed-styles', required=False,
 276            default=False, action='store_true',
 277            help='make CSS rules inline rather than linking the file')
 278    argparser.add_argument('-nh', '--plain', required=False, default=False,
 279            action='store_true', help='write/send plain text rather than HTML')
 280    argparser.add_argument('-q', '--quiet', required=False, default=False,
 281            action='store_true', help='no output to stdout')
 282    argparser.add_argument('-nm', '--no-mail', required=False, default=False,
 283            action="store_true",
 284            help="do not send email (overrides config file)")
 285    argparser.add_argument('-nw', '--no-write', required=False, default=False,
 286            action="store_true",
 287            help="do not write output file (overrides config file)")
 288    argparser.add_argument('-tp', '--time-period', required=False,
 289            help="time period to analyse logs for (applies to all parsers)")
 290
 291    return argparser
 292
 293
 294
 295
 296def rotate():
 297    """
 298    Rotate logs using systemd logrotate. This requires root privileges, and a
 299    basic check for this is attempted below. Root password will be prompted
 300    for if permissions are not automatically granted.
 301    """
 302
 303    logger = logging.getLogger(__name__)
 304    try:
 305        if not os.geteuid() == 0:
 306            if stdin.isatty():
 307                logger.warning("Not running as root, using sudo \
 308                        (may require password to be entered)")
 309                rotate_shell = check_output(
 310                        "sudo logrotate /etc/logrotate.conf", shell=True)
 311            else:
 312                raise PermissionError("Root priviliges are required to run \
 313                        logrotate but were not provided")
 314        else:
 315            rotate_shell = check_output(
 316                    "/usr/sbin/logrotate /etc/logrotate.conf", shell=True)
 317        logger.info("Rotated logfiles")
 318        logger.debug("logrotate output: " + rotate_shell)
 319    except Exception as e:
 320        logger.warning("Failed to rotate log files: " + str(e))
 321
 322
 323def rotate_sim():   # Simulate log rotation
 324    """
 325    Simulate log rotation using logrotate's -d flag. This does not require root
 326    privileges, but permission errors will be shown in the output without it.
 327    """
 328
 329    logger = logging.getLogger(__name__)
 330    try:
 331        if not os.geteuid() == 0:
 332            logger.warning("Cannot run logrotate as root - \
 333                    you will see permission errors in the output below")
 334        sim_cmd = "logrotate -d /etc/logrotate.conf"
 335        logger.debug("Here is the output of `{0}` (simulated):".format(sim_cmd))
 336        sim = check_output(sim_cmd, shell=True)
 337        logger.debug(sim)
 338    except Exception as e:
 339        logger.warning("Failed to get logrotate simulation: " + str(e))