From da428b66cdef8be5094f9d7f0c8b075f75c4328c Mon Sep 17 00:00:00 2001
From: Alexander Stephan <alexander.stephan@sap.com>
Date: Mon, 6 Jul 2026 09:20:48 +0000
Subject: [PATCH 3/5] MINOR: server: add 'clear counters server
 <backend>/<server>' CLI command

Add a CLI command to reset the statistics counters of a single server:

  clear counters server <backend>/<server>

Motivation: 'clear counters all' is ADMIN-scoped and resets counters
across every proxy and server in the process, which is too blunt for
common operational needs. In particular, when a server slot is being
reused to represent a different logical entity (e.g. a different
Kubernetes pod occupying the same slot after 'set server <b>/<s> name'),
the operator needs per-slot counter attribution and cannot afford to
wipe the entire process' stats.

The command requires ACCESS_LVL_OPER, matching the base 'clear
counters'. Like 'clear counters' / 'clear counters all', it is not
gated by the server's administrative state: the reset only zeroes
counter values and does not touch the server object or its runtime
state, so it is safe to issue on a live server (a concurrent counter
increment races on a value exactly as it already does for 'clear
counters all').

The reset covers both the native server counters, via counters_be_reset()
introduced earlier in this series (which safely zeroes the per-tgroup
accumulated counter contents while preserving the shared.tg pointer
array), and the module-registered extra counters, via the new
srv_stats_clear_extra_counters() helper. The latter only resets
clearable modules, matching the OPER-level 'clear counters' semantics;
non-clearable modules are reset only by the ADMIN-only 'clear counters
all', which has no per-server equivalent.

Note on the dispatch: 'clear counters' is a two-word CLI keyword, and
cli_find_kw() matches the first keyword whose tokens are all consumed,
so a separate three-word 'clear counters server' keyword would be
shadowed (or would shadow) depending on registration order. To avoid
that ambiguity the new sub-command is dispatched from within
cli_parse_clear_counters() when args[2] == "server", mirroring how the
existing "all" argument is handled. The per-server worker lives in
server.c (cli_clear_counters_server()) where the server lookup and
lock helpers are available.
---
 include/haproxy/server.h      |  1 +
 include/haproxy/stats-proxy.h |  2 ++
 src/server.c                  | 38 +++++++++++++++++++++++++++++++++++
 src/stats-proxy.c             | 29 ++++++++++++++++++++++++++
 src/stats.c                   | 22 +++++++++++++++-----
 5 files changed, 87 insertions(+), 5 deletions(-)

diff --git a/include/haproxy/server.h b/include/haproxy/server.h
index 44f82137e..5da16a085 100644
--- a/include/haproxy/server.h
+++ b/include/haproxy/server.h
@@ -77,6 +77,7 @@ int srv_set_addr_via_libc(struct server *srv, int *err_code);
 int srv_postinit(struct server *srv);
 int srv_init_addr(void);
 struct server *cli_find_server(struct appctx *appctx, char *arg);
+int cli_clear_counters_server(struct appctx *appctx, char *arg);
 struct server *new_server(struct proxy *proxy);
 void srv_take(struct server *srv);
 struct server *srv_drop(struct server *srv);
diff --git a/include/haproxy/stats-proxy.h b/include/haproxy/stats-proxy.h
index 81a60f016..e85af3b76 100644
--- a/include/haproxy/stats-proxy.h
+++ b/include/haproxy/stats-proxy.h
@@ -6,9 +6,11 @@
 struct buffer;
 struct htx;
 struct stconn;
+struct server;
 
 int stats_dump_proxies(struct stconn *sc, struct buffer *buf, struct htx *htx);
 
 void proxy_stats_clear_counters(int clrall, struct list *stat_modules);
+void srv_stats_clear_extra_counters(struct server *sv);
 
 #endif /* _HAPROXY_STATS_PROXY_H */
diff --git a/src/server.c b/src/server.c
index 15dd282ac..d76009031 100644
--- a/src/server.c
+++ b/src/server.c
@@ -47,6 +47,7 @@
 #include <haproxy/sample.h>
 #include <haproxy/sc_strm.h>
 #include <haproxy/server.h>
+#include <haproxy/stats-proxy.h>
 #include <haproxy/stats.h>
 #include <haproxy/ssl_sock.h>
 #include <haproxy/stconn.h>
@@ -6804,6 +6805,43 @@ static int cli_parse_delete_server(char **args, char *payload, struct appctx *ap
 	return 1;
 }
 
+/* Reset the statistics counters of a single server, invoked from the
+ * "clear counters server <backend>/<server>" CLI command (dispatched by
+ * cli_parse_clear_counters() in stats.c, since "clear counters" is a
+ * two-word keyword that would otherwise shadow a three-word variant).
+ *
+ * The command is not gated by the server's administrative state: like
+ * "clear counters" / "clear counters all", it only zeroes counter values
+ * and does not touch the server object or its runtime state, so it is safe
+ * to issue on a live server (a concurrent counter increment races on a
+ * value exactly as it already does for "clear counters all"). This is
+ * useful when a server slot is being reused to represent a different
+ * logical entity (e.g. a different Kubernetes pod occupying the same slot
+ * after a rename) and per-entity counter attribution is required.
+ *
+ * <arg> is the "<backend>/<server>" argument. Always returns 1 (the CLI
+ * parser convention for "message emitted, stop"); success or error is
+ * reported to <appctx>.
+ */
+int cli_clear_counters_server(struct appctx *appctx, char *arg)
+{
+	struct server *sv;
+
+	sv = cli_find_server(appctx, arg);
+	if (!sv)
+		return 1;
+
+	HA_SPIN_LOCK(SERVER_LOCK, &sv->lock);
+
+	counters_be_reset(&sv->counters);
+	srv_stats_clear_extra_counters(sv);
+
+	HA_SPIN_UNLOCK(SERVER_LOCK, &sv->lock);
+
+	cli_msg(appctx, LOG_NOTICE, "Server counters cleared.\n");
+	return 1;
+}
+
 /* register cli keywords */
 static struct cli_kw_list cli_kws = {{ },{
 	{ { "disable", "agent",  NULL },         "disable agent                           : disable agent checks",                                        cli_parse_disable_agent, NULL },
diff --git a/src/stats-proxy.c b/src/stats-proxy.c
index 826449458..d120fb73c 100644
--- a/src/stats-proxy.c
+++ b/src/stats-proxy.c
@@ -1716,3 +1716,32 @@ void proxy_stats_clear_counters(int clrall, struct list *stat_modules)
 		}
 	}
 }
+
+/* Reset the module-registered extra counters of a single server <sv>, as done
+ * by "clear counters" for every server. Only modules flagged clearable are
+ * reset, matching the OPER-level "clear counters" semantics (the ADMIN-only
+ * "clear counters all" resets non-clearable modules too, but there is no
+ * per-server equivalent of "all"). The native counters are handled separately
+ * by the caller via counters_be_reset().
+ */
+void srv_stats_clear_extra_counters(struct server *sv)
+{
+	struct list *stat_modules = &stats_module_list[STATS_DOMAIN_PROXY];
+	struct stats_module *mod;
+
+	list_for_each_entry(mod, stat_modules, list) {
+		enum stats_domain_px_cap mod_cap = stats_px_get_cap(mod->domain_flags);
+
+		if (!mod->clearable)
+			continue;
+		if (!(mod_cap & STATS_PX_CAP_SRV))
+			continue;
+		if (!sv->extra_counters)
+			continue;
+
+		EXTRA_COUNTERS_INIT(sv->extra_counters,
+		                    mod,
+		                    mod->counters,
+		                    mod->counters_size);
+	}
+}
diff --git a/src/stats.c b/src/stats.c
index 6843f49b4..6a7d8178f 100644
--- a/src/stats.c
+++ b/src/stats.c
@@ -890,6 +890,18 @@ static int cli_parse_clear_counters(char **args, char *payload, struct appctx *a
 {
 	int clrall = 0;
 
+	/* "clear counters server <b>/<s>" resets the counters of a single
+	 * server. Handled here rather than via a dedicated three-word CLI
+	 * keyword because "clear counters" is a two-word keyword that would
+	 * shadow any three-word variant during keyword lookup. Requires
+	 * OPER level, like the base "clear counters".
+	 */
+	if (strcmp(args[2], "server") == 0) {
+		if (!cli_has_level(appctx, ACCESS_LVL_OPER))
+			return 1;
+		return cli_clear_counters_server(appctx, args[3]);
+	}
+
 	if (strcmp(args[2], "all") == 0)
 		clrall = 1;
 
@@ -1355,11 +1367,11 @@ REGISTER_PER_THREAD_FREE(free_trash_counters);
 
 /* register cli keywords */
 static struct cli_kw_list cli_kws = {{ },{
-	{ { "clear", "counters",  NULL },      "clear counters [all]                    : clear max statistics counters (or all counters)", cli_parse_clear_counters, NULL, NULL },
-	{ { "show", "info",  NULL },           "show info [desc|json|typed|float]*      : report information about the running process",    cli_parse_show_info, cli_io_handler_dump_info, NULL },
-	{ { "show", "stat",  NULL },           "show stat [desc|json|no-maint|typed|up]*: report counters for each proxy and server",       cli_parse_show_stat, cli_io_handler_dump_stat, cli_io_handler_release_stat },
-	{ { "show", "schema",  "json", NULL }, "show schema json                        : report schema used for stats",                    cli_parse_show_schema_json, cli_io_handler_dump_json_schema, NULL },
-	{ { "dump", "stats-file", NULL },      "dump stats-file                         : dump stats for restore",                          cli_parse_dump_stat_file, cli_io_handler_dump_stat_file, cli_io_handler_release_dump_stat_file },
+	{ { "clear", "counters",  NULL },      "clear counters [all|server <bk>/<srv>]  : clear max statistics counters (or all, or one server's)", cli_parse_clear_counters,   NULL, NULL },
+	{ { "show", "info",  NULL },           "show info [desc|json|typed|float]*      : report information about the running process",            cli_parse_show_info,        cli_io_handler_dump_info, NULL },
+	{ { "show", "stat",  NULL },           "show stat [desc|json|no-maint|typed|up]*: report counters for each proxy and server",               cli_parse_show_stat,        cli_io_handler_dump_stat, cli_io_handler_release_stat },
+	{ { "show", "schema",  "json", NULL }, "show schema json                        : report schema used for stats",                            cli_parse_show_schema_json, cli_io_handler_dump_json_schema, NULL },
+	{ { "dump", "stats-file", NULL },      "dump stats-file                         : dump stats for restore",                                  cli_parse_dump_stat_file,   cli_io_handler_dump_stat_file, cli_io_handler_release_dump_stat_file },
 	{{},}
 }};
 
-- 
2.43.7

