Nagless

Monitoring software often suffers from the "Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf! Wolf!" (W) problem, wherein issues are raised repeatedly to the point that some folks ignore the alerts. Other folks complicate the monitoring by putting a layer between the "things that generate alerts" (monitoring software) and "targets of those alerts" (the poor sods who are on-call); this new layer only allows some alerts through based on various rules, which can range from a simple "do not send another alert if the prior alert was within the last 30 minutes" to highly complicated schemes where there is a hierarchy, whereby if, say, DNS is down all the things that depend on DNS will have their alerts suppressed. Larger sites are probably the only ones that need the complexity of a hierarchical alarm suppression system, or escalation should the on-call(s) not respond in a suitable amount of time, etc. Still other folks are okay with W, but I really do not get their thinking, especially given that all of the rest of the team ended up ignoring all of the alerts. Too much noise.

Alerts tend to go out via email, or did back in the day, or still do for folks who have email infrastructure, so a good place to put a declutter layer is on a mail server. Some account will accept all the monitoring alerts and determine whether to forward the alerts, or not, based on whatever logic is deemed necessary to balance the need for timely notification of important events versus not drowning the on-call in noise. Very important alerts may also be sent to everyone via a pager system in addition to email. The declutter layer could also redirect alerts to, say, an IRC channel, or might have logic that varies where the alerts go depending on whether someone is near a computer, or not. Too much logic of course is bloat and may well come with errors that require time to debug and fix, or may cause too many alerts to be missed. "…and that amount of Wu Wei is just right," said Goldilocks.

OpenSMTP

The mail server is a good place to route messages to a program, though this could also be done via a .forward file, or a program could process messages via IMAP depending on the level of access to the system. Mail from spammers and other such malicious sources should ideally not be able to reach the nagless address, as besides the time wasted in cleaning up their little messes there is also the risk of a security vulnerability, or, less likely, the ability of a remote attacker to somehow mask system alerts. An inside attacker would be more likely to break an alert system, as they likely have access to relevant code, documentation, or systems involved. Some duplication of monitoring may be necessary in larger sites, as those duplicate checks may notice something that has been suppressed by an insider in another system?

    $ grep nagless /etc/mail/smtpd.conf 
    action "nagless" mda "/usr/bin/true"
    match from local for rcpt-to regex "^nagless@" action "nagless"
    match from any for rcpt-to regex "^nagless@" reject
    $ echo foo | mail -s test nagless
    $ grep nagless /var/log/maillog
    ...

Sometimes you just want to get your foot in the door, so a delivery to true(1) should not fail, though will throw the messages away. Monitoring zero. Next up is something that actually parses the messages and does suitable things with them. Or you could stop right here?

Dedup

The MDA protocol is fairly simple; the email shows up on standard input, line by line. We do not need much of a parser here, maybe only to look for the "Subject" header. Even this can have complications; I had a MDA once that would extract the message subject and post it to syslog wherein they would show up in a tailed logfile, and foolish me assumed that the subject would appear somewhere in a 4096K buffer of the message. Efficient block read, search the block, Done. Right? However, certain messages from Microsoft Exchange somehow managed to stuff somewhere north of 8192K into the headers before the subject line. Why does a message with a few sentences in it need orders of magnitude more metadata along for the ride, and how many sanity points would it cost to learn what the heck Microsoft was doing there?

You may need a more complicated parser if there is information you need to dig out from somewhere within a MIME part. The following only looks for a line starting with "Subject: " in the header and may do something with it, otherwise not much happens. Some may want better logs of what is going on.

    // nagless - mail delivery agent (MDA) that forwards but suppresses
    // messages for some amount of time, to cut down on monitoring spam

    #include 
    #include 
    #include 

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 

    // PORTABILITY some OS put this at /usr/lib/sendmail or you may instead
    // need to use something like msmtp to send the mail off elsewhere
    #ifndef SENDMAIL_PATH
    #define SENDMAIL_PATH "/usr/sbin/sendmail"
    #endif

    // NOTE syslog any other things have limits if you want to increase the
    // maximum Subject length sent
    #define MAX_BLATHER 70

    #define NAGFAIL EX_TEMPFAIL

    enum { PIPE_R, PIPE_W };

    char *Checkpoint_File;
    char *Contact_Address;
    int Nagless_Sec;

    int Flag_Syslog; // -s

    static void emit_help(void);
    static void handle_subject(char *subj, ssize_t subjlen);
    static void maybe_notify(char *subj, ssize_t subjlen);

    int
    main(int argc, char *argv[])
    {
        int ch;
        const char *errstr;

        char *line      = NULL;
        size_t linesize = 0;
        ssize_t linelen;

        if (!setlocale(LC_ALL, "C")) err(1, "setlocale");

        while ((ch = getopt(argc, argv, "s")) != -1) {
            switch (ch) {
            case 's':
                Flag_Syslog = 1;
                break;
            default:
                emit_help();
            }
        }
        argc -= optind;
        argv += optind;

        if (argc != 3 || !argv[0] || !argv[0][0] || !argv[1] || !argv[1][0] ||
            !argv[2] || !argv[2][0])
            emit_help();
        Checkpoint_File = argv[0];
        // PORTABILITY use libbsd or strtoul(3) or hopefully not atoi(3)
        Nagless_Sec = (int) strtonum(argv[1], 0, INT_MAX, &errstr);
        if (errstr) err(NAGFAIL, "strtonum '%s': %s", argv[1], errstr);
        Contact_Address = argv[2];

        if (Flag_Syslog) openlog("nagless", LOG_NDELAY, LOG_USER);

    #ifdef __OpenBSD__
        if (pledge("exec fattr proc rpath stdio unveil", NULL) == -1)
            err(1, "pledge");
        if (unveil(SENDMAIL_PATH, "x") == -1) err(1, "unveil");
        if (unveil(Checkpoint_File, "rw") == -1) err(1, "unveil");
        if (unveil(NULL, NULL) == -1) err(1, "unveil");
    #endif

        while ((linelen = getline(&line, &linesize, stdin)) != -1) {
            // TODO but what if we get a \r\n line??
            if (linelen == 1) goto DONE; // blank line, end of headers
            if (!strncmp(line, "Subject: ", 9)) {
                handle_subject(line + 9, linelen - 10);
                goto DONE;
            }
        }
        //free(line);
        //if (ferror(stdin)) err(NAGFAIL, "getline");
    DONE:
        exit(EXIT_SUCCESS);
    }

    static void
    emit_help(void)
    {
        fputs(
          "Usage: nagless [-s] checkpoint-file window-seconds notify-address\n",
          stderr);
        exit(NAGFAIL);
    }

    inline static void
    handle_subject(char *subj, ssize_t subjlen)
    {
        char *cp;
        if (subjlen <= 0) return;
        if (subjlen > MAX_BLATHER) subjlen = MAX_BLATHER;
        cp = subj;
        for (size_t i = 0; i < (size_t) subjlen; ++i, ++cp)
            if (!isprint(*cp)) *cp = '.';
        if (Flag_Syslog) syslog(LOG_NOTICE, "%.*s", (int) subjlen, subj);
        maybe_notify(subj, subjlen);
    }

    inline static void
    maybe_notify(char *subj, ssize_t subjlen)
    {
        char *failure = "unknown";
        int ret, status, tochild[2];
        pid_t child, pid;
        struct stat sb;
        time_t now;
        if (time(&now) == (time_t) -1) err(NAGFAIL, "time");
        if (stat(Checkpoint_File, &sb) == -1)
            err(NAGFAIL, "stat '%s'", Checkpoint_File);
        if (llabs(now - sb.st_mtim.tv_sec) < Nagless_Sec) return;
        utimes(Checkpoint_File, NULL);

        if (pipe(tochild) == -1) {
            failure = "pipe";
            goto NOTIFY_FAIL;
        }

        pid = fork();
        if (pid < 0) {
            failure = "fork";
            goto NOTIFY_FAIL;
        } else if (pid == 0) { // child
            close(tochild[PIPE_W]);
            if (dup2(tochild[PIPE_R], STDIN_FILENO) != STDIN_FILENO)
                err(1, "dup2");
            close(tochild[PIPE_R]);
            execl(SENDMAIL_PATH, "sendmail", "-t", (char *) 0);
            err(1, "execl");
        }

        close(tochild[PIPE_R]);
        ret = dprintf(tochild[PIPE_W], "To: %s\nSubject: %.*s\n\nfyi",
                      Contact_Address, (int) subjlen, subj);
        if (ret < 0)
            warn("dprintf");
        else if (ret == 0)
            warn("dprintf zero write??");
        if (close(tochild[PIPE_W]) == -1) warn("close");
        // TODO may need timeout here, in the event the mail program gets stuck
        child = wait(&status);
        if (child == -1) warn("wait");
        if (status) warnx("non-zero exit (%d)", status);
        return;

    NOTIFY_FAIL:
        if (Flag_Syslog) syslog(LOG_ERR, "notification failure: %s", failure);
        warn("notification failure: %s", failure);
    }

Back in smtpd.conf, the action line should look something like

    action "nagless" mda "/usr/local/libexec/nagless -s /var/run/nagless/cpf 1800 user@example.org" user nagless

and you'll need to touch and chown and chmod the /var/run/nagless/cpf file so that the nagless user can peek and poke at it. Then, point monit or whatever at the local nagless account instead of directly at your user account:

    # grep set\ alert /etc/monitrc 
    set alert nagless@example.org
    set alert nagless@example.org not on { instance, action }

In theory, if all goes well, only some alerts will make their way through. This is pretty barebones so probably does need more complications on what gets through, how events are logged for review, etc.

Improvements?

Placid

If your alerts fire too infrequently then the on-call and everyone else may be out of practice on what to do, or who knows if the monitoring code has not bitrotted. A not very exciting environment may need practice alerts, much as folks test a building's fire alarm system even though there has not been a fire for who knows how long. Computer systems, being both new and typically riddled with errors, tend not to (but can) have this problem.

Message Reduction

Turning off httpd generates a bunch of syslog messages, too many perhaps, but the number of emails generated for this event was just one:

    Nov 20 01:56:37 thrig httpd[86929]: parent terminating, pid 86929
    Nov 20 01:57:09 thrig monit[83927]: 'httpd' process is not running
    Nov 20 01:57:09 thrig nagless: monit:restart|httpd|Does not exist
    Nov 20 01:57:09 thrig monit[83927]: 'httpd' trying to restart
    Nov 20 01:57:09 thrig monit[83927]: 'httpd' start: '/usr/sbin/rcctl start httpd'
    Nov 20 01:57:09 thrig nagless: monit:restart|httpd|Does not exist
    Nov 20 01:57:09 thrig httpd[96273]: startup
    Nov 20 01:58:10 thrig monit[83927]: 'httpd' process is running with pid 96273
    Nov 20 01:58:10 thrig nagless: monit:alert|httpd|Exists
    Nov 20 01:58:10 thrig nagless: monit:alert|httpd|Exists