impstats: add log.file.overwrite parameter for atomic overwrites

This change adds the capability to overwrite the statistics log file
instead of appending to it. This is particularly useful for
observability tools like Prometheus scraping sidecars or node exporter,
which expect a consistent and complete set of metrics in a single file.

The implementation ensures atomicity by writing the statistics to a
temporary file and then renaming it to the final destination. This
prevents reader processes from seeing partial or inconsistent data
during the emission process.

This commit includes:
- The implementation in impstats.c.
- New test cases in the testbench.
- User-facing documentation for the new parameter.

Impact: Users can now enable atomic overwrites using
log.file.overwrite="on". Default behavior remains append.

Refs: no issue
AI-Agent: Antigravity
This commit is contained in:
Rainer Gerhards 2025-12-19 18:51:31 +01:00
parent 853165bf29
commit 280fde6164
6 changed files with 173 additions and 6 deletions

View File

@ -103,6 +103,10 @@ Module Parameters
- .. include:: ../../reference/parameters/impstats-log-file.rst
:start-after: .. summary-start
:end-before: .. summary-end
* - :ref:`param-impstats-log-file-overwrite`
- .. include:: ../../reference/parameters/impstats-log-file-overwrite.rst
:start-after: .. summary-start
:end-before: .. summary-end
* - :ref:`param-impstats-ruleset`
- .. include:: ../../reference/parameters/impstats-ruleset.rst
:start-after: .. summary-start
@ -294,5 +298,6 @@ See Also
../../reference/parameters/impstats-format
../../reference/parameters/impstats-log-syslog
../../reference/parameters/impstats-log-file
../../reference/parameters/impstats-log-file-overwrite
../../reference/parameters/impstats-ruleset
../../reference/parameters/impstats-bracketing

View File

@ -0,0 +1,52 @@
.. _param-impstats-log-file-overwrite:
.. _impstats.parameter.module.log-file-overwrite:
log.file.overwrite
==================
.. index::
single: impstats; log.file.overwrite
single: log.file.overwrite
.. summary-start
If set to "on", the statistics log file specified by :ref:`param-impstats-log-file`
is overwritten with each emission instead of being appended to.
.. summary-end
This parameter applies to :doc:`../../configuration/modules/impstats`.
:Name: log.file.overwrite
:Scope: module
:Type: binary
:Default: off
:Required?: no
:Introduced: 8.2602.0
Description
-----------
When this parameter is set to ``on``, rsyslog will overwrite the file specified
in ``log.file`` every time it emits statistics. This is useful for external
monitoring tools (like Prometheus sidecars or node exporter) that expect to
read a single, consistent set of metrics from a file.
To ensure that reader processes always see a complete and consistent set of
statistics, rsyslog writes the data to a temporary file first and then
atomically renames it to the final destination.
Note that this parameter only has an effect if ``log.file`` is also specified.
Module usage
------------
.. _impstats.parameter.module.log-file-overwrite-usage:
.. code-block:: rsyslog
module(load="impstats"
logFile="/var/log/rsyslog-stats"
log.file.overwrite="on")
See also
--------
See also :doc:`../../configuration/modules/impstats`.

View File

@ -89,6 +89,7 @@ struct modConfData_s {
sbool bLogToSyslog;
sbool bResetCtrs;
sbool bBracketing;
sbool bLogOverwrite;
char *logfile;
sbool configSetViaV2Method;
uchar *pszBindRuleset; /* name of ruleset to bind to */
@ -102,9 +103,11 @@ static prop_t *pInputName = NULL;
/* module-global parameters */
static struct cnfparamdescr modpdescr[] = {
{"interval", eCmdHdlrInt, 0}, {"facility", eCmdHdlrInt, 0}, {"severity", eCmdHdlrInt, 0},
{"bracketing", eCmdHdlrBinary, 0}, {"log.syslog", eCmdHdlrBinary, 0}, {"resetcounters", eCmdHdlrBinary, 0},
{"log.file", eCmdHdlrGetWord, 0}, {"format", eCmdHdlrGetWord, 0}, {"ruleset", eCmdHdlrString, 0}};
{"interval", eCmdHdlrInt, 0}, {"facility", eCmdHdlrInt, 0},
{"severity", eCmdHdlrInt, 0}, {"bracketing", eCmdHdlrBinary, 0},
{"log.syslog", eCmdHdlrBinary, 0}, {"resetcounters", eCmdHdlrBinary, 0},
{"log.file", eCmdHdlrGetWord, 0}, {"format", eCmdHdlrGetWord, 0},
{"ruleset", eCmdHdlrString, 0}, {"log.file.overwrite", eCmdHdlrBinary, 0}};
static struct cnfparamblk modpblk = {CNFPARAMBLK_VERSION, sizeof(modpdescr) / sizeof(struct cnfparamdescr), modpdescr};
@ -204,6 +207,12 @@ static void doLogToFile(const char *ln, const size_t lenLn) {
if (lenLn == 0) goto done;
if (runModConf->logfd == -1) {
if (runModConf->bLogOverwrite) {
/* If overwriting, the file should have been opened by the main loop.
* If it's not open, we just skip logging to file.
*/
goto done;
}
runModConf->logfd = open(runModConf->logfile, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, S_IRUSR | S_IWUSR);
if (runModConf->logfd == -1) {
DBGPRINTF("impstats: error opening stats file %s\n", runModConf->logfile);
@ -308,6 +317,7 @@ BEGINbeginCnfLoad
loadModConf->bLogToSyslog = 1;
loadModConf->bBracketing = 0;
loadModConf->bResetCtrs = 0;
loadModConf->bLogOverwrite = 0;
bLegacyCnfModGlobalsPermitted = 1;
/* init legacy config vars */
initConfigSettings();
@ -344,6 +354,8 @@ BEGINsetModCnf
loadModConf->bResetCtrs = (sbool)pvals[i].val.d.n;
} else if (!strcmp(modpblk.descr[i].name, "log.file")) {
loadModConf->logfile = es_str2cstr(pvals[i].val.d.estr, NULL);
} else if (!strcmp(modpblk.descr[i].name, "log.file.overwrite")) {
loadModConf->bLogOverwrite = (sbool)pvals[i].val.d.n;
} else if (!strcmp(modpblk.descr[i].name, "format")) {
mode = es_str2cstr(pvals[i].val.d.estr, NULL);
if (!strcasecmp(mode, "json")) {
@ -441,9 +453,11 @@ BEGINdoHUP
DBGPRINTF("impstats: received HUP\n");
pthread_mutex_lock(&hup_mutex);
if (runModConf->logfd != -1) {
DBGPRINTF("impstats: closing log file due to HUP\n");
close(runModConf->logfd);
runModConf->logfd = -1;
if (!runModConf->bLogOverwrite) {
DBGPRINTF("impstats: closing log file due to HUP\n");
close(runModConf->logfd);
runModConf->logfd = -1;
}
}
pthread_mutex_unlock(&hup_mutex);
ENDdoHUP
@ -525,9 +539,41 @@ BEGINrunInput
while (glbl.GetGlobalInputTermState() == 0) {
srSleep(runModConf->iStatsInterval, 0); /* seconds, micro seconds */
DBGPRINTF("impstats: woke up, generating messages\n");
char *tmp_logfile = NULL;
if (runModConf->bLogOverwrite && runModConf->logfile != NULL) {
const size_t len_tmp_logfile = strlen(runModConf->logfile) + 5;
if ((tmp_logfile = malloc(len_tmp_logfile)) == NULL) {
LogError(errno, RS_RET_OUT_OF_MEMORY, "impstats: could not allocate memory for temp log file name");
} else {
snprintf(tmp_logfile, len_tmp_logfile, "%s.tmp", runModConf->logfile);
pthread_mutex_lock(&hup_mutex);
runModConf->logfd = open(tmp_logfile, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR);
if (runModConf->logfd == -1) {
LogError(errno, RS_RET_ERR, "impstats: error opening temp stats file %s", tmp_logfile);
free(tmp_logfile);
tmp_logfile = NULL;
}
pthread_mutex_unlock(&hup_mutex);
}
}
if (runModConf->bBracketing) submitLine("BEGIN", sizeof("BEGIN") - 1);
generateStatsMsgs();
if (runModConf->bBracketing) submitLine("END", sizeof("END") - 1);
if (tmp_logfile != NULL) {
pthread_mutex_lock(&hup_mutex);
close(runModConf->logfd);
runModConf->logfd = -1;
pthread_mutex_unlock(&hup_mutex);
if (rename(tmp_logfile, runModConf->logfile) != 0) {
LogError(errno, RS_RET_ERR, "impstats: error renaming temp stats file %s to %s", tmp_logfile,
runModConf->logfile);
unlink(tmp_logfile);
}
free(tmp_logfile);
}
}
ENDrunInput

View File

@ -1277,6 +1277,8 @@ endif # ENABLE_REDIS_TESTS
if ENABLE_IMPSTATS
TESTS += \
impstats-hup.sh \
impstats-overwrite.sh \
impstats-no-overwrite.sh \
perctile-simple.sh \
dynstats.sh \
dynstats_overflow.sh \
@ -3163,6 +3165,8 @@ EXTRA_DIST= \
dynstats_reset.sh \
dynstats_reset-vg.sh \
impstats-hup.sh \
impstats-overwrite.sh \
impstats-no-overwrite.sh \
dynstats.sh \
dynstats-vg.sh \
dynstats_prevent_premature_eviction.sh \

29
tests/impstats-no-overwrite.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
# test if impstats appends by default (no log.file.overwrite)
# This file is part of the rsyslog project, released under ASL 2.0
. ${srcdir:=.}/diag.sh init
generate_conf
add_conf '
module(load="../plugins/impstats/.libs/impstats"
log.file=`echo $RSYSLOG_OUT_LOG`
interval="1")
'
startup
# Wait for at least two emissions
./msleep 2500
shutdown_when_empty
wait_shutdown
# Check how many times "resource-usage" appears in the log file.
# It should appear at least twice.
NUM_EMISSIONS=$(grep -c "resource-usage" $RSYSLOG_OUT_LOG)
echo "Number of resource-usage entries found: $NUM_EMISSIONS"
if [ "$NUM_EMISSIONS" -lt 2 ]; then
echo "FAIL: expected at least 2 emissions in log file, but found $NUM_EMISSIONS"
exit 1
fi
echo "SUCCESS: impstats appended as expected"
exit_test

31
tests/impstats-overwrite.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# test if log.file.overwrite works for impstats
# This file is part of the rsyslog project, released under ASL 2.0
. ${srcdir:=.}/diag.sh init
generate_conf
add_conf '
module(load="../plugins/impstats/.libs/impstats"
log.file=`echo $RSYSLOG_OUT_LOG`
log.file.overwrite="on"
interval="1")
'
startup
# Wait for at least two emissions
./msleep 2500
shutdown_when_empty
wait_shutdown
# Check how many times "resource-usage" appears in the log file.
# It should appear exactly once if overwrite is working correctly.
NUM_EMISSIONS=$(grep -c "resource-usage" $RSYSLOG_OUT_LOG)
cat -n $RSYSLOG_OUT_LOG
echo "Number of resource-usage entries found: $NUM_EMISSIONS"
if [ "$NUM_EMISSIONS" -ne 1 ]; then
echo "FAIL: expected 1 emission in log file, but found $NUM_EMISSIONS"
exit 1
fi
echo "SUCCESS: log.file.overwrite worked as expected"
exit_test