lib_log.c

/*
 * Copyright (C) 2023 Jo-Philipp Wich <jo@mein.io>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, 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.
 */

/**
 * # System logging functions
 *
 * The `log` module provides bindings to the POSIX syslog functions `openlog()`,
 * `syslog()` and `closelog()` as well as - when available - the OpenWrt
 * specific ulog library functions.
 *
 * Functions can be individually imported and directly accessed using the
 * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#named_import named import}
 * syntax:
 *
 *   ```
 *   import { openlog, syslog, LOG_PID, LOG_USER, LOG_ERR } from 'log';
 *
 *   openlog("my-log-ident", LOG_PID, LOG_USER);
 *   syslog(LOG_ERR, "An error occurred!");
 *
 *   // OpenWrt specific ulog functions
 *   import { ulog_open, ulog, ULOG_SYSLOG, LOG_DAEMON, LOG_INFO } from 'log';
 *
 *   ulog_open(ULOG_SYSLOG, LOG_DAEMON, "my-log-ident");
 *   ulog(LOG_INFO, "The current epoch is %d", time());
 *   ```
 *
 * Alternatively, the module namespace can be imported
 * using a wildcard import statement:
 *
 *   ```
 *   import * as log from 'log';
 *
 *   log.openlog("my-log-ident", log.LOG_PID, log.LOG_USER);
 *   log.syslog(log.LOG_ERR, "An error occurred!");
 *
 *   // OpenWrt specific ulog functions
 *   log.ulog_open(log.ULOG_SYSLOG, log.LOG_DAEMON, "my-log-ident");
 *   log.ulog(log.LOG_INFO, "The current epoch is %d", time());
 *   ```
 *
 * Additionally, the log module namespace may also be imported by invoking the
 * `ucode` interpreter with the `-llog` switch.
 *
 * ## Constants
 *
 * The `log` module declares a number of numeric constants to specify logging
 * facility, priority and option values, as well as ulog specific channels.
 *
 * ### Syslog Options
 *
 * | Constant Name | Description                                             |
 * |---------------|---------------------------------------------------------|
 * | `LOG_PID`     | Include PID with each message.                          |
 * | `LOG_CONS`    | Log to console if error occurs while sending to syslog. |
 * | `LOG_NDELAY`  | Open the connection to the logger immediately.          |
 * | `LOG_ODELAY`  | Delay open until the first message is logged.           |
 * | `LOG_NOWAIT`  | Do not wait for child processes created during logging. |
 *
 * ### Syslog Facilities
 *
 * | Constant Name  | Description                                      |
 * |----------------|--------------------------------------------------|
 * | `LOG_AUTH`     | Authentication/authorization messages.           |
 * | `LOG_AUTHPRIV` | Private authentication messages.                 |
 * | `LOG_CRON`     | Clock daemon (cron and at commands).             |
 * | `LOG_DAEMON`   | System daemons without separate facility values. |
 * | `LOG_FTP`      | FTP server daemon.                               |
 * | `LOG_KERN`     | Kernel messages.                                 |
 * | `LOG_LPR`      | Line printer subsystem.                          |
 * | `LOG_MAIL`     | Mail system.                                     |
 * | `LOG_NEWS`     | Network news subsystem.                          |
 * | `LOG_SYSLOG`   | Messages generated internally by syslogd.        |
 * | `LOG_USER`     | Generic user-level messages.                     |
 * | `LOG_UUCP`     | UUCP subsystem.                                  |
 * | `LOG_LOCAL0`   | Local use 0 (custom facility).                   |
 * | `LOG_LOCAL1`   | Local use 1 (custom facility).                   |
 * | `LOG_LOCAL2`   | Local use 2 (custom facility).                   |
 * | `LOG_LOCAL3`   | Local use 3 (custom facility).                   |
 * | `LOG_LOCAL4`   | Local use 4 (custom facility).                   |
 * | `LOG_LOCAL5`   | Local use 5 (custom facility).                   |
 * | `LOG_LOCAL6`   | Local use 6 (custom facility).                   |
 * | `LOG_LOCAL7`   | Local use 7 (custom facility).                   |
 *
 * ### Syslog Priorities
 *
 * | Constant Name | Description                         |
 * |---------------|-------------------------------------|
 * | `LOG_EMERG`   | System is unusable.                 |
 * | `LOG_ALERT`   | Action must be taken immediately.   |
 * | `LOG_CRIT`    | Critical conditions.                |
 * | `LOG_ERR`     | Error conditions.                   |
 * | `LOG_WARNING` | Warning conditions.                 |
 * | `LOG_NOTICE`  | Normal, but significant, condition. |
 * | `LOG_INFO`    | Informational message.              |
 * | `LOG_DEBUG`   | Debug-level message.                |
 *
 * ### Ulog channels
 *
 * | Constant Name | Description                          |
 * |---------------|--------------------------------------|
 * | `ULOG_KMSG`   | Log messages to `/dev/kmsg` (dmesg). |
 * | `ULOG_STDIO`  | Log messages to stdout.              |
 * | `ULOG_SYSLOG` | Log messages to syslog.              |
 *
 * @module log
 */

#include <syslog.h>
#include <errno.h>

#ifdef HAVE_ULOG
#include <libubox/ulog.h>
#endif

#include "ucode/module.h"


static char log_ident[32];

/**
 * The following log option strings are recognized:
 *
 * | Log Option | Description                                                |
 * |------------|------------------------------------------------------------|
 * | `"pid"`    | Include PID with each message.                             |
 * | `"cons"`   | Log to console if an error occurs while sending to syslog. |
 * | `"ndelay"` | Open the connection to the logger immediately.             |
 * | `"odelay"` | Delay open until the first message is logged.              |
 * | `"nowait"` | Do not wait for child processes created during logging.    |
 *
 * @typedef {string} module:log.LogOption
 * @enum {module:log.LogOption}
 *
 */
static const struct { const char *name; int value; } log_options[] = {
	{ "pid", LOG_PID },
	{ "cons", LOG_CONS },
	{ "ndelay", LOG_NDELAY },
	{ "odelay", LOG_ODELAY },
	{ "nowait", LOG_NOWAIT },
};

/**
 * The following log facility strings are recognized:
 *
 * | Facility     | Description                                      |
 * |--------------|--------------------------------------------------|
 * | `"auth"`     | Authentication/authorization messages.           |
 * | `"authpriv"` | Private authentication messages.                 |
 * | `"cron"`     | Clock daemon (cron and at commands).             |
 * | `"daemon"`   | System daemons without separate facility values. |
 * | `"ftp"`      | FTP server daemon.                               |
 * | `"kern"`     | Kernel messages.                                 |
 * | `"lpr"`      | Line printer subsystem.                          |
 * | `"mail"`     | Mail system.                                     |
 * | `"news"`     | Network news subsystem.                          |
 * | `"syslog"`   | Messages generated internally by syslogd.        |
 * | `"user"`     | Generic user-level messages.                     |
 * | `"uucp"`     | UUCP subsystem.                                  |
 * | `"local0"`   | Local use 0 (custom facility).                   |
 * | `"local1"`   | Local use 1 (custom facility).                   |
 * | `"local2"`   | Local use 2 (custom facility).                   |
 * | `"local3"`   | Local use 3 (custom facility).                   |
 * | `"local4"`   | Local use 4 (custom facility).                   |
 * | `"local5"`   | Local use 5 (custom facility).                   |
 * | `"local6"`   | Local use 6 (custom facility).                   |
 * | `"local7"`   | Local use 7 (custom facility).                   |
 *
 * @typedef {string} module:log.LogFacility
 * @enum {module:log.LogFacility}
 */
static const struct { const char *name; int value; } log_facilities[] = {
	{ "auth", LOG_AUTH },
#ifdef LOG_AUTHPRIV
	{ "authpriv", LOG_AUTHPRIV },
#endif
	{ "cron", LOG_CRON },
	{ "daemon", LOG_DAEMON },
#ifdef LOG_FTP
	{ "ftp", LOG_FTP },
#endif
	{ "kern", LOG_KERN },
	{ "lpr", LOG_LPR },
	{ "mail", LOG_MAIL },
	{ "news", LOG_NEWS },
	{ "syslog", LOG_SYSLOG },
	{ "user", LOG_USER },
	{ "uucp", LOG_UUCP },
	{ "local0", LOG_LOCAL0 },
	{ "local1", LOG_LOCAL1 },
	{ "local2", LOG_LOCAL2 },
	{ "local3", LOG_LOCAL3 },
	{ "local4", LOG_LOCAL4 },
	{ "local5", LOG_LOCAL5 },
	{ "local6", LOG_LOCAL6 },
	{ "local7", LOG_LOCAL7 },
};

/**
 * The following log priority strings are recognized:
 *
 * | Priority    | Description                         |
 * |-------------|-------------------------------------|
 * | `"emerg"`   | System is unusable.                 |
 * | `"alert"`   | Action must be taken immediately.   |
 * | `"crit"`    | Critical conditions.                |
 * | `"err"`     | Error conditions.                   |
 * | `"warning"` | Warning conditions.                 |
 * | `"notice"`  | Normal, but significant, condition. |
 * | `"info"`    | Informational message.              |
 * | `"debug"`   | Debug-level message.                |
 *
 * @typedef {string} module:log.LogPriority
 * @enum {module:log.LogPriority}
 */
static const struct { const char *name; int value; } log_priorities[] = {
	{ "emerg", LOG_EMERG },
	{ "alert", LOG_ALERT },
	{ "crit", LOG_CRIT },
	{ "err", LOG_ERR },
	{ "warning", LOG_WARNING },
	{ "notice", LOG_NOTICE },
	{ "info", LOG_INFO },
	{ "debug", LOG_DEBUG },
};


static int
parse_facility(uc_value_t *facility)
{
	char *s;
	int rv;

	switch (ucv_type(facility)) {
	case UC_STRING:
		s = ucv_string_get(facility);

		for (size_t i = 0; i < ARRAY_SIZE(log_facilities); i++)
			if (s && !strcasecmp(s, log_facilities[i].name))
				return log_facilities[i].value;

		return -1;

	case UC_INTEGER:
		rv = ucv_int64_get(facility);

		if (errno == ERANGE || rv < 0)
			return -1;

		return rv;

	case UC_NULL:
		return 0;

	default:
		return -1;
	}
}

static int
parse_options(uc_value_t *option)
{
	char *s;
	int rv;

	switch (ucv_type(option)) {
	case UC_ARRAY:
		rv = 0;

		for (size_t i = 0; i < ucv_array_length(option); i++) {
			uc_value_t *opt = ucv_array_get(option, i);
			char *s = ucv_string_get(opt);

			for (size_t j = 0; j < ARRAY_SIZE(log_options); j++) {
				if (s && !strcasecmp(log_options[j].name, s))
					rv |= log_options[j].value;
				else
					return -1;
			}
		}

		return rv;

	case UC_STRING:
		s = ucv_string_get(option);

		for (size_t i = 0; i < ARRAY_SIZE(log_options); i++)
			if (s && !strcasecmp(s, log_options[i].name))
				return log_options[i].value;

		return -1;

	case UC_INTEGER:
		rv = ucv_int64_get(option);

		if (errno == ERANGE || rv < 0)
			return -1;

		return rv;

	case UC_NULL:
		return 0;

	default:
		return -1;
	}
}

static int
parse_priority(uc_value_t *priority)
{
	char *s;
	int rv;

	switch (ucv_type(priority)) {
	case UC_STRING:
		s = ucv_string_get(priority);

		for (size_t i = 0; i < ARRAY_SIZE(log_priorities); i++)
			if (s && !strcasecmp(s, log_priorities[i].name))
				return log_priorities[i].value;

		return -1;

	case UC_INTEGER:
		rv = ucv_int64_get(priority);

		if (errno == ERANGE || rv < 0)
			return -1;

		return rv;

	case UC_NULL:
		return LOG_INFO;

	default:
		return -1;
	}
}

static char *
parse_ident(uc_vm_t *vm, uc_value_t *ident)
{
	if (!ident)
		return NULL;

	char *s = ucv_to_string(vm, ident);

	snprintf(log_ident, sizeof(log_ident), "%s", s ? s : "");
	free(s);

	return log_ident[0] ? log_ident : NULL;
}

/**
 * Open connection to system logger.
 *
 * The `openlog()` function instructs the program to establish a connection to
 * the system log service and configures the default facility and identification
 * for use in subsequent log operations. It may be omitted, in which case the
 * first call to `syslog()` will implicitly call `openlog()` with a default
 * ident value representing the program name and a default `LOG_USER` facility.
 *
 * The log option argument may be either a single string value containing an
 * option name, an array of option name strings or a numeric value representing
 * a bitmask of `LOG_*` option constants.
 *
 * The facility argument may be either a single string value containing a
 * facility name or one of the numeric `LOG_*` facility constants in the module
 * namespace.
 *
 * Returns `true` if the system `openlog()` function was invoked.
 *
 * Returns `false` if an invalid argument, such as an unrecognized option or
 * facility name, was provided.
 *
 * @function module:log#openlog
 *
 * @param {string} [ident]
 * A string identifying the program name. If omitted, the name of the calling
 * process is used by default.
 *
 * @param {number|module:log.LogOption|module:log.LogOption[]} [options]
 * Logging options to use.
 *
 * See {@link module:log.LogOption|LogOption} for recognized option names.
 *
 * @param {number|module:log.LogFacility} [facility="user"]
 * The facility to use for log messages generated by subsequent syslog calls.
 *
 * See {@link module:log.LogFacility|LogFacility} for recognized facility names.
 *
 * @returns {boolean}
 *
 * @example
 * // Example usage of openlog function
 * openlog("myapp", LOG_PID | LOG_NDELAY, LOG_LOCAL0);
 *
 * // Using option names instead of bitmask and LOG_USER facility
 * openlog("myapp", [ "pid", "ndelay" ], "user");
 */
static uc_value_t *
uc_openlog(uc_vm_t *vm, size_t nargs)
{
	char *ident = parse_ident(vm, uc_fn_arg(0));
	int options = parse_options(uc_fn_arg(1));
	int facility = parse_facility(uc_fn_arg(2));

	if (options == -1 || facility == -1)
		return ucv_boolean_new(false);

	openlog(ident, options, facility);

	return ucv_boolean_new(true);
}

/**
 * Log a message to the system logger.
 *
 * This function logs a message to the system logger. The function behaves in a
 * sprintf-like manner, allowing the use of format strings and associated
 * arguments to construct log messages.
 *
 * If the `openlog` function has not been called explicitly before, `syslog()`
 * implicitly calls `openlog()`, using a default ident and `LOG_USER` facility
 * value before logging the message.
 *
 * If the `format` argument is not a string and not `null`, it will be
 * implicitly converted to a string and logged as-is, without further format
 * string processing.
 *
 * Returns `true` if a message was passed to the system `syslog()` function.
 *
 * Returns `false` if an invalid priority value or an empty message was given.
 *
 * @function module:log#syslog
 *
 * @param {number|module:log.LogPriority} priority
 * Log message priority. May be either a number value (potentially bitwise OR-ed
 * with a log facility constant) which is passed as-is to the system `syslog()`
 * function or a priority name string.
 *
 * See {@link module:log.LogPriority|LogPriority} for recognized priority names.
 *
 * @param {*} format
 * The sprintf-like format string for the log message, or any other, non-null,
 * non-string value type which will be implicitly stringified and logged as-is.
 *
 * @param {...*} [args]
 * In case a format string value was provided in the previous argument, then
 * all subsequent arguments are used to replace the placeholders in the format
 * string.
 *
 * @returns {boolean}
 *
 * @example
 * // Example usage of syslog function with format string and arguments
 * const username = "user123";
 * const errorCode = 404;
 * syslog(LOG_ERR, "User %s encountered error: %d", username, errorCode);
 *
 * // If openlog has not been called explicitly, it is implicitly called with defaults:
 * syslog(LOG_INFO, "This message will be logged with default settings.");
 *
 * // Selectively override used facility by OR-ing numeric constant
 * const password =" secret";
 * syslog(LOG_DEBUG|LOG_AUTHPRIV, "The password %s has been wrong", secret);
 *
 * // Using priority names for logging
 * syslog("emerg", "System shutdown imminent!");
 *
 * // Implicit stringification
 * syslog("debug", { foo: 1, bar: true, baz: [1, 2, 3] });
 */
static uc_value_t *
uc_syslog(uc_vm_t *vm, size_t nargs)
{
	int priority = parse_priority(uc_fn_arg(0));

	if (priority == -1 || nargs < 2)
		return ucv_boolean_new(false);

	uc_value_t *fmt = uc_fn_arg(1), *msg;
	uc_cfn_ptr_t fmtfn;
	char *s;

	switch (ucv_type(fmt)) {
	case UC_STRING:
		fmtfn = uc_stdlib_function("sprintf");
		msg = fmtfn(vm, nargs - 1);

		if (msg) {
			syslog(priority, "%s", ucv_string_get(msg));
			ucv_put(msg);

			return ucv_boolean_new(true);
		}

		break;

	case UC_NULL:
		break;

	default:
		s = ucv_to_string(vm, fmt);

		if (s) {
			syslog(priority, "%s", s);
			free(s);

			return ucv_boolean_new(true);
		}

		break;
	}

	return ucv_boolean_new(false);
}

/**
 * Close connection to system logger.
 *
 * The usage of this function is optional, and usually an explicit log
 * connection tear down is not required.
 *
 * @function module:log#closelog
 */
static uc_value_t *
uc_closelog(uc_vm_t *vm, size_t nargs)
{
	closelog();

	return NULL;
}


#ifdef HAVE_ULOG
/**
 * The following ulog channel strings are recognized:
 *
 * | Channel    | Description                                       |
 * |------------|---------------------------------------------------|
 * | `"kmsg"`   | Log to `/dev/kmsg`, log messages appear in dmesg. |
 * | `"syslog"` | Use standard `syslog()` mechanism.                |
 * | `"stdio"`  | Use stderr for log output.                        |
 *
 * @typedef {string} module:log.UlogChannel
 * @enum {module:log.UlogChannel}
 */
static const struct { const char *name; int value; } ulog_channels[] = {
	{ "kmsg", ULOG_KMSG },
	{ "syslog", ULOG_SYSLOG },
	{ "stdio", ULOG_STDIO },
};

static int
parse_channels(uc_value_t *channels)
{
	char *s;
	int rv;

	switch (ucv_type(channels)) {
	case UC_ARRAY:
		rv = 0;

		for (size_t i = 0; i < ucv_array_length(channels); i++) {
			uc_value_t *channel = ucv_array_get(channels, i);
			char *s = ucv_string_get(channel);

			for (size_t j = 0; j < ARRAY_SIZE(ulog_channels); j++) {
				if (s && !strcasecmp(s, ulog_channels[j].name))
					rv |= ulog_channels[j].value;
				else
					return -1;
			}
		}

		return rv;

	case UC_STRING:
		s = ucv_string_get(channels);

		for (size_t i = 0; i < ARRAY_SIZE(ulog_channels); i++)
			if (s && !strcasecmp(s, ulog_channels[i].name))
				return ulog_channels[i].value;

		return -1;

	case UC_INTEGER:
		rv = ucv_uint64_get(channels);

		if (errno == ERANGE)
			return -1;

		return rv & (ULOG_KMSG|ULOG_STDIO|ULOG_SYSLOG);

	case UC_NULL:
		return 0;

	default:
		return -1;
	}
}

/**
 * Configure ulog logger.
 *
 * This functions configures the ulog mechanism and is analogeous to using the
 * `openlog()` function in conjuncton with `syslog()`.
 *
 * The `ulog_open()` function is OpenWrt specific and may not be present on
 * other systems. Use `openlog()` and `syslog()` instead for portability to
 * non-OpenWrt environments.
 *
 * A program may use multiple channels to simultaneously output messages using
 * different means. The channel argument may either be a single string value
 * containing a channel name, an array of channel names or a numeric value
 * representing a bitmask of `ULOG_*` channel constants.
 *
 * The facility argument may be either a single string value containing a
 * facility name or one of the numeric `LOG_*` facility constants in the module
 * namespace.
 *
 * The default facility value varies, depending on the execution context of the
 * program. In OpenWrt's preinit boot phase, or when stdout is not connected to
 * an interactive terminal, the facility defaults to `"daemon"` (`LOG_DAEMON`),
 * otherwise to `"user"` (`LOG_USER`).
 *
 * Likewise, the default channel is selected depending on the context. During
 * OpenWrt's preinit boot phase, the `"kmsg"` channel is used, for interactive
 * terminals the `"stdio"` one and for all other cases the `"syslog"` channel
 * is selected.
 *
 * Returns `true` if ulog was configured.
 *
 * Returns `false` if an invalid argument, such as an unrecognized channel or
 * facility name, was provided.
 *
 * @function module:log#ulog_open
 *
 * @param {number|module:log.UlogChannel|module:log.UlogChannel[]} [channel]
 * Specifies the log channels to use.
 *
 * See {@link module:log.UlogChannel|UlogChannel} for recognized channel names.
 *
 * @param {number|module:log.LogFacility} [facility]
 * The facility to use for log messages generated by subsequent `ulog()` calls.
 *
 * See {@link module:log.LogFacility|LogFacility} for recognized facility names.
 *
 * @param {string} [ident]
 * A string identifying the program name. If omitted, the name of the calling
 * process is used by default.
 *
 * @returns {boolean}
 *
 * @example
 * // Log to dmesg and stderr
 * ulog_open(["stdio", "kmsg"], "daemon", "my-program");
 *
 * // Use numeric constants and use implicit default ident
 * ulog_open(ULOG_SYSLOG, LOG_LOCAL0);
 */
static uc_value_t *
uc_ulog_open(uc_vm_t *vm, size_t nargs)
{
	int channels = parse_channels(uc_fn_arg(0));
	int facility = parse_facility(uc_fn_arg(1));
	char *ident = parse_ident(vm, uc_fn_arg(2));

	if (channels == -1 || facility == -1)
		return ucv_boolean_new(false);

	ulog_open(channels, facility, ident);

	return ucv_boolean_new(true);
}

static uc_value_t *
uc_ulog_log_common(uc_vm_t *vm, size_t nargs, int priority)
{
	uc_value_t *fmt = uc_fn_arg(0), *msg;
	uc_cfn_ptr_t fmtfn;
	char *s;

	switch (ucv_type(fmt)) {
	case UC_STRING:
		fmtfn = uc_stdlib_function("sprintf");
		msg = fmtfn(vm, nargs);

		if (msg) {
			ulog(priority, "%s", ucv_string_get(msg));
			ucv_put(msg);

			return ucv_boolean_new(true);
		}

		break;

	case UC_NULL:
		break;

	default:
		s = ucv_to_string(vm, fmt);

		if (s) {
			ulog(priority, "%s", s);
			free(s);

			return ucv_boolean_new(true);
		}

		break;
	}

	return ucv_boolean_new(false);
}

/**
 * Log a message via the ulog mechanism.
 *
 * The `ulog()` function outputs the given log message to all configured ulog
 * channels unless the given priority level exceeds the globally configured ulog
 * priority threshold. See {@link module:log#ulog_threshold|ulog_threshold()}
 * for details.
 *
 * The `ulog()` function is OpenWrt specific and may not be present on other
 * systems. Use `syslog()` instead for portability to non-OpenWrt environments.
 *
 * Like `syslog()`, the function behaves in a sprintf-like manner, allowing the
 * use of format strings and associated arguments to construct log messages.
 *
 * If the `ulog_open()` function has not been called explicitly before, `ulog()`
 * implicitly configures certain defaults, see
 * {@link module:log#ulog_open|ulog_open()} for a detailled description.
 *
 * If the `format` argument is not a string and not `null`, it will be
 * implicitly converted to a string and logged as-is, without further format
 * string processing.
 *
 * Returns `true` if a message was passed to the underlying `ulog()` function.
 *
 * Returns `false` if an invalid priority value or an empty message was given.
 *
 * @function module:log#ulog
 *
 * @param {number|module:log.LogPriority} priority
 * Log message priority. May be either a number value or a priority name string.
 *
 * See {@link module:log.LogPriority|LogPriority} for recognized priority names.
 *
 * @param {*} format
 * The sprintf-like format string for the log message, or any other, non-null,
 * non-string value type which will be implicitly stringified and logged as-is.
 *
 * @param {...*} [args]
 * In case a format string value was provided in the previous argument, then
 * all subsequent arguments are used to replace the placeholders in the format
 * string.
 *
 * @returns {boolean}
 *
 * @example
 * // Example usage of ulog function with format string and arguments
 * const username = "user123";
 * const errorCode = 404;
 * ulog(LOG_ERR, "User %s encountered error: %d", username, errorCode);
 *
 * // Using priority names for logging
 * ulog("err", "General error encountered");
 *
 * // Implicit stringification
 * ulog("debug", { foo: 1, bar: true, baz: [1, 2, 3] });
 *
 * @see module:log#ulog_open
 * @see module:log#ulog_threshold
 * @see module:log#syslog
 */
static uc_value_t *
uc_ulog_log(uc_vm_t *vm, size_t nargs)
{
	int priority = parse_priority(uc_fn_arg(0));

	if (priority == -1 || nargs < 2)
		return ucv_boolean_new(false);

	return uc_ulog_log_common(vm, nargs - 1, priority);
}

/**
 * Close ulog logger.
 *
 * Resets the ulog channels, the default facility and the log ident value to
 * defaults.
 *
 * In case the `"syslog"` channel has been configured, the underlying
 * `closelog()` function will be invoked.
 *
 * The usage of this function is optional, and usually an explicit ulog teardown
 * is not required.
 *
 * The `ulog_close()` function is OpenWrt specific and may not be present on
 * other systems. Use `closelog()` in conjunction with `syslog()` instead for
 * portability to non-OpenWrt environments.
 *
 * @function module:log#ulog_close
 *
 * @see module:log#closelog
 */
static uc_value_t *
uc_ulog_close(uc_vm_t *vm, size_t nargs)
{
	ulog_close();

	return NULL;
}

/**
 * Set ulog priority threshold.
 *
 * This function configures the application wide log message threshold for log
 * messages emitted with `ulog()`. Any message with a priority higher (= less
 * severe) than the threshold priority will be discarded. This is useful to
 * implement application wide verbosity settings without having to wrap `ulog()`
 * invocations into a helper function or guarding code.
 *
 * When no explicit threshold has been set, `LOG_DEBUG` is used by default,
 * allowing log messages with all known priorities.
 *
 * The `ulog_threshold()` function is OpenWrt specific and may not be present on
 * other systems. There is no syslog equivalent to this ulog specific threshold
 * mechanism.
 *
 * The priority argument may be either a string value containing a priority name
 * or one of the numeric `LOG_*` priority constants in the module namespace.
 *
 * Returns `true` if a threshold was set.
 *
 * Returns `false` if an invalid priority value was given.
 *
 * @function module:log#ulog_threshold
 *
 * @param {number|module:log.LogPriority} [priority]
 * The priority threshold to configure.
 *
 * See {@link module:log.LogPriority|LogPriority} for recognized priority names.
 *
 * @returns {boolean}
 *
 * @example
 * // Set threshold to "warning" or more severe
 * ulog_threshold(LOG_WARNING);
 *
 * // This message will be supressed
 * ulog(LOG_DEBUG, "Testing thresholds");
 *
 * // Using priority name
 * ulog_threshold("debug");
 */
static uc_value_t *
uc_ulog_threshold(uc_vm_t *vm, size_t nargs)
{
	int priority = parse_priority(uc_fn_arg(0));

	if (priority == -1)
		return ucv_boolean_new(false);

	ulog_threshold(priority);

	return ucv_boolean_new(true);
}

/**
 * Invoke ulog with LOG_INFO.
 *
 * This function is convenience wrapper for `ulog(LOG_INFO, ...)`.
 *
 * See {@link module:log#ulog|ulog()} for details.
 *
 * @function module:log#INFO
 *
 * @param {*} format
 * The sprintf-like format string for the log message, or any other, non-null,
 * non-string value type which will be implicitly stringified and logged as-is.
 *
 * @param {...*} [args]
 * In case a format string value was provided in the previous argument, then
 * all subsequent arguments are used to replace the placeholders in the format
 * string.
 *
 * @returns {boolean}
 *
 * @example
 * INFO("This is an info log message");
 */
static uc_value_t *
uc_ulog_INFO(uc_vm_t *vm, size_t nargs)
{
	return uc_ulog_log_common(vm, nargs, LOG_INFO);
}

/**
 * Invoke ulog with LOG_NOTICE.
 *
 * This function is convenience wrapper for `ulog(LOG_NOTICE, ...)`.
 *
 * See {@link module:log#ulog|ulog()} for details.
 *
 * @function module:log#NOTE
 *
 * @param {*} format
 * The sprintf-like format string for the log message, or any other, non-null,
 * non-string value type which will be implicitly stringified and logged as-is.
 *
 * @param {...*} [args]
 * In case a format string value was provided in the previous argument, then
 * all subsequent arguments are used to replace the placeholders in the format
 * string.
 *
 * @returns {boolean}
 *
 * @example
 * NOTE("This is a notification log message");
 */
static uc_value_t *
uc_ulog_NOTE(uc_vm_t *vm, size_t nargs)
{
	return uc_ulog_log_common(vm, nargs, LOG_NOTICE);
}

/**
 * Invoke ulog with LOG_WARNING.
 *
 * This function is convenience wrapper for `ulog(LOG_WARNING, ...)`.
 *
 * See {@link module:log#ulog|ulog()} for details.
 *
 * @function module:log#WARN
 *
 * @param {*} format
 * The sprintf-like format string for the log message, or any other, non-null,
 * non-string value type which will be implicitly stringified and logged as-is.
 *
 * @param {...*} [args]
 * In case a format string value was provided in the previous argument, then
 * all subsequent arguments are used to replace the placeholders in the format
 * string.
 *
 * @returns {boolean}
 *
 * @example
 * WARN("This is a warning");
 */
static uc_value_t *
uc_ulog_WARN(uc_vm_t *vm, size_t nargs)
{
	return uc_ulog_log_common(vm, nargs, LOG_WARNING);
}

/**
 * Invoke ulog with LOG_ERR.
 *
 * This function is convenience wrapper for `ulog(LOG_ERR, ...)`.
 *
 * See {@link module:log#ulog|ulog()} for details.
 *
 * @function module:log#ERR
 *
 * @param {*} format
 * The sprintf-like format string for the log message, or any other, non-null,
 * non-string value type which will be implicitly stringified and logged as-is.
 *
 * @param {...*} [args]
 * In case a format string value was provided in the previous argument, then
 * all subsequent arguments are used to replace the placeholders in the format
 * string.
 *
 * @returns {boolean}
 *
 * @example
 * ERR("This is an error!");
 */
static uc_value_t *
uc_ulog_ERR(uc_vm_t *vm, size_t nargs)
{
	return uc_ulog_log_common(vm, nargs, LOG_ERR);
}
#endif


static const uc_function_list_t global_fns[] = {
	{ "openlog",		uc_openlog },
	{ "syslog",			uc_syslog },
	{ "closelog",		uc_closelog },

#ifdef HAVE_ULOG
	{ "ulog_open",		uc_ulog_open },
	{ "ulog",			uc_ulog_log },
	{ "ulog_close",		uc_ulog_close },
	{ "ulog_threshold",	uc_ulog_threshold },
	{ "INFO",			uc_ulog_INFO },
	{ "NOTE",			uc_ulog_NOTE },
	{ "WARN",			uc_ulog_WARN },
	{ "ERR",			uc_ulog_ERR },
#endif
};


void uc_module_init(uc_vm_t *vm, uc_value_t *scope)
{
	uc_function_list_register(scope, global_fns);

#define ADD_CONST(x) ucv_object_add(scope, #x, ucv_int64_new(x))

	ADD_CONST(LOG_PID);
	ADD_CONST(LOG_CONS);
	ADD_CONST(LOG_NDELAY);
	ADD_CONST(LOG_ODELAY);
	ADD_CONST(LOG_NOWAIT);

	ADD_CONST(LOG_AUTH);
#ifdef LOG_AUTHPRIV
	ADD_CONST(LOG_AUTHPRIV);
#endif
	ADD_CONST(LOG_CRON);
	ADD_CONST(LOG_DAEMON);
#ifdef LOG_FTP
	ADD_CONST(LOG_FTP);
#endif
	ADD_CONST(LOG_KERN);
	ADD_CONST(LOG_LPR);
	ADD_CONST(LOG_MAIL);
	ADD_CONST(LOG_NEWS);
	ADD_CONST(LOG_SYSLOG);
	ADD_CONST(LOG_USER);
	ADD_CONST(LOG_UUCP);
	ADD_CONST(LOG_LOCAL0);
	ADD_CONST(LOG_LOCAL1);
	ADD_CONST(LOG_LOCAL2);
	ADD_CONST(LOG_LOCAL3);
	ADD_CONST(LOG_LOCAL4);
	ADD_CONST(LOG_LOCAL5);
	ADD_CONST(LOG_LOCAL6);
	ADD_CONST(LOG_LOCAL7);

	ADD_CONST(LOG_EMERG);
	ADD_CONST(LOG_ALERT);
	ADD_CONST(LOG_CRIT);
	ADD_CONST(LOG_ERR);
	ADD_CONST(LOG_WARNING);
	ADD_CONST(LOG_NOTICE);
	ADD_CONST(LOG_INFO);
	ADD_CONST(LOG_DEBUG);

#ifdef HAVE_ULOG
	ADD_CONST(ULOG_KMSG);
	ADD_CONST(ULOG_SYSLOG);
	ADD_CONST(ULOG_STDIO);
#endif
}