mirror of
https://github.com/rsyslog/rsyslog.git
synced 2026-04-23 14:58:14 +02:00
ratelimit: watch YAML policy files with debounce
Why YAML-backed ratelimit policies were only reloaded on HUP, which made policy tuning slower and harder to automate. Impact policyWatch and policyWatchDebounce now reload watched ratelimit policies automatically when inotify is available, and otherwise fall back cleanly to HUP-only behavior. Before/After Before: external ratelimit policy changes required HUP to take effect. After: watched policy files reload automatically after a debounce interval, while unsupported builds keep the previous behavior. Technical Overview Add policyWatch and policyWatchDebounce to ratelimit() config parsing and store the settings in shared ratelimit state. Introduce an inotify-backed watcher path in runtime/ratelimit.c that watches parent directories, coalesces rapid file events with a debounce window, and reloads both policy and per-source policy files by reusing existing HUP-era parse-and-swap logic. Keep unsupported builds working by compiling the watcher code behind inotify feature checks and downgrading watch requests to warning-only HUP semantics. Document the new options and add integration coverage for direct file updates, debounce behavior, rename-based replacement, and YAML frontend parity via tests guarded by inotify availability. Closes https://github.com/rsyslog/rsyslog/issues/6599 With the help of AI-Agents: Codex
This commit is contained in:
parent
30413f6115
commit
1cc464e8d0
@ -51,6 +51,38 @@ burst
|
||||
|
||||
The maximum number of messages allowed within the ``interval``.
|
||||
|
||||
.. _ratelimit_policywatch:
|
||||
|
||||
policyWatch
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. csv-table::
|
||||
:header: "type", "required", "default"
|
||||
:widths: 20, 10, 20
|
||||
|
||||
"boolean", "no", "off"
|
||||
|
||||
Enable automatic reload of configured external policy files when they change.
|
||||
When watch support is available, rsyslog monitors the configured ``policy`` and
|
||||
``perSourcePolicy`` files and reloads them after the debounce interval. When
|
||||
watch support is unavailable in the current build or runtime environment,
|
||||
rsyslog logs a warning and continues with HUP-only reload behavior.
|
||||
|
||||
.. _ratelimit_policywatchdebounce:
|
||||
|
||||
policyWatchDebounce
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. csv-table::
|
||||
:header: "type", "required", "default"
|
||||
:widths: 20, 10, 20
|
||||
|
||||
"time interval", "no", "5s"
|
||||
|
||||
Quiet period applied to ``policyWatch`` reloads. Each new file event resets the
|
||||
timer, so rapid updates are coalesced into one reload. Supported suffixes are
|
||||
``ms``, ``s``, ``m``, and ``h``. Bare numbers are interpreted as seconds.
|
||||
|
||||
.. _ratelimit_persource:
|
||||
|
||||
perSource
|
||||
@ -138,6 +170,12 @@ Example
|
||||
# Define a strict rate limit for public facing ports
|
||||
ratelimit(name="strict" interval="1" burst="50")
|
||||
|
||||
# Define a watched YAML policy for automatic reload
|
||||
ratelimit(name="watched"
|
||||
policy="/etc/rsyslog/ratelimit.yaml"
|
||||
policyWatch="on"
|
||||
policyWatchDebounce="500ms")
|
||||
|
||||
# Define per-source policy for TCP inputs
|
||||
ratelimit(name="per_source"
|
||||
perSource="on"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,8 @@ typedef struct ratelimit_shared_s {
|
||||
unsigned int burst;
|
||||
intTiny severity;
|
||||
char *policy_file;
|
||||
sbool policy_watch;
|
||||
unsigned int policy_watch_debounce_ms;
|
||||
pthread_mutex_t mut;
|
||||
sbool per_source_enabled;
|
||||
char *per_source_policy_file;
|
||||
@ -99,6 +101,8 @@ rsRetVal ratelimitAddConfig(rsconf_t *conf,
|
||||
unsigned int burst,
|
||||
intTiny severity,
|
||||
const char *policy_file,
|
||||
sbool policy_watch,
|
||||
const char *policy_watch_debounce,
|
||||
sbool per_source_enabled,
|
||||
const char *per_source_policy_file,
|
||||
const char *per_source_key_tpl_name,
|
||||
|
||||
@ -159,6 +159,8 @@ static struct cnfparamdescr ratelimitpdescr[] = {{"name", eCmdHdlrString, CNFPAR
|
||||
{"burst", eCmdHdlrInt, 0},
|
||||
{"severity", eCmdHdlrSeverity, 0},
|
||||
{"policy", eCmdHdlrString, 0},
|
||||
{"policyWatch", eCmdHdlrBinary, 0},
|
||||
{"policyWatchDebounce", eCmdHdlrString, 0},
|
||||
{"perSource", eCmdHdlrBinary, 0},
|
||||
{"perSourcePolicy", eCmdHdlrString, 0},
|
||||
{"perSourceKeyTpl", eCmdHdlrString, 0},
|
||||
@ -493,6 +495,8 @@ static rsRetVal initFunc_ratelimit(struct cnfobj *o) {
|
||||
int burst = 10000;
|
||||
int severity = -1; /* -1 means not set/all */
|
||||
uchar *policy = NULL;
|
||||
int policy_watch = 0;
|
||||
uchar *policy_watch_debounce = NULL;
|
||||
int per_source_enabled = 0;
|
||||
uchar *per_source_policy = NULL;
|
||||
uchar *per_source_key_tpl = NULL;
|
||||
@ -517,6 +521,10 @@ static rsRetVal initFunc_ratelimit(struct cnfobj *o) {
|
||||
severity = (int)pvals[i].val.d.n;
|
||||
} else if (!strcmp(ratelimitpblk.descr[i].name, "policy")) {
|
||||
policy = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
|
||||
} else if (!strcmp(ratelimitpblk.descr[i].name, "policyWatch")) {
|
||||
policy_watch = (int)pvals[i].val.d.n;
|
||||
} else if (!strcmp(ratelimitpblk.descr[i].name, "policyWatchDebounce")) {
|
||||
policy_watch_debounce = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
|
||||
} else if (!strcmp(ratelimitpblk.descr[i].name, "perSource")) {
|
||||
per_source_enabled = (int)pvals[i].val.d.n;
|
||||
} else if (!strcmp(ratelimitpblk.descr[i].name, "perSourcePolicy")) {
|
||||
@ -548,12 +556,14 @@ static rsRetVal initFunc_ratelimit(struct cnfobj *o) {
|
||||
}
|
||||
|
||||
CHKiRet(ratelimitAddConfig(loadConf, (char *)name, (unsigned)interval, (unsigned)burst, (intTiny)severity,
|
||||
(char *)policy, per_source_enabled, (char *)per_source_policy,
|
||||
(char *)per_source_key_tpl, (unsigned)per_source_max_states, (unsigned)per_source_topn));
|
||||
(char *)policy, policy_watch, (char *)policy_watch_debounce, per_source_enabled,
|
||||
(char *)per_source_policy, (char *)per_source_key_tpl, (unsigned)per_source_max_states,
|
||||
(unsigned)per_source_topn));
|
||||
|
||||
finalize_it:
|
||||
free(name);
|
||||
free(policy);
|
||||
free(policy_watch_debounce);
|
||||
free(per_source_policy);
|
||||
free(per_source_key_tpl);
|
||||
cnfparamvalsDestruct(pvals, &ratelimitpblk);
|
||||
|
||||
@ -584,6 +584,11 @@ TESTS_LIBYAML = \
|
||||
yaml-template-subtree.sh \
|
||||
yaml-statements-reload-lookup.sh
|
||||
|
||||
TESTS_RATELIMIT_WATCH = \
|
||||
ratelimit_policy_watch.sh \
|
||||
ratelimit_policy_watch_debounce.sh \
|
||||
yaml-ratelimit-policywatch.sh
|
||||
|
||||
TESTS_IMTCP = \
|
||||
fromhost-port.sh \
|
||||
fromhost-port-tuple.sh \
|
||||
@ -1733,6 +1738,7 @@ EXTRA_DIST += $(TESTS_OSSL_WRONG_OPT)
|
||||
EXTRA_DIST += $(TESTS_DEFAULT_VALGRIND)
|
||||
EXTRA_DIST += $(TESTS_LIBGCRYPT_VALGRIND)
|
||||
EXTRA_DIST += $(TESTS_LIBYAML)
|
||||
EXTRA_DIST += $(TESTS_RATELIMIT_WATCH)
|
||||
EXTRA_DIST += $(TESTS_LIBYAML_IMTCP)
|
||||
EXTRA_DIST += $(TESTS_LIBYAML_OMSTDOUT)
|
||||
EXTRA_DIST += $(TESTS_IMTCP)
|
||||
@ -2096,6 +2102,9 @@ endif # ENABLE_LIBGCRYPT
|
||||
endif # HAVE_VALGRIND
|
||||
if HAVE_LIBYAML
|
||||
TESTS += $(TESTS_LIBYAML)
|
||||
if ENABLE_INOTIFY
|
||||
TESTS += $(TESTS_RATELIMIT_WATCH)
|
||||
endif # ENABLE_INOTIFY
|
||||
if ENABLE_IMTCP_TESTS
|
||||
TESTS += $(TESTS_LIBYAML_IMTCP)
|
||||
endif # ENABLE_IMTCP_TESTS
|
||||
|
||||
69
tests/ratelimit_policy_watch.sh
Executable file
69
tests/ratelimit_policy_watch.sh
Executable file
@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# Test inotify-based reload of external rate limit policies, including
|
||||
# rename-based atomic replacement.
|
||||
|
||||
. ${srcdir:=.}/diag.sh init
|
||||
. $srcdir/diag.sh check-inotify
|
||||
|
||||
export PORT_RCVR="$(get_free_port)"
|
||||
export POLICY_FILE="$(pwd)/${RSYSLOG_DYNNAME}.policy.yaml"
|
||||
export POLICY_TMP="$(pwd)/${RSYSLOG_DYNNAME}.policy.tmp.yaml"
|
||||
export SENDMESSAGES=20
|
||||
|
||||
cat > "$POLICY_FILE" <<'YAML'
|
||||
interval: 1
|
||||
burst: 1000
|
||||
severity: 0
|
||||
YAML
|
||||
|
||||
generate_conf
|
||||
add_conf '
|
||||
global(processInternalMessages="on")
|
||||
ratelimit(name="watch_limiter" policy="'$POLICY_FILE'" policyWatch="on" policyWatchDebounce="200ms")
|
||||
module(load="../plugins/imudp/.libs/imudp" batchSize="1")
|
||||
input(type="imudp" port="'$PORT_RCVR'" ratelimit.name="watch_limiter" ruleset="main")
|
||||
|
||||
template(name="outfmt" type="string" string="RECEIVED RAW: %rawmsg%\n")
|
||||
|
||||
ruleset(name="main") {
|
||||
action(type="omfile" file="'$RSYSLOG_OUT_LOG'" template="outfmt")
|
||||
}
|
||||
'
|
||||
startup
|
||||
|
||||
./tcpflood -Tudp -p$PORT_RCVR -m $SENDMESSAGES -M "msgnum:"
|
||||
wait_file_lines "$RSYSLOG_OUT_LOG" 20 100
|
||||
|
||||
: > "$RSYSLOG_OUT_LOG"
|
||||
|
||||
cat > "$POLICY_FILE" <<'YAML'
|
||||
interval: 10
|
||||
burst: 0
|
||||
severity: 0
|
||||
YAML
|
||||
./msleep 1500
|
||||
|
||||
./tcpflood -Tudp -p$PORT_RCVR -m $SENDMESSAGES -M "msgnum:"
|
||||
./msleep 1000
|
||||
wait_queueempty
|
||||
|
||||
content_count=$(grep -c "msgnum:" "$RSYSLOG_OUT_LOG" || true)
|
||||
if [ "$content_count" -ne 0 ]; then
|
||||
echo "FAIL: watch reload expected 0 messages after restrictive update, got $content_count"
|
||||
error_exit 1
|
||||
fi
|
||||
|
||||
cat > "$POLICY_TMP" <<'YAML'
|
||||
interval: 1
|
||||
burst: 1000
|
||||
severity: 0
|
||||
YAML
|
||||
mv -f "$POLICY_TMP" "$POLICY_FILE"
|
||||
./msleep 1500
|
||||
|
||||
./tcpflood -Tudp -p$PORT_RCVR -m $SENDMESSAGES -M "msgnum:"
|
||||
wait_file_lines "$RSYSLOG_OUT_LOG" 20 100
|
||||
|
||||
shutdown_when_empty
|
||||
wait_shutdown
|
||||
exit_test
|
||||
68
tests/ratelimit_policy_watch_debounce.sh
Executable file
68
tests/ratelimit_policy_watch_debounce.sh
Executable file
@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# Test debounce handling for inotify-based ratelimit policy reloads.
|
||||
|
||||
. ${srcdir:=.}/diag.sh init
|
||||
. $srcdir/diag.sh check-inotify
|
||||
|
||||
export PORT_RCVR="$(get_free_port)"
|
||||
export POLICY_FILE="$(pwd)/${RSYSLOG_DYNNAME}.policy.yaml"
|
||||
export SENDMESSAGES=20
|
||||
|
||||
cat > "$POLICY_FILE" <<'YAML'
|
||||
interval: 1
|
||||
burst: 1000
|
||||
severity: 0
|
||||
YAML
|
||||
|
||||
generate_conf
|
||||
add_conf '
|
||||
global(processInternalMessages="on")
|
||||
ratelimit(name="debounce_limiter" policy="'$POLICY_FILE'" policyWatch="on" policyWatchDebounce="500ms")
|
||||
module(load="../plugins/imudp/.libs/imudp" batchSize="1")
|
||||
input(type="imudp" port="'$PORT_RCVR'" ratelimit.name="debounce_limiter" ruleset="main")
|
||||
|
||||
template(name="outfmt" type="string" string="RECEIVED RAW: %rawmsg%\n")
|
||||
|
||||
ruleset(name="main") {
|
||||
action(type="omfile" file="'$RSYSLOG_OUT_LOG'" template="outfmt")
|
||||
}
|
||||
'
|
||||
startup
|
||||
|
||||
cat > "$POLICY_FILE" <<'YAML'
|
||||
interval: 1
|
||||
burst: 200
|
||||
severity: 0
|
||||
YAML
|
||||
./msleep 100
|
||||
cat > "$POLICY_FILE" <<'YAML'
|
||||
interval: 1
|
||||
burst: 100
|
||||
severity: 0
|
||||
YAML
|
||||
./msleep 100
|
||||
cat > "$POLICY_FILE" <<'YAML'
|
||||
interval: 10
|
||||
burst: 0
|
||||
severity: 0
|
||||
YAML
|
||||
|
||||
./msleep 1500
|
||||
|
||||
./tcpflood -Tudp -p$PORT_RCVR -m $SENDMESSAGES -M "msgnum:"
|
||||
./msleep 1000
|
||||
wait_queueempty
|
||||
|
||||
if [ -f "$RSYSLOG_OUT_LOG" ]; then
|
||||
content_count=$(grep -c "msgnum:" "$RSYSLOG_OUT_LOG" || true)
|
||||
else
|
||||
content_count=0
|
||||
fi
|
||||
if [ "$content_count" -ne 0 ]; then
|
||||
echo "FAIL: final debounced policy expected 0 messages, got $content_count"
|
||||
error_exit 1
|
||||
fi
|
||||
|
||||
shutdown_when_empty
|
||||
wait_shutdown
|
||||
exit_test
|
||||
73
tests/yaml-ratelimit-policywatch.sh
Executable file
73
tests/yaml-ratelimit-policywatch.sh
Executable file
@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Test YAML ratelimit object policyWatch/policyWatchDebounce parsing and reload.
|
||||
|
||||
. ${srcdir:=.}/diag.sh init
|
||||
. $srcdir/diag.sh check-inotify
|
||||
|
||||
export PORT_RCVR="$(get_free_port)"
|
||||
export POLICY_FILE="$(pwd)/${RSYSLOG_DYNNAME}.policy.yaml"
|
||||
export SENDMESSAGES=20
|
||||
|
||||
cat > "$POLICY_FILE" <<'YAML'
|
||||
interval: 1
|
||||
burst: 1000
|
||||
severity: 0
|
||||
YAML
|
||||
|
||||
generate_conf
|
||||
add_conf '
|
||||
include(file="'${RSYSLOG_DYNNAME}'.yaml")
|
||||
module(load="../plugins/imudp/.libs/imudp" batchSize="1")
|
||||
input(type="imudp" port="'$PORT_RCVR'" ratelimit.name="yaml_watch" ruleset="main")
|
||||
'
|
||||
|
||||
cat > "${RSYSLOG_DYNNAME}.yaml" <<'YAMLEOF'
|
||||
ratelimits:
|
||||
- name: yaml_watch
|
||||
policy: "${POLICY_FILE}"
|
||||
policyWatch: on
|
||||
policyWatchDebounce: 200ms
|
||||
|
||||
templates:
|
||||
- name: outfmt
|
||||
type: string
|
||||
string: "RECEIVED RAW: %rawmsg%\n"
|
||||
|
||||
rulesets:
|
||||
- name: main
|
||||
script: |
|
||||
action(type="omfile" file="${RSYSLOG_OUT_LOG}" template="outfmt")
|
||||
YAMLEOF
|
||||
|
||||
sed -i \
|
||||
-e "s|\${POLICY_FILE}|${POLICY_FILE}|g" \
|
||||
-e "s|\${RSYSLOG_OUT_LOG}|${RSYSLOG_OUT_LOG}|g" \
|
||||
"${RSYSLOG_DYNNAME}.yaml"
|
||||
|
||||
startup
|
||||
|
||||
./tcpflood -Tudp -p$PORT_RCVR -m $SENDMESSAGES -M "msgnum:"
|
||||
wait_file_lines "$RSYSLOG_OUT_LOG" 20 100
|
||||
|
||||
: > "$RSYSLOG_OUT_LOG"
|
||||
|
||||
cat > "$POLICY_FILE" <<'YAML'
|
||||
interval: 10
|
||||
burst: 0
|
||||
severity: 0
|
||||
YAML
|
||||
./msleep 1500
|
||||
|
||||
./tcpflood -Tudp -p$PORT_RCVR -m $SENDMESSAGES -M "msgnum:"
|
||||
./msleep 1000
|
||||
wait_queueempty
|
||||
|
||||
content_count=$(grep -c "msgnum:" "$RSYSLOG_OUT_LOG" || true)
|
||||
if [ "$content_count" -ne 0 ]; then
|
||||
echo "FAIL: YAML ratelimit policyWatch expected 0 messages after update, got $content_count"
|
||||
error_exit 1
|
||||
fi
|
||||
|
||||
shutdown_when_empty
|
||||
wait_shutdown
|
||||
exit_test
|
||||
Loading…
x
Reference in New Issue
Block a user