/* prometheus.c -- Aggregate statistics for prometheus
 *
 * Copyright (c) 1994-2017 Carnegie Mellon University.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any legal
 *    details, please contact
 *      Carnegie Mellon University
 *      Center for Technology Transfer and Enterprise Creation
 *      4615 Forbes Avenue
 *      Suite 302
 *      Pittsburgh, PA  15213
 *      (412) 268-7393, fax: (412) 268-7395
 *      innovation@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <sys/types.h>

#include <dirent.h>
#include <errno.h>
#include <string.h>
#include <sysexits.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>

#include "lib/assert.h"
#include "lib/cyr_lock.h"
#include "lib/libconfig.h"
#include "lib/map.h"
#include "lib/ptrarray.h"
#include "lib/util.h"

#include "imap/global.h"
#include "imap/imap_err.h"
#include "imap/prometheus.h"

struct prometheus_handle {
    struct mappedfile *mf;
};

static struct prometheus_handle *promhandle = NULL;
static int prometheus_enabled = -1;

static void prometheus_init(void);
static void prometheus_done(void *rock __attribute__((unused)));

EXPORTED const char *prometheus_stats_dir(void)
{
    static struct buf statsdir = BUF_INITIALIZER;
    const char *tmp;

    if (buf_len(&statsdir) > 0) return buf_cstring(&statsdir);

    if ((tmp = config_getstring(IMAPOPT_PROMETHEUS_STATS_DIR))) {
        if (tmp[0] != '/')
            fatal("prometheus_stats_dir must be fully qualified", EX_CONFIG);

        if (strlen(tmp) < 2)
            fatal("prometheus_stats_dir must not be '/'", EX_CONFIG);

        buf_setcstr(&statsdir, tmp);

        if (statsdir.s[statsdir.len-1] != '/')
            buf_putc(&statsdir, '/');
    }
    else {
        buf_setcstr(&statsdir, config_dir);
        buf_appendcstr(&statsdir, FNAME_PROM_STATS_DIR);
        buf_putc(&statsdir, '/');
    }

    return buf_cstring(&statsdir);
}

static void prometheus_init(void)
{
    char fname[PATH_MAX];
    struct prometheus_handle *handle = NULL;
    struct prom_stats stats = PROM_STATS_INITIALIZER;
    int r;

    if (promhandle != NULL) return;

    prometheus_enabled = config_getswitch(IMAPOPT_PROMETHEUS_ENABLED);
    if (!prometheus_enabled) return;

    r = snprintf(stats.ident, sizeof(stats.ident), "%s", config_ident);
    if (r < 0 || (size_t) r >= sizeof(stats.ident))
        syslog(LOG_WARNING, "service name '%s' is longer than " SIZE_T_FMT
                            " characters - prometheus label will be truncated",
                            config_ident,
                            sizeof(stats.ident) - 1);

    r = snprintf(fname, sizeof(fname), "%s%jd",
                 prometheus_stats_dir(), (intmax_t) getpid());
    if (r < 0 || (size_t) r >= sizeof(fname))
        fatal("unable to register stats for prometheus", EX_CONFIG);

    r = cyrus_mkdir(fname, 0755);
    if (r) return;

    handle = xzmalloc(sizeof(*handle));
    r = mappedfile_open(&handle->mf, fname, MAPPEDFILE_CREATE | MAPPEDFILE_RW);
    if (r) goto error;

    r = mappedfile_writelock(handle->mf);
    if (r) goto error;

    r = mappedfile_pwrite(handle->mf, &stats, sizeof(stats), 0);
    if (r != sizeof(stats)) {
        syslog(LOG_ERR, "IOERROR: mappedfile_pwrite: expected to write " SIZE_T_FMT "bytes, "
                        "actually wrote %d",
                        sizeof(stats), r);
        goto error;
    }

    r = mappedfile_commit(handle->mf);
    if (r) goto error;

    r = mappedfile_unlock(handle->mf);
    if (r) goto error;

    promhandle = handle;
    cyrus_modules_add(&prometheus_done, NULL);
    return;

error:
    if (handle) {
        if (handle->mf) {
            mappedfile_unlock(handle->mf);
            mappedfile_close(&handle->mf);
        }
        free(handle);
    }
    promhandle = NULL;
}

static void prometheus_done(void *rock __attribute__((unused)))
{
    struct prom_stats accum = PROM_STATS_INITIALIZER;
    struct prom_stats thisproc = PROM_STATS_INITIALIZER;
    struct mappedfile *doneprocs = NULL;
    char *doneprocs_fname = NULL;
    char *doneprocs_lock_fname;
    int doneprocs_lock_fd;
    int i, r = 0;
    int unlinked = 0;

    if (!promhandle) return; /* make double-call safe */

    /* hold a lock on .doneprocs.lock - this keeps promstatsd from double
     * counting while we're juggling files */
    doneprocs_lock_fname = strconcat(prometheus_stats_dir(), ".",
                                     FNAME_PROM_DONEPROCS, ".lock", NULL);

    doneprocs_lock_fd = open(doneprocs_lock_fname, O_CREAT|O_TRUNC|O_RDWR, 0644);
    if (doneprocs_lock_fd == -1) {
        syslog(LOG_ERR, "can't open doneprocs lock: %s (%m)", doneprocs_lock_fname);
        goto done;
    }
    if (lock_setlock(doneprocs_lock_fd, /*ex*/1, /*nb*/0, doneprocs_lock_fname)) {
        syslog(LOG_ERR, "can't get exclusive lock on %s", doneprocs_lock_fname);
        close(doneprocs_lock_fd);
        doneprocs_lock_fd = -1;
        goto done;
    }

    /* load existing doneprocs stats */
    doneprocs_fname = strconcat(prometheus_stats_dir(), FNAME_PROM_DONEPROCS,
                                ".", config_ident, NULL);
    r = mappedfile_open(&doneprocs, doneprocs_fname, MAPPEDFILE_CREATE | MAPPEDFILE_RW);
    if (r) {
        syslog(LOG_ERR, "IOERROR: mappedfile_open(%s): %s",
                        doneprocs_fname, error_message(r));
        goto done;
    }

    r = mappedfile_writelock(doneprocs);
    if (r) goto done;

    memcpy(&accum, mappedfile_base(doneprocs), mappedfile_size(doneprocs));
    if (accum.ident[0] == '\0') {
        snprintf(accum.ident, sizeof(accum.ident), "%s", config_ident);
    }

    /* read stats from this process */
    r = mappedfile_readlock(promhandle->mf);
    if (r) {
        syslog(LOG_ERR, "IOERROR: mappedfile_open(%s): %s",
                        doneprocs_fname, error_message(r));
        goto done;
    }
    memcpy(&thisproc, mappedfile_base(promhandle->mf), mappedfile_size(promhandle->mf));
    mappedfile_unlock(promhandle->mf);

    /* unlink per-process stats file, we don't need it anymore */
    r = unlink(mappedfile_fname(promhandle->mf));
    if (r && errno != ENOENT) goto done;
    unlinked = 1;

    /* accumulate the statistics */
    for (i = 0; i < PROM_NUM_METRICS; i++) {
        accum.metrics[i].value += thisproc.metrics[i].value;
        accum.metrics[i].last_updated = MAX(accum.metrics[i].last_updated,
                                            thisproc.metrics[i].last_updated);
    }

    /* and write it out */
    r = mappedfile_pwrite(doneprocs, &accum, sizeof(accum), 0);
    if (r != sizeof(accum)) {
        syslog(LOG_ERR, "IOERROR: mappedfile_pwrite: expected to write " SIZE_T_FMT "bytes, "
                        "actually wrote %d",
                        sizeof(accum), r);
        goto done;
    }

    mappedfile_commit(doneprocs);

done:
    free(doneprocs_fname);

    if (!unlinked) {
        syslog(LOG_NOTICE, "per-process prometheus statistics file not removed");
    }
    mappedfile_close(&promhandle->mf);

    free(promhandle);
    promhandle = NULL;

    mappedfile_unlock(doneprocs);
    mappedfile_close(&doneprocs);

    /* release .doneprocs.lock */
    if (doneprocs_lock_fd != -1) {
        unlink(doneprocs_lock_fname);
        lock_unlock(doneprocs_lock_fd, doneprocs_lock_fname);
        close(doneprocs_lock_fd);
    }
    free(doneprocs_lock_fname);
}

/* use the prometheus_increment() and prometheus_decrement() wrapper macros
 * for readability if that's all you're doing.
 */
EXPORTED void prometheus_apply_delta(enum prom_metric_id metric_id,
                                     double delta)
{
    struct prom_metric metric;
    size_t offset;
    int r;

    if (!prometheus_enabled) return;

    if (!promhandle) prometheus_init();

    if (!prometheus_enabled) return;

    assert(metric_id >= 0 && metric_id < PROM_NUM_METRICS);

    r = mappedfile_writelock(promhandle->mf);
    if (r) {
        syslog(LOG_ERR, "IOERROR: mappedfile_writelock unable to obtain lock on %s",
                        mappedfile_fname(promhandle->mf));
        return;
    }

    offset = offsetof(struct prom_stats, metrics) + metric_id * sizeof(metric);
    memcpy(&metric, mappedfile_base(promhandle->mf) + offset, sizeof(metric));
    if (delta < 0) {
        /* counters must not be decremented */
        assert(prom_metric_descs[metric_id].type != PROM_METRIC_COUNTER);
    }
    metric.value = metric.value + delta;
    metric.last_updated = now_ms();

    r = mappedfile_pwrite(promhandle->mf, &metric, sizeof(metric), offset);
    if (r != sizeof(metric)) {
        syslog(LOG_ERR, "IOERROR: mappedfile_pwrite: expected to write "
                        SIZE_T_FMT " bytes, actually wrote %d",
                        sizeof(metric), r);
    }
    else {
        mappedfile_commit(promhandle->mf);
    }

    mappedfile_unlock(promhandle->mf);
}

EXPORTED int prometheus_text_report(struct buf *buf, const char **mimetype)
{
    char *report_fname = NULL;
    struct mappedfile *mf = NULL;
    int r;

    if (!prometheus_enabled) return IMAP_INTERNAL;

    report_fname = strconcat(prometheus_stats_dir(), FNAME_PROM_REPORT, NULL);

    r = mappedfile_open(&mf, report_fname, 0);
    if (r) {
        free(report_fname);
        return r;
    }

    r = mappedfile_readlock(mf);
    if (!r) {
        buf_setmap(buf, mappedfile_base(mf), mappedfile_size(mf));
        if (mimetype)
            *mimetype = "text/plain; version=0.0.4";
    }

    mappedfile_unlock(mf);
    mappedfile_close(&mf);
    free(report_fname);

    if (r) return r;

    report_fname = strconcat(prometheus_stats_dir(), FNAME_PROM_MASTER_REPORT, NULL);

    r = mappedfile_open(&mf, report_fname, 0);
    if (r) {
        free(report_fname);
        return 0; /* no master.txt yet? no worries */
    }

    r = mappedfile_readlock(mf);
    if (!r) {
        buf_appendmap(buf, mappedfile_base(mf), mappedfile_size(mf));
    }

    mappedfile_unlock(mf);
    mappedfile_close(&mf);
    free(report_fname);

    return 0;
}

EXPORTED enum prom_metric_id prometheus_lookup_label(enum prom_labelled_metric metric,
                                                     const char *value)
{
    size_t i;

    assert(metric >= 0 && metric < PROM_NUM_LABELLED_METRICS);

    for (i = 0; prom_label_lookup_table[metric][i].value != NULL; i++) {
        const struct prom_label_lookup_value *v = &prom_label_lookup_table[metric][i];

        int cmp = strcmp(v->value, value);
        if (cmp == 0) /* found it */
            return v->id;
        if (cmp > 0) /* gone too far, not found */
            break;
    }

    fatal("invalid metric value -- compile time bug", EX_SOFTWARE);
}
