Browse Source

Merge pull request #5668 from rspamd/vstakhov-lua-logger-imps

[Feature] Add type specifiers support to lua_logger
pull/5663/merge
Vsevolod Stakhov 1 day ago
committed by GitHub
parent
commit
901ef50bbb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 312
      src/lua/lua_logger.c
  2. 125
      test/lua/unit/logger.lua

312
src/lua/lua_logger.c

@ -27,7 +27,7 @@ local rspamd_logger = require "rspamd_logger"
local a = 'string'
local b = 1.5
local c = 1
local c = 100
local d = {
'aa',
1,
@ -39,17 +39,30 @@ local e = {
}
-- New extended interface
-- %<number> means numeric arguments and %s means the next argument
-- for example %1, %2, %s: %s would mean the third argument
-- Positional arguments: %<number> (e.g., %1, %2, %3)
-- Sequential arguments: %s (uses the next argument)
-- Type specifiers can be combined with positional or sequential:
-- %d - signed integer
-- %ud - unsigned integer
-- %f - double (floating point)
-- %.Nf - double with N decimal places (e.g., %.2f for 2 decimals)
-- Default formatting (automatic type detection)
rspamd_logger.info('a=%1, b=%2, c=%3, d=%4, e=%s', a, b, c, d, e)
-- Output: a=string, b=1.50000, c=1, d={[1] = aa, [2] = 1, [3] = bb} e={[key]=value, [key2]=1.0}
-- Output: a=string, b=1.500000, c=100, d={[1] = aa, [2] = 1, [3] = bb} e={[key]=value, [key2]=1.0}
-- Create string using logger API
local str = rspamd_logger.slog('a=%1, b=%2, c=%3, d=%4, e=%5', a, b, c, d, e)
-- Using type specifiers
rspamd_logger.info('count=%1d, price=%.2f, name=%3', c, b, a)
-- Output: count=100, price=1.50, name=string
-- Sequential formatting with types
rspamd_logger.info('int=%d, float=%.3f, str=%s', c, b, a)
-- Output: int=100, float=1.500, str=string
-- Create string using logger API
local str = rspamd_logger.slog('value=%1d, percent=%.1f%%', c, b)
print(str)
-- Output: a=string, b=1.50000, c=1, d={[1] = aa, [2] = 1, [3] = bb} e={[key]=value, [key2]=1.0}
-- Output: value=100, percent=1.5%
*/
/* Logger methods */
@ -86,36 +99,56 @@ LUA_FUNCTION_DEF(logger, debug);
/***
* @function logger.errx(fmt[, args)
* Extended interface to make an error log message
* @param {string} fmt format string, arguments are encoded as %<number>
* @param {any} args list of arguments to be replaced in %<number> positions
* @param {string} fmt format string supporting:
* - Positional arguments: %<number> (e.g., %1, %2, %3)
* - Sequential arguments: %s
* - Type specifiers: %d (int), %ud (unsigned), %f (float), %.Nf (float with precision)
* - Combined: %1d, %2f, %.2f
* @param {any} args list of arguments to be formatted
*/
LUA_FUNCTION_DEF(logger, errx);
/***
* @function logger.warn(fmt[, args)
* Extended interface to make a warning log message
* @param {string} fmt format string, arguments are encoded as %<number>
* @param {any} args list of arguments to be replaced in %<number> positions
* @param {string} fmt format string supporting:
* - Positional arguments: %<number> (e.g., %1, %2, %3)
* - Sequential arguments: %s
* - Type specifiers: %d (int), %ud (unsigned), %f (float), %.Nf (float with precision)
* - Combined: %1d, %2f, %.2f
* @param {any} args list of arguments to be formatted
*/
LUA_FUNCTION_DEF(logger, warnx);
/***
* @function logger.infox(fmt[, args)
* Extended interface to make an informational log message
* @param {string} fmt format string, arguments are encoded as %<number>
* @param {any} args list of arguments to be replaced in %<number> positions
* @param {string} fmt format string supporting:
* - Positional arguments: %<number> (e.g., %1, %2, %3)
* - Sequential arguments: %s
* - Type specifiers: %d (int), %ud (unsigned), %f (float), %.Nf (float with precision)
* - Combined: %1d, %2f, %.2f
* @param {any} args list of arguments to be formatted
*/
LUA_FUNCTION_DEF(logger, infox);
/***
* @function logger.infox(fmt[, args)
* Extended interface to make an informational log message
* @param {string} fmt format string, arguments are encoded as %<number>
* @param {any} args list of arguments to be replaced in %<number> positions
* @function logger.messagex(fmt[, args)
* Extended interface to make a notice log message
* @param {string} fmt format string supporting:
* - Positional arguments: %<number> (e.g., %1, %2, %3)
* - Sequential arguments: %s
* - Type specifiers: %d (int), %ud (unsigned), %f (float), %.Nf (float with precision)
* - Combined: %1d, %2f, %.2f
* @param {any} args list of arguments to be formatted
*/
LUA_FUNCTION_DEF(logger, messagex);
/***
* @function logger.debugx(fmt[, args)
* Extended interface to make a debug log message
* @param {string} fmt format string, arguments are encoded as %<number>
* @param {any} args list of arguments to be replaced in %<number> positions
* @param {string} fmt format string supporting:
* - Positional arguments: %<number> (e.g., %1, %2, %3)
* - Sequential arguments: %s
* - Type specifiers: %d (int), %ud (unsigned), %f (float), %.Nf (float with precision)
* - Combined: %1d, %2f, %.2f
* @param {any} args list of arguments to be formatted
*/
LUA_FUNCTION_DEF(logger, debugx);
@ -124,15 +157,19 @@ LUA_FUNCTION_DEF(logger, debugx);
* Extended interface to make a debug log message
* @param {string} module debug module
* @param {task|cfg|pool|string} id id to log
* @param {string} fmt format string, arguments are encoded as %<number>
* @param {any} args list of arguments to be replaced in %<number> positions
* @param {string} fmt format string supporting type specifiers (%d, %ud, %f, %.Nf)
* @param {any} args list of arguments to be formatted
*/
LUA_FUNCTION_DEF(logger, debugm);
/***
* @function logger.slog(fmt[, args)
* Create string replacing percent params with corresponding arguments
* @param {string} fmt format string, arguments are encoded as %<number>
* @param {any} args list of arguments to be replaced in %<number> positions
* @param {string} fmt format string supporting:
* - Positional arguments: %<number> (e.g., %1, %2, %3)
* - Sequential arguments: %s
* - Type specifiers: %d (int), %ud (unsigned), %f (float), %.Nf (float with precision)
* - Combined: %1d, %2f, %.2f
* @param {any} args list of arguments to be formatted
* @return {string} string with percent parameters substituted
*/
LUA_FUNCTION_DEF(logger, slog);
@ -142,8 +179,8 @@ LUA_FUNCTION_DEF(logger, slog);
* Extended interface to make a generic log message on any level
* @param {number} log level as a number (see GLogLevelFlags enum for values)
* @param {task|cfg|pool|string} id id to log
* @param {string} fmt format string, arguments are encoded as %<number>
* @param {any} args list of arguments to be replaced in %<number> positions
* @param {string} fmt format string supporting type specifiers (%d, %ud, %f, %.Nf)
* @param {any} args list of arguments to be formatted
*/
LUA_FUNCTION_DEF(logger, logx);
@ -280,6 +317,120 @@ lua_logger_char_safe(int t, unsigned int esc_type)
return true;
}
/* Format specifier types */
enum lua_logger_format_type {
LUA_FMT_STRING = 0, /* %s - default, any type */
LUA_FMT_INT, /* %d - signed integer */
LUA_FMT_UINT, /* %ud - unsigned integer */
LUA_FMT_DOUBLE, /* %f - double with optional precision */
};
/* Format a number as integer */
static gsize
lua_logger_out_int(lua_State *L, int pos, char *outbuf, gsize len, gboolean is_unsigned)
{
if (lua_type(L, pos) == LUA_TNUMBER) {
lua_Number num = lua_tonumber(L, pos);
if (is_unsigned) {
guint64 uval = (guint64) num;
return rspamd_snprintf(outbuf, len, "%uL", uval);
}
else {
gint64 ival = (gint64) num;
return rspamd_snprintf(outbuf, len, "%L", ival);
}
}
else if (lua_type(L, pos) == LUA_TSTRING) {
/* Try to convert string to number */
gsize slen;
const char *str = lua_tolstring(L, pos, &slen);
if (is_unsigned) {
guint64 uval;
if (rspamd_strtoul(str, slen, &uval)) {
return rspamd_snprintf(outbuf, len, "%uL", uval);
}
}
else {
gint64 ival;
if (rspamd_strtol(str, slen, &ival)) {
return rspamd_snprintf(outbuf, len, "%L", ival);
}
}
}
/* Fallback for non-numeric types */
return rspamd_snprintf(outbuf, len, is_unsigned ? "0" : "0");
}
/* Format a number as double with precision */
static gsize
lua_logger_out_double(lua_State *L, int pos, char *outbuf, gsize len, int precision)
{
gsize r;
char *p;
if (lua_type(L, pos) == LUA_TNUMBER) {
lua_Number num = lua_tonumber(L, pos);
if (precision >= 0) {
return rspamd_snprintf(outbuf, len, "%.*f", precision, (double) num);
}
else {
/* Default: smart formatting without trailing zeros */
r = rspamd_snprintf(outbuf, len, "%.6f", (double) num);
/* Remove trailing zeros, but keep at least one digit after decimal point */
if (r > 0 && outbuf[0] != '\0') {
p = outbuf + r - 1;
while (p > outbuf && *p == '0') {
p--;
}
/* Keep at least one digit after decimal point */
if (*p == '.') {
p++;
}
p++;
*p = '\0';
return p - outbuf;
}
return r;
}
}
else if (lua_type(L, pos) == LUA_TSTRING) {
/* Try to convert string to number */
gsize slen;
const char *str = lua_tolstring(L, pos, &slen);
char *endptr;
double dval = g_ascii_strtod(str, &endptr);
if (endptr != str && (*endptr == '\0' || endptr == str + slen)) {
if (precision >= 0) {
return rspamd_snprintf(outbuf, len, "%.*f", precision, dval);
}
else {
/* Default: smart formatting without trailing zeros */
r = rspamd_snprintf(outbuf, len, "%.6f", dval);
/* Remove trailing zeros, but keep at least one digit after decimal point */
if (r > 0 && outbuf[0] != '\0') {
p = outbuf + r - 1;
while (p > outbuf && *p == '0') {
p--;
}
/* Keep at least one digit after decimal point */
if (*p == '.') {
p++;
}
p++;
*p = '\0';
return p - outbuf;
}
return r;
}
}
}
/* Fallback for non-numeric types */
if (precision >= 0) {
return rspamd_snprintf(outbuf, len, "%.*f", precision, 0.0);
}
return rspamd_snprintf(outbuf, len, "0.0");
}
#define LUA_MAX_ARGS 32
/* Gracefully handles argument mismatches by substituting missing args and noting extra args */
static glong
@ -294,17 +445,73 @@ lua_logger_log_format_str(lua_State *L, int offset, char *logbuf, gsize remain,
unsigned int arg_num, cur_arg = 0, arg_max = lua_gettop(L) - offset;
gboolean args_used[LUA_MAX_ARGS];
unsigned int used_args_count = 0;
enum lua_logger_format_type fmt_type;
int precision;
memset(args_used, 0, sizeof(args_used));
while (remain > 1 && *fmt) {
if (*fmt == '%') {
++fmt;
/* Check for %% (escaped percent) */
if (*fmt == '%') {
*d++ = '%';
++fmt;
--remain;
continue;
}
c = fmt;
if (*fmt == 's') {
fmt_type = LUA_FMT_STRING;
precision = -1;
/* Check for precision specifier (%.Nf) */
if (*fmt == '.') {
++fmt;
precision = 0;
while ((digit = g_ascii_digit_value(*fmt)) >= 0) {
precision = precision * 10 + digit;
++fmt;
}
/* Expect 'f' after precision */
if (*fmt == 'f') {
fmt_type = LUA_FMT_DOUBLE;
++fmt;
++cur_arg;
}
else {
/* Invalid format, reset */
fmt = c;
}
}
/* Check for format type specifiers */
else if (*fmt == 's') {
fmt_type = LUA_FMT_STRING;
++fmt;
++cur_arg;
}
else {
else if (*fmt == 'd') {
fmt_type = LUA_FMT_INT;
++fmt;
++cur_arg;
}
else if (*fmt == 'u') {
++fmt;
if (*fmt == 'd') {
fmt_type = LUA_FMT_UINT;
++fmt;
++cur_arg;
}
else {
/* Just 'u' without 'd', treat as literal */
fmt = c;
}
}
else if (*fmt == 'f') {
fmt_type = LUA_FMT_DOUBLE;
++fmt;
++cur_arg;
}
/* Check for positional argument (%<number>) */
else if (g_ascii_isdigit(*fmt)) {
arg_num = 0;
while ((digit = g_ascii_digit_value(*fmt)) >= 0) {
++fmt;
@ -317,6 +524,27 @@ lua_logger_log_format_str(lua_State *L, int offset, char *logbuf, gsize remain,
}
if (fmt > c) {
/* Check for type specifier after number */
if (*fmt == 'd') {
fmt_type = LUA_FMT_INT;
++fmt;
}
else if (*fmt == 'u') {
++fmt;
if (*fmt == 'd') {
fmt_type = LUA_FMT_UINT;
++fmt;
}
else {
--fmt; /* Backtrack */
}
}
else if (*fmt == 'f') {
fmt_type = LUA_FMT_DOUBLE;
++fmt;
}
/* else: default to LUA_FMT_STRING */
/* Update the current argument */
cur_arg = arg_num;
}
@ -328,8 +556,22 @@ lua_logger_log_format_str(lua_State *L, int offset, char *logbuf, gsize remain,
r = rspamd_snprintf(d, remain, "<MISSING ARGUMENT>");
}
else {
/* Valid argument - output it */
r = lua_logger_out(L, offset + cur_arg, d, remain, esc_type);
/* Valid argument - output it based on format type */
switch (fmt_type) {
case LUA_FMT_INT:
r = lua_logger_out_int(L, offset + cur_arg, d, remain, FALSE);
break;
case LUA_FMT_UINT:
r = lua_logger_out_int(L, offset + cur_arg, d, remain, TRUE);
break;
case LUA_FMT_DOUBLE:
r = lua_logger_out_double(L, offset + cur_arg, d, remain, precision);
break;
case LUA_FMT_STRING:
default:
r = lua_logger_out(L, offset + cur_arg, d, remain, esc_type);
break;
}
/* Track which arguments are used */
if (cur_arg <= LUA_MAX_ARGS && !args_used[cur_arg - 1]) {
args_used[cur_arg - 1] = TRUE;
@ -343,8 +585,14 @@ lua_logger_log_format_str(lua_State *L, int offset, char *logbuf, gsize remain,
continue;
}
/* Copy % */
--fmt;
/* Copy % if we couldn't parse a format specifier */
if (fmt == c) {
--fmt;
}
else {
/* We parsed something but didn't match a valid format */
--fmt;
}
}
*d++ = *fmt++;

125
test/lua/unit/logger.lua

@ -99,4 +99,129 @@ context("Logger unit tests", function()
c[2], s))
end
end)
test("Logger type specifiers", function()
local log = require "rspamd_logger"
-- Test %d (signed integer)
local int_cases = {
{ '%d', '100', 100 },
{ '%d', '100', 100.5 }, -- Should truncate to integer
{ '%d', '-42', -42 },
{ '%d', '0', 0 },
{ '%1d', '100', 100 },
{ 'count=%d', 'count=100', 100 },
{ 'count=%1d', 'count=100', 100 },
{ '%d items', '100 items', 100 },
}
for _, c in ipairs(int_cases) do
local s = log.slog(c[1], c[3])
assert_equal(s, c[2], string.format("Int format test: '%s' doesn't match with '%s'",
c[2], s))
end
-- Test %ud (unsigned integer)
local uint_cases = {
{ '%ud', '100', 100 },
{ '%1ud', '100', 100 },
{ 'size=%ud bytes', 'size=1024 bytes', 1024 },
}
for _, c in ipairs(uint_cases) do
local s = log.slog(c[1], c[3])
assert_equal(s, c[2], string.format("Unsigned int format test: '%s' doesn't match with '%s'",
c[2], s))
end
-- Test %f (float) - smart formatting without trailing zeros
local float_cases = {
{ '%f', '1.5', 1.5 },
{ '%f', '100.0', 100 },
{ '%f', '-42.75', -42.75 },
{ '%1f', '1.5', 1.5 },
{ 'pi=%f', 'pi=3.14', 3.14 },
}
for _, c in ipairs(float_cases) do
local s = log.slog(c[1], c[3])
assert_equal(s, c[2], string.format("Float format test: '%s' doesn't match with '%s'",
c[2], s))
end
-- Test %.Nf (float with precision)
local precision_cases = {
{ '%.2f', '1.50', 1.5 },
{ '%.3f', '3.145', 3.145 },
{ '%.1f', '100.0', 100 },
{ '%.0f', '42', 42.0 },
{ 'price=%.2f', 'price=19.99', 19.99 },
}
for _, c in ipairs(precision_cases) do
local s = log.slog(c[1], c[3])
assert_equal(s, c[2], string.format("Precision format test: '%s' doesn't match with '%s'",
c[2], s))
end
-- Test mixed type specifiers
local mixed_type_cases = {
{ 'count=%1d, price=%.2f, name=%3', 'count=100, price=1.50, name=string', 100, 1.5, 'string' },
{ '%d %f %s', '42 3.14 test', 42, 3.14, 'test' },
{ 'int=%d, float=%.3f, str=%s', 'int=100, float=1.500, str=hello', 100, 1.5, 'hello' },
}
for _, c in ipairs(mixed_type_cases) do
local s = log.slog(c[1], c[3], c[4], c[5])
assert_equal(s, c[2], string.format("Mixed type format test: '%s' doesn't match with '%s'",
c[2], s))
end
-- Test type conversion from strings
local string_conversion_cases = {
{ '%d', '42', '42' }, -- String to int
{ '%f', '3.14', '3.14' }, -- String to float
{ '%.2f', '3.14', '3.14' }, -- String to float with precision
}
for _, c in ipairs(string_conversion_cases) do
local s = log.slog(c[1], c[3])
assert_equal(s, c[2], string.format("String conversion test: '%s' doesn't match with '%s'",
c[2], s))
end
-- Test fallback for non-numeric types
local fallback_cases = {
{ '%d', '0', nil }, -- nil should become 0
{ '%f', '0.0', nil }, -- nil should become 0.0
{ '%.2f', '0.00', nil }, -- nil with precision should become 0.00
}
for _, c in ipairs(fallback_cases) do
local s = log.slog(c[1], c[3])
assert_equal(s, c[2], string.format("Fallback test: '%s' doesn't match with '%s'",
c[2], s))
end
-- Test %% escaping
local escape_cases = {
{ '%%', '%' },
{ '100%%', '100%' },
{ 'price=%.2f%%', 'price=19.99%', 19.99 },
{ '%1 is %%d not %d', '100 is %d not 42', 100, 42 },
}
for _, c in ipairs(escape_cases) do
local s
if c[4] then
s = log.slog(c[1], c[3], c[4])
elseif c[3] then
s = log.slog(c[1], c[3])
else
s = log.slog(c[1])
end
assert_equal(s, c[2], string.format("Escape test: '%s' doesn't match with '%s'",
c[2], s))
end
end)
end)
Loading…
Cancel
Save