MINOR: vars: implement dump_all_vars() sample fetch

This patch implements dump_all_vars([scope],[prefix]) sample fetch
function that dumps all variables in a given scope, optionally
filtered by name prefix.

Output format: var1=value1, var2=value2, ...
- String values are quoted and escaped (", , \r, \n, \b, \0)
- All sample types are supported via sample_convert()
- Scope can be: sess, txn, req, res, proc
- Prefix filtering is optional

Example usage:
  http-request return string %[dump_all_vars(txn)]
  http-request return string %[dump_all_vars(txn,user)]

This addresses GitHub issue #1623.
This commit is contained in:
Hyeonggeun Oh 2026-01-13 03:07:16 +09:00 committed by Willy Tarreau
parent 079bc39645
commit bd1807aa3a

View File

@ -58,6 +58,16 @@ static struct var_set_condition conditions_array[] = {
{ NULL, 0 }
};
/* Variable scope names with their prefixes for output */
static const char *var_scope_names[] = {
[SCOPE_SESS] = "sess.",
[SCOPE_TXN] = "txn.",
[SCOPE_REQ] = "req.",
[SCOPE_RES] = "res.",
[SCOPE_PROC] = "proc.",
[SCOPE_CHECK] = "check.",
};
/* returns the struct vars pointer for a session, stream and scope, or NULL if
* it does not exist.
*/
@ -352,6 +362,171 @@ static int smp_fetch_var(const struct arg *args, struct sample *smp, const char
return vars_get_by_desc(var_desc, smp, def);
}
/* Dumps all variables in the specified scope, optionally filtered by prefix.
* Output format: var1=value1, var2=value2, ...
* String values are quoted and escaped, binary values are hex-encoded (x...).
* Returns 1 on success, 0 on failure (buffer too small).
* Note: When using prefix filtering, all variables are still visited, so this
* should not be used with configs involving thousands of variables.
*/
static int smp_fetch_dump_all_vars(const struct arg *args, struct sample *smp, const char *kw, void *private)
{
struct buffer *output;
struct vars *vars;
struct var *var;
struct var_desc desc;
const char *prefix = NULL;
size_t prefix_len = 0;
const char *delim = ", ";
size_t delim_len = 2;
int first = 1;
int i;
/* Get output buffer */
output = get_trash_chunk();
chunk_reset(output);
/* Parse arguments */
if (args[0].type == ARGT_SINT) {
desc.scope = args[0].data.sint;
} else {
/* Auto-detect scope from context*/
if (smp->strm)
desc.scope = SCOPE_TXN;
else if (smp->sess)
desc.scope = SCOPE_SESS;
else
desc.scope = SCOPE_PROC;
}
/* Optional prefix filter */
if (args[1].type == ARGT_STR) {
prefix = args[1].data.str.area;
prefix_len = args[1].data.str.data;
}
/* Optional delimiter */
if (args[2].type == ARGT_STR) {
delim = args[2].data.str.area;
delim_len = args[2].data.str.data;
}
desc.flags = 0;
desc.name_hash = 0;
vars = get_vars(smp->sess, smp->strm, &desc);
if (!vars || vars->scope != desc.scope)
return 0;
vars_rdlock(vars);
/* Iterate through all variable roots */
for (i = 0; i < VAR_NAME_ROOTS; i++) {
var = cebu64_item_first(&vars->name_root[i], name_node, name_hash, struct var);
while (var) {
const char *scope_prefix;
/* Check prefix filter */
if (prefix) {
if (!var->name || strncmp(var->name, prefix, prefix_len) != 0) {
var = cebu64_item_next(&vars->name_root[i], name_node, name_hash, var);
continue;
}
}
/* Add delimiter */
if (!first) {
if (output->data + delim_len >= output->size)
goto fail_unlock;
chunk_memcat(output, delim, delim_len);
}
first = 0;
/* Add variable name with scope prefix */
scope_prefix = var_scope_names[desc.scope];
if (var->name) {
if (chunk_appendf(output, "%s%s=", scope_prefix, var->name) < 0)
goto fail_unlock;
} else {
if (chunk_appendf(output, "var_%016llx=", (unsigned long long)var->name_hash) < 0)
goto fail_unlock;
}
/* Convert value based on type */
if (var->data.type == SMP_T_STR) {
/* String: quote and escape */
if (chunk_escape_string(output, var->data.u.str.area, var->data.u.str.data) < 0)
goto fail_unlock;
} else if (var-> data.type == SMP_T_BIN) {
/* Binary: hex encode */
if (dump_binary(output, var->data.u.str.area, var->data.u.str.data) != var->data.u.str.data)
goto fail_unlock;
} else if (var->data.type == SMP_T_SINT) {
/* Integer */
if (chunk_appendf(output, "%lld", (long long)var->data.u.sint) < 0)
goto fail_unlock;
} else if (var->data.type == SMP_T_BOOL) {
/* Boolean */
const char *bool_str = var->data.u.sint ? "true" : "false";
if (chunk_appendf(output, "%s", bool_str) < 0)
goto fail_unlock;
} else if (var->data.type == SMP_T_IPV4 || var->data.type == SMP_T_IPV6) {
/* Address */
char addr_str[INET6_ADDRSTRLEN];
const char *res;
if (var->data.type == SMP_T_IPV4)
res = inet_ntop(AF_INET, &var->data.u.ipv4, addr_str, sizeof(addr_str));
else
res = inet_ntop(AF_INET6, &var->data.u.ipv6, addr_str, sizeof(addr_str));
if (!res) {
if (chunk_appendf(output, "(addr)") < 0)
goto fail_unlock;
} else {
if (chunk_appendf(output, "%s", addr_str) < 0)
goto fail_unlock;
}
} else if (var->data.type == SMP_T_METH) {
/* HTTP Method */
if (var->data.u.meth.meth == HTTP_METH_OTHER) {
if (chunk_escape_string(output, var->data.u.meth.str.area, var->data.u.meth.str.data) < 0)
goto fail_unlock;
} else {
const char *method_str = http_known_methods[var->data.u.meth.meth].ptr;
if (chunk_appendf(output, "\"%s\"", method_str) < 0)
goto fail_unlock;
}
} else {
/* Other types: show type number */
if (chunk_appendf(output, "(type:%d)", var->data.type) < 0)
goto fail_unlock;
}
var = cebu64_item_next(&vars->name_root[i], name_node, name_hash, var);
}
}
vars_rdunlock(vars);
/* Set output sample */
smp->data.type = SMP_T_STR;
smp->data.u.str.area = output->area;
smp->data.u.str.data = output->data;
smp->flags &= ~SMP_F_CONST;
return 1;
fail_unlock:
vars_rdunlock(vars);
output->data = 0;
return 0;
}
/*
* Clear the contents of a variable so that it can be reset directly.
* This function is used just before a variable is filled out of a sample's
@ -916,6 +1091,60 @@ static int smp_check_var(struct arg *args, char **err)
return vars_check_arg(&args[0], err);
}
/* This function checks all arguments for dump_all_vars()
* Args: [scope], [prefix], [delimiter]
* Both arguments are optional
*/
static int smp_check_dump_all_vars(struct arg *args, char **err)
{
/* First argument (scope) is optional */
if (args[0].type == ARGT_STR) {
const char *scope = args[0].data.str.area;
int scope_id = -1;
int i;
char buf[16];
if (args[0].data.str.data < sizeof(buf) - 1) {
snprintf(buf, sizeof(buf), "%s.", scope);
for (i = 0; i <= SCOPE_CHECK; i++) {
if (strcmp(buf, var_scope_names[i]) == 0) {
scope_id = i;
break;
}
}
}
if (scope_id == -1) {
memprintf(err, "invalid scope '%s', must be one of: sess, txn, req, res, proc", scope);
return 0;
}
chunk_destroy(&args[0].data.str);
args[0].type = ARGT_SINT;
args[0].data.sint = scope_id;
}
else if (args[0].type != ARGT_STOP) {
memprintf(err, "first argument must be a string (scope) or omitted");
return 0;
}
/* Second argument (prefix) is optional */
if (args[1].type != ARGT_STR && args[1].type != ARGT_STOP) {
memprintf(err, "second argument must be a string (prefix) or omitted");
return 0;
}
/* Third argument (delimiter) is optional */
if (args[2].type != ARGT_STR && args[2].type != ARGT_STOP) {
memprintf(err, "third argument must be a string (delimiter) or omitted");
return 0;
}
return 1;
}
static int conv_check_var(struct arg *args, struct sample_conv *conv,
const char *file, int line, char **err_msg)
{
@ -1410,6 +1639,7 @@ INITCALL0(STG_PREPARE, vars_init);
static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, {
{ "dump_all_vars", smp_fetch_dump_all_vars, ARG3(0,STR,STR,STR), smp_check_dump_all_vars, SMP_T_STR, SMP_USE_CONST },
{ "var", smp_fetch_var, ARG2(1,STR,STR), smp_check_var, SMP_T_ANY, SMP_USE_CONST },
{ /* END */ },
}};