// SPDX-License-Identifier: GPL-3.0-or-later

// do not REMOVE this, it is used by systemd-journal includes to prevent saving the file, function, line of the
// source code that makes the calls, allowing our loggers to log the lines of source code that actually log
#define SD_JOURNAL_SUPPRESS_LOCATION

#include "../libnetdata.h"
#include "nd_log-internals.h"
#include "../stacktrace/stacktrace.h"

const char *program_name = "";
uint64_t debug_flags = 0;
int aclklog_enabled = 0;

// --------------------------------------------------------------------------------------------------------------------

ALWAYS_INLINE void errno_clear(void) {
    errno = 0;

#if defined(OS_WINDOWS)
    SetLastError(ERROR_SUCCESS);
#endif
}

// --------------------------------------------------------------------------------------------------------------------
// logger router

static ND_LOG_METHOD nd_logger_select_output(ND_LOG_SOURCES source, FILE **fpp, int *fdp, netdata_mutex_t **mutexp) {
    bool mutexes_initialized = __atomic_load_n(&nd_log.mutexes_initialized, __ATOMIC_ACQUIRE);

    *fdp = -1;
    *mutexp = NULL;

    if(source >= _NDLS_MAX)
        source = NDLS_DAEMON;

    ND_LOG_METHOD output = nd_log.sources[source].method;

    switch(output) {
        case NDLM_JOURNAL:
            if(unlikely(!nd_log.journal_direct.initialized && !nd_log.journal.initialized)) {
                output = NDLM_FILE;
                *fpp = stderr;
                *fdp = STDERR_FILENO;
                *mutexp = &nd_log.std_error.mutex;
            }
            else {
                *fpp = NULL;
            }
            break;

#if defined(OS_WINDOWS) && (defined(HAVE_ETW) || defined(HAVE_WEL))
#if defined(HAVE_ETW)
        case NDLM_ETW:
#endif
#if defined(HAVE_WEL)
        case NDLM_WEL:
#endif
            if(unlikely(!nd_log.eventlog.initialized)) {
                output = NDLM_FILE;
                *fpp = stderr;
                *fdp = STDERR_FILENO;
                *mutexp = &nd_log.std_error.mutex;
            }
            else {
                *fpp = NULL;
            }
            break;
#endif

        case NDLM_SYSLOG:
            if(unlikely(!nd_log.syslog.initialized)) {
                output = NDLM_FILE;
                *fpp = stderr;
                *fdp = STDERR_FILENO;
                *mutexp = &nd_log.std_error.mutex;
            }
            else {
                *fpp = NULL;
            }
            break;

        case NDLM_FILE:
            if(!nd_log.sources[source].fp) {
                *fpp = stderr;
                *fdp = STDERR_FILENO;
                *mutexp = &nd_log.std_error.mutex;
            }
            else {
                *fpp = nd_log.sources[source].fp;
                *fdp = nd_log.sources[source].fd;
                *mutexp = &nd_log.sources[source].mutex;
            }
            break;

        case NDLM_STDOUT:
            output = NDLM_FILE;
            *fpp = stdout;
            *fdp = STDOUT_FILENO;
            *mutexp = &nd_log.std_output.mutex;
            break;

        default:
        case NDLM_DEFAULT:
        case NDLM_STDERR:
            output = NDLM_FILE;
            *fpp = stderr;
            *fdp = STDERR_FILENO;
            *mutexp = &nd_log.std_error.mutex;
            break;

        case NDLM_DISABLED:
        case NDLM_DEVNULL:
            output = NDLM_DISABLED;
            *fpp = NULL;
            break;
    }

    if(output == NDLM_FILE && *fpp && *fdp < 0) {
        *fpp = stderr;
        *fdp = STDERR_FILENO;
        *mutexp = &nd_log.std_error.mutex;
    }

    // Constructor-time logging can happen before the explicit startup init
    // path runs. Until then, bypass write serialization and log unlocked.
    if(nd_log.single_threaded_child || !mutexes_initialized)
        *mutexp = NULL;

    return output;
}

static inline netdata_mutex_t *nd_logger_stderr_mutex(void) {
    if(nd_log.single_threaded_child)
        return NULL;

    if(!__atomic_load_n(&nd_log.mutexes_initialized, __ATOMIC_ACQUIRE))
        return NULL;

    return &nd_log.std_error.mutex;
}

// --------------------------------------------------------------------------------------------------------------------

static __thread bool nd_log_fatal_event = false;

static void nd_log_fatal_hook(struct log_field *fields, size_t fields_max __maybe_unused) {
    if(!nd_log_fatal_event)
        return;

    nd_log_fatal_event = false;

    if(!nd_log.fatal_hook_cb)
        return;

    const char *filename = log_field_strdupz(&fields[NDF_FILE]);
    const char *message = log_field_strdupz(&fields[NDF_MESSAGE]);
    const char *function = log_field_strdupz(&fields[NDF_FUNC]);
    const char *stack_trace = log_field_strdupz(&fields[NDF_STACK_TRACE]);
    const char *errno_str = log_field_strdupz(&fields[NDF_ERRNO]);
    long line = log_field_to_int64(&fields[NDF_LINE]);

    nd_log.fatal_hook_cb(filename, function, message, errno_str, stack_trace, line);
}

void nd_log_register_fatal_hook_cb(log_event_t cb) {
    nd_log.fatal_hook_cb = cb;
}

// --------------------------------------------------------------------------------------------------------------------

void nd_log_register_fatal_final_cb(fatal_event_t cb) {
    nd_log.fatal_final_cb = cb;
}

// --------------------------------------------------------------------------------------------------------------------
// high level logger

// Write serialization uses a netdata_mutex_t (sleeping mutex) per output
// destination, passed through from nd_logger_select_output(). Post-fork nofork
// spawn-server children stay single-threaded in-tree, so they bypass logger
// locking instead of trying to reuse inherited lock state.
static void nd_logger_log_fields(FILE *fp, int fd, netdata_mutex_t *mutex, bool limit,
                                 ND_LOG_FIELD_PRIORITY priority,
                                 ND_LOG_METHOD output, struct nd_log_source *source,
                                 struct log_field *fields, size_t fields_max) {
    nd_log_fatal_hook(fields, fields_max);

    // check the limits (uses its own source->limits.spinlock internally)
    if(limit && nd_log_limit_reached(source))
        return;

    if(output == NDLM_JOURNAL) {
        if(!nd_logger_journal_direct(fields, fields_max) && !nd_logger_journal_libsystemd(fields, fields_max)) {
            // we can't log to journal, let's log to stderr
            output = NDLM_FILE;
            fp = stderr;
            fd = STDERR_FILENO;
            mutex = nd_logger_stderr_mutex();
        }
    }

#if defined(OS_WINDOWS)
#if defined(HAVE_ETW)
    if(output == NDLM_ETW) {
        if(!nd_logger_etw(source, fields, fields_max)) {
            // we can't log to windows events, let's log to stderr
            output = NDLM_FILE;
            fp = stderr;
            fd = STDERR_FILENO;
            mutex = nd_logger_stderr_mutex();
        }
    }
#endif
#if defined(HAVE_WEL)
    if(output == NDLM_WEL) {
        if(!nd_logger_wel(source, fields, fields_max)) {
            // we can't log to windows events, let's log to stderr
            output = NDLM_FILE;
            fp = stderr;
            fd = STDERR_FILENO;
            mutex = nd_logger_stderr_mutex();
        }
    }
#endif
#endif

    if(output == NDLM_SYSLOG)
        nd_logger_syslog(priority, source->format, fields, fields_max);

    if(output == NDLM_FILE)
        nd_logger_file(fd, fp, mutex, source->format, fields, fields_max);
}

static void nd_logger_unset_all_thread_fields(void) {
    size_t fields_max = THREAD_FIELDS_MAX;
    for(size_t i = 0; i < fields_max ; i++)
        thread_log_fields[i].entry.set = false;
}

static void nd_logger_merge_log_stack_to_thread_fields(void) {
    for(size_t c = 0; c < thread_log_stack_next ;c++) {
        struct log_stack_entry *lgs = thread_log_stack_base[c];

        for(size_t i = 0; lgs[i].id != NDF_STOP ; i++) {
            if(lgs[i].id >= _NDF_MAX || !lgs[i].set)
                continue;

            struct log_stack_entry *e = &lgs[i];
            ND_LOG_STACK_FIELD_TYPE type = lgs[i].type;

            // do not add empty / unset fields
            if((type == NDFT_TXT && (!e->txt || !*e->txt)) ||
                (type == NDFT_BFR && (!e->bfr || !buffer_strlen(e->bfr))) ||
                (type == NDFT_STR && !e->str) ||
                (type == NDFT_UUID && (!e->uuid || uuid_is_null(*e->uuid))) ||
                (type == NDFT_CALLBACK && !e->cb.formatter) ||
                type == NDFT_UNSET)
                continue;

            thread_log_fields[lgs[i].id].entry = *e;
        }
    }
}

static void nd_logger(const char *file, const char *function, const unsigned long line,
               ND_LOG_SOURCES source, ND_LOG_FIELD_PRIORITY priority, bool limit,
               int saved_errno, size_t saved_winerror __maybe_unused, const char *fmt, va_list ap) {

    FILE *fp;
    int fd;
    netdata_mutex_t *mutex;
    ND_LOG_METHOD output = nd_logger_select_output(source, &fp, &fd, &mutex);
    if(!IS_FINAL_LOG_METHOD(output))
        return;

    // mark all fields as unset
    nd_logger_unset_all_thread_fields();

    // flatten the log stack into the fields
    nd_logger_merge_log_stack_to_thread_fields();

    // set the common fields that are automatically set by the logging subsystem

#if 0
    // getting stack traces is crashing on some architectures, so we get them only for daemon status file
    if(likely(!thread_log_fields[NDF_STACK_TRACE].entry.set) && priority <= NDLP_ALERT) // only on fatal errors
        thread_log_fields[NDF_STACK_TRACE].entry = ND_LOG_FIELD_CB(NDF_STACK_TRACE, stack_trace_formatter, NULL);
#endif

    if(likely(!thread_log_fields[NDF_INVOCATION_ID].entry.set))
        thread_log_fields[NDF_INVOCATION_ID].entry = ND_LOG_FIELD_UUID(NDF_INVOCATION_ID, &nd_log.invocation_id);

    if(likely(!thread_log_fields[NDF_LOG_SOURCE].entry.set))
        thread_log_fields[NDF_LOG_SOURCE].entry = ND_LOG_FIELD_TXT(NDF_LOG_SOURCE, nd_log_id2source(source));
    else {
        ND_LOG_SOURCES src = source;

        if(thread_log_fields[NDF_LOG_SOURCE].entry.type == NDFT_TXT)
            src = nd_log_source2id(thread_log_fields[NDF_LOG_SOURCE].entry.txt, source);
        else if(thread_log_fields[NDF_LOG_SOURCE].entry.type == NDFT_U64)
            src = thread_log_fields[NDF_LOG_SOURCE].entry.u64;

        if(src != source && src < _NDLS_MAX) {
            source = src;
            output = nd_logger_select_output(source, &fp, &fd, &mutex);
            if(output != NDLM_FILE && output != NDLM_JOURNAL && output != NDLM_SYSLOG)
                return;
        }
    }

    if(likely(!thread_log_fields[NDF_SYSLOG_IDENTIFIER].entry.set))
        thread_log_fields[NDF_SYSLOG_IDENTIFIER].entry = ND_LOG_FIELD_TXT(NDF_SYSLOG_IDENTIFIER, program_name);

    if(likely(!thread_log_fields[NDF_LINE].entry.set)) {
        thread_log_fields[NDF_LINE].entry = ND_LOG_FIELD_U64(NDF_LINE, line);
        thread_log_fields[NDF_FILE].entry = ND_LOG_FIELD_TXT(NDF_FILE, file);
        thread_log_fields[NDF_FUNC].entry = ND_LOG_FIELD_TXT(NDF_FUNC, function);
    }

    if(likely(!thread_log_fields[NDF_PRIORITY].entry.set)) {
        thread_log_fields[NDF_PRIORITY].entry = ND_LOG_FIELD_U64(NDF_PRIORITY, priority);
    }

    if(likely(!thread_log_fields[NDF_TID].entry.set))
        thread_log_fields[NDF_TID].entry = ND_LOG_FIELD_U64(NDF_TID, gettid_cached());

    if(likely(!thread_log_fields[NDF_THREAD_TAG].entry.set)) {
        const char *thread_tag = nd_thread_tag();
        thread_log_fields[NDF_THREAD_TAG].entry = ND_LOG_FIELD_TXT(NDF_THREAD_TAG, thread_tag);

        // TODO: fix the ND_MODULE in logging by setting proper module name in threads
//        if(!thread_log_fields[NDF_MODULE].entry.set)
//            thread_log_fields[NDF_MODULE].entry = ND_LOG_FIELD_CB(NDF_MODULE, thread_tag_to_module, (void *)thread_tag);
    }

    if(likely(!thread_log_fields[NDF_TIMESTAMP_REALTIME_USEC].entry.set))
        thread_log_fields[NDF_TIMESTAMP_REALTIME_USEC].entry = ND_LOG_FIELD_U64(NDF_TIMESTAMP_REALTIME_USEC, now_realtime_usec());

    if(saved_errno != 0 && !thread_log_fields[NDF_ERRNO].entry.set)
        thread_log_fields[NDF_ERRNO].entry = ND_LOG_FIELD_I64(NDF_ERRNO, saved_errno);

    if(saved_winerror != 0 && !thread_log_fields[NDF_WINERROR].entry.set)
        thread_log_fields[NDF_WINERROR].entry = ND_LOG_FIELD_U64(NDF_WINERROR, saved_winerror);

    CLEAN_BUFFER *wb = NULL;
    if(fmt && !thread_log_fields[NDF_MESSAGE].entry.set) {
        wb = buffer_create(1024, NULL);
        buffer_vsprintf(wb, fmt, ap);
        thread_log_fields[NDF_MESSAGE].entry = ND_LOG_FIELD_TXT(NDF_MESSAGE, buffer_tostring(wb));
    }

    nd_logger_log_fields(fp, fd, mutex, limit, priority, output, &nd_log.sources[source],
                         thread_log_fields, THREAD_FIELDS_MAX);

    if(nd_log.sources[source].pending_msg && spinlock_trylock(&nd_log.sources[source].limits.spinlock)) {
        // we have to check again if the pending message is still there

        const char *pending_msg = nd_log.sources[source].pending_msg;

        if(pending_msg) {
            nd_logger_unset_all_thread_fields();

            thread_log_fields[NDF_TIMESTAMP_REALTIME_USEC].entry = (struct log_stack_entry){
                .set = true,
                .type = NDFT_U64,
                .u64 = now_realtime_usec(),
            };

            thread_log_fields[NDF_LOG_SOURCE].entry = (struct log_stack_entry){
                .set = true,
                .type = NDFT_TXT,
                .txt = nd_log_id2source(source),
            };

            thread_log_fields[NDF_SYSLOG_IDENTIFIER].entry = (struct log_stack_entry){
                .set = true,
                .type = NDFT_TXT,
                .txt = program_name,
            };

            thread_log_fields[NDF_MESSAGE].entry = (struct log_stack_entry){
                .set = true,
                .type = NDFT_TXT,
                .txt = pending_msg,
            };

            thread_log_fields[NDF_MESSAGE_ID].entry = (struct log_stack_entry){
                .set = nd_log.sources[source].pending_msgid != NULL,
                .type = NDFT_UUID,
                .uuid = nd_log.sources[source].pending_msgid,
            };

            nd_log.sources[source].pending_msg = NULL;
            nd_log.sources[source].pending_msgid = NULL;
        }

        spinlock_unlock(&nd_log.sources[source].limits.spinlock);

        if(pending_msg)
            nd_logger_log_fields(fp, fd, mutex, false, priority, output, &nd_log.sources[source],
                                 thread_log_fields, THREAD_FIELDS_MAX);

        freez((void *)pending_msg);
    }

    errno_clear();
}

static ND_LOG_SOURCES nd_log_validate_source(ND_LOG_SOURCES source) {
    if(source >= _NDLS_MAX)
        source = NDLS_DAEMON;

    if(nd_log.overwrite_process_source)
        source = nd_log.overwrite_process_source;

    return source;
}

// --------------------------------------------------------------------------------------------------------------------
// public API for loggers

NEVER_INLINE
void netdata_logger(ND_LOG_SOURCES source, ND_LOG_FIELD_PRIORITY priority, const char *file, const char *function, unsigned long line, const char *fmt, ... )
{
    int saved_errno = errno;

    size_t saved_winerror = 0;
#if defined(OS_WINDOWS)
    saved_winerror = GetLastError();
#endif

    source = nd_log_validate_source(source);

    if (source != NDLS_DEBUG && priority > nd_log.sources[source].min_priority)
        return;

    va_list args;
    va_start(args, fmt);
    nd_logger(file, function, line, source, priority,
              source == NDLS_DAEMON || source == NDLS_COLLECTORS,
              saved_errno, saved_winerror, fmt, args);
    va_end(args);
}

NEVER_INLINE
void netdata_logger_with_limit(ERROR_LIMIT *erl, ND_LOG_SOURCES source, ND_LOG_FIELD_PRIORITY priority, const char *file __maybe_unused, const char *function __maybe_unused, const unsigned long line __maybe_unused, const char *fmt, ... ) {
    int saved_errno = errno;

    size_t saved_winerror = 0;
#if defined(OS_WINDOWS)
    saved_winerror = GetLastError();
#endif

    source = nd_log_validate_source(source);

    if (source != NDLS_DEBUG && priority > nd_log.sources[source].min_priority)
        return;

    if(erl->sleep_ut)
        sleep_usec(erl->sleep_ut);

    if(!nd_log.single_threaded_child)
        spinlock_lock(&erl->spinlock);

    erl->count++;
    time_t now = now_boottime_sec();
    if(now - erl->last_logged < erl->log_every) {
        if(!nd_log.single_threaded_child)
            spinlock_unlock(&erl->spinlock);
        return;
    }

    if(!nd_log.single_threaded_child)
        spinlock_unlock(&erl->spinlock);

    va_list args;
    va_start(args, fmt);
    nd_logger(file, function, line, source, priority,
            source == NDLS_DAEMON || source == NDLS_COLLECTORS,
            saved_errno, saved_winerror, fmt, args);
    va_end(args);
    erl->last_logged = now;
    erl->count = 0;
}

NEVER_INLINE NORETURN
static void recursive_fatal_abort(void) {
    // keep this as a separate function, to have it logged like this in sentry
#ifdef ENABLE_SENTRY
    abort();
#endif
    _exit(1);
}

#ifdef NETDATA_INTERNAL_CHECKS
NEVER_INLINE NORETURN
static void fatal_abort_internal_checks(void) {
    // keep this as a separate function, to have it logged like this in sentry
    abort();
    _exit(1);
}
#endif

NEVER_INLINE
void netdata_logger_fatal(const char *file, const char *function, const unsigned long line, const char *fmt, ... ) {
    // Per-thread re-entrancy flag and a process-wide "a fatal is in progress" counter. These detect two different
    // situations that the previous single
    // global counter could not tell apart:
    //  1. Same-thread re-entrancy: this thread called fatal() while already
    //     handling its own fatal (e.g. an allocation in the logging path below
    //     failed and re-entered fatal). That is a real bug in the fatal path
    //     itself; abort with the recursive marker so it is visible.
    //  2. Concurrency: a DIFFERENT thread is already handling a fatal. This is
    //     common when a deadlock makes several threads time out and call fatal()
    //     at once. The first thread owns the fatal record; this one must not
    //     interleave its output or re-run cleanup, and it must NOT abort via
    //     recursive_fatal_abort() -- that would surface as a separate crash and
    //     mask the first (real) fatal. It waits a bounded time for the first to
    //     finish writing, then exits quietly.
    static __thread bool this_thread_in_fatal = false;
    static size_t threads_in_fatal = 0;

    // NOTE: the two exit paths below must NOT use stdio (fprintf/fflush). The
    // first fatal thread is likely holding stderr's stdio lock mid-write, and a
    // second fprintf(stderr) would block on that lock indefinitely -- defeating
    // the bounded exit these paths are meant to guarantee. Use a raw write(),
    // which takes no lock.
    char msg[512];

    if(this_thread_in_fatal) {
        // (1) same thread re-entered fatal: the outer fatal is suspended below
        // us on this stack and cannot make progress, so do not wait for it.
        int n = snprintf(msg, sizeof(msg),
                "\nRECURSIVE FATAL STATEMENTS, latest from %s() of %lu@%s, EXITING NOW! 23e93dfccbf64e11aac858b9410d8a82\n",
                function, line, file);
        if(n > 0 && write(STDERR_FILENO, msg, (size_t)((n < (int)sizeof(msg)) ? n : (int)sizeof(msg) - 1)) < 0) { /* best effort */ }
        recursive_fatal_abort();
    }
    this_thread_in_fatal = true;

    if(__atomic_add_fetch(&threads_in_fatal, 1, __ATOMIC_SEQ_CST) > 1) {
        // (2) another thread is already writing a fatal. Give it a bounded
        // chance to finish, then exit quietly (not via recursive_fatal_abort,
        // which would mask the first fatal as a crash of its own).
        int n = snprintf(msg, sizeof(msg),
                "\nCONCURRENT FATAL from %s() of %lu@%s, deferring to the first fatal and exiting.\n",
                function, line, file);
        if(n > 0 && write(STDERR_FILENO, msg, (size_t)((n < (int)sizeof(msg)) ? n : (int)sizeof(msg) - 1)) < 0) { /* best effort */ }
        sleep(2); // give the first fatal the chance to be written
        _exit(1);
    }

    // send this event to deamon_status_file
    nd_log_fatal_event = true;

    int saved_errno = errno;
    size_t saved_winerror = 0;
#if defined(OS_WINDOWS)
    saved_winerror = GetLastError();
#endif

    // make sure the msg id does not leak
    {
        ND_LOG_STACK lgs[] = {
            ND_LOG_FIELD_UUID(NDF_MESSAGE_ID, &netdata_fatal_msgid),
            ND_LOG_FIELD_END(),
        };
        ND_LOG_STACK_PUSH(lgs);

        ND_LOG_SOURCES source = NDLS_DAEMON;
        source = nd_log_validate_source(source);

        va_list args;
        va_start(args, fmt);
        nd_logger(file, function, line, source, NDLP_ALERT, true, saved_errno, saved_winerror, fmt, args);
        va_end(args);
    }

#if defined(FSANITIZE_ADDRESS)
    fprintf(stderr, "FATAL: %04lu@%s:%s, errno = %d\n", line, file, function, saved_errno);
#endif

#ifdef NETDATA_INTERNAL_CHECKS
    fatal_abort_internal_checks();
#endif

    if(nd_log.fatal_final_cb)
        nd_log.fatal_final_cb();

    exit(1);
}
