* [PATCH 0/2] extend interactive telemetry script
@ 2026-05-21 15:39 Bruce Richardson
2026-05-21 15:39 ` [PATCH 1/2] usertools/telemetry: add a FOREACH command Bruce Richardson
` (4 more replies)
0 siblings, 5 replies; 20+ messages in thread
From: Bruce Richardson @ 2026-05-21 15:39 UTC (permalink / raw)
To: dev; +Cc: Bruce Richardson
To simplify interactive telemetry script for general use, i.e. not from
other scripts, we can add two new features to it:
1. Support for FOREACH to allow gathering a set of output values across
a list of ports or devices, e.g. ethdevs or rawdevs.
2. Support having predefined aliases in a file in the user's home
directory to simplify the use of more complicated FOREACH commands.
Putting these together, we can create new commands such as "eth_names".
bruce@host:$ cat ~/.dpdk_telemetry_aliases
eth_names=FOREACH index /ethdev/list /ethdev/info,$index .name
bruce@host:$ echo eth_names | ./usertools/dpdk-telemetry.py | jq
[
{
"index": 0,
"name": "0000:16:00.0"
},
{
"index": 1,
"name": "0000:16:00.1"
}
]
Bruce Richardson (2):
usertools/telemetry: add a FOREACH command
usertools/telemetry: support using aliases for long commands
doc/guides/howto/telemetry.rst | 76 +++++++++++++
usertools/dpdk-telemetry.py | 201 ++++++++++++++++++++++++++++++++-
2 files changed, 271 insertions(+), 6 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH 1/2] usertools/telemetry: add a FOREACH command
2026-05-21 15:39 [PATCH 0/2] extend interactive telemetry script Bruce Richardson
@ 2026-05-21 15:39 ` Bruce Richardson
2026-05-21 15:39 ` [PATCH 2/2] usertools/telemetry: support using aliases for long commands Bruce Richardson
` (3 subsequent siblings)
4 siblings, 0 replies; 20+ messages in thread
From: Bruce Richardson @ 2026-05-21 15:39 UTC (permalink / raw)
To: dev; +Cc: Bruce Richardson
To simplify querying data from multiple devices, e.g. across ethdevs, or
dmadevs, add a FOREACH command to the python script, allowing you to
run, e.g. /ethdev/list, and then run a second command for each item in
the list, gathering the relevant output values, optionally including an
index counter.
Simple examples are given in the documentation:
--> FOREACH /ethdev/list /ethdev/stats .opackets
[0, 0]
--> FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
[{"ipackets": 0, "opackets": 0}, {"ipackets": 0, "opackets": 0}]
--> FOREACH i /ethdev/list /ethdev/info,$i .name
[{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
--> FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
[{"i": 0, "ipackets": 0, "opackets": 0}, {"i": 1, "ipackets": 0, "opackets": 0}]
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
doc/guides/howto/telemetry.rst | 42 +++++++++++
usertools/dpdk-telemetry.py | 128 ++++++++++++++++++++++++++++++++-
2 files changed, 167 insertions(+), 3 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index 0464c431fe..4bf48c635e 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -88,6 +88,48 @@ and query information using the telemetry client python script.
{"/help": {"/ethdev/xstats": "Returns the extended stats for a port.
Parameters: int port_id"}}
+ * Run a compound query using ``FOREACH``.
+
+ The ``FOREACH`` command runs a list command, iterates each returned item,
+ runs a second command for each item, and emits combined JSON output.
+
+ Start with the simplest form (no loop variable)::
+
+ FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
+
+ To include numbered output, use a loop variable::
+
+ FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
+
+ Notes:
+
+ - Field selectors are whitespace-separated tokens, each starting with ``.``.
+ - In no-variable mode, the iter command is called as ``/<iter_cmd>,<item>``.
+ - In loop-variable mode, use ``$<var>`` in the iter command where the
+ item value should be substituted.
+
+ Examples::
+
+ --> FOREACH /ethdev/list /ethdev/stats .opackets
+ [0, 0]
+
+ --> FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
+ [{"ipackets": 0, "opackets": 0}, {"ipackets": 0, "opackets": 0}]
+
+ --> FOREACH i /ethdev/list /ethdev/info,$i .name
+ [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
+
+ --> FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
+ [{"i": 0, "ipackets": 0, "opackets": 0}, {"i": 1, "ipackets": 0, "opackets": 0}]
+
+ Output behavior:
+
+ - Without loop variable and one field: returns an array of values.
+ - Without loop variable and multiple fields: returns an array of objects
+ containing named value fields.
+ - With loop variable: returns an array of objects containing the loop
+ variable field and requested value fields.
+
Connecting to Different DPDK Processes
--------------------------------------
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 09258a1f7e..2de10cff69 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -23,6 +23,130 @@
CMDS = []
+def send_command(sock, cmd, output_buf_len, echo=False, pretty=False):
+ """Send a telemetry command and return the parsed JSON reply"""
+ sock.send(cmd.encode())
+ return read_socket(sock, output_buf_len, echo, pretty)
+
+
+def get_cmd_payload(reply, cmd):
+ """Return the payload for a command response if present"""
+ if isinstance(reply, dict) and len(reply) == 1:
+ return next(iter(reply.values()))
+ return None
+
+
+def get_path_value(payload, path):
+ """Resolve a dotted path (e.g. '.name' or '.a.b') from a JSON payload"""
+ if not path:
+ return payload
+
+ keys = [k for k in path.lstrip(".").split(".") if k]
+ val = payload
+ for key in keys:
+ if not isinstance(val, dict) or key not in val:
+ return None
+ val = val[key]
+ return val
+
+
+def parse_selectors(selector_text):
+ """Parse whitespace-separated dotted selectors"""
+ selectors = selector_text.split()
+ if not selectors:
+ print("Invalid FOREACH syntax: missing selector")
+ return None
+ if any(not selector.startswith(".") for selector in selectors):
+ print("Invalid FOREACH syntax: selector must start with '.'")
+ return None
+ return selectors
+
+
+def parse_foreach(text):
+ """Parse FOREACH [<var>] /<cmd> /<parameterized cmd> .<value> [.<value> ...]"""
+ try:
+ tokens = text.split(None, 3)
+ except ValueError:
+ print("Invalid FOREACH syntax")
+ return None
+
+ if len(tokens) != 4:
+ print("Invalid FOREACH syntax")
+ return None
+
+ _, arg1, arg2, arg3 = tokens
+ if arg1.startswith("/"):
+ var_name = None
+ list_cmd = arg1
+ iter_cmd = arg2
+ selector_text = arg3
+ else:
+ var_name = arg1
+ list_cmd = arg2
+ try:
+ iter_cmd, selector_text = arg3.split(None, 1)
+ except ValueError:
+ print("Invalid FOREACH syntax")
+ return None
+
+ if not list_cmd.startswith("/") or not iter_cmd.startswith("/"):
+ print("Invalid FOREACH syntax: commands must start with '/'")
+ return None
+
+ selectors = parse_selectors(selector_text)
+ if selectors is None:
+ return None
+
+ return var_name, list_cmd, iter_cmd, selectors
+
+
+def build_foreach_result(item, var_name, payload, selectors):
+ """Build one FOREACH result entry based on selector count and index mode"""
+ values = {selector.lstrip("."): get_path_value(payload, selector) for selector in selectors}
+
+ if var_name is None and len(selectors) == 1:
+ return next(iter(values.values()))
+ if var_name is None:
+ return values
+
+ return {var_name: item, **values}
+
+
+def handle_foreach(sock, output_buf_len, text, pretty=False):
+ """Handle FOREACH queries and print telemetry-like JSON array output"""
+ parsed = parse_foreach(text)
+ if parsed is None:
+ return
+ var_name, list_cmd, iter_cmd, selectors = parsed
+
+ list_reply = send_command(sock, list_cmd, output_buf_len)
+ values = get_cmd_payload(list_reply, list_cmd)
+ if not isinstance(values, list):
+ print("FOREACH source command did not return a JSON array")
+ return
+
+ output = []
+ for item in values:
+ if var_name is None:
+ cmd = "{},{}".format(iter_cmd, item)
+ else:
+ cmd = iter_cmd.replace("$" + var_name, str(item))
+ item_reply = send_command(sock, cmd, output_buf_len)
+ item_payload = get_cmd_payload(item_reply, cmd)
+ output.append(build_foreach_result(item, var_name, item_payload, selectors))
+
+ indent = 2 if pretty else None
+ print(json.dumps(output, indent=indent))
+
+
+def handle_command(sock, output_buf_len, text, pretty=False):
+ """Execute a user command if recognized"""
+ if text.startswith("/"):
+ send_command(sock, text, output_buf_len, echo=True, pretty=pretty)
+ elif text.startswith("FOREACH "):
+ handle_foreach(sock, output_buf_len, text, pretty)
+
+
def read_socket(sock, buf_len, echo=True, pretty=False):
"""Read data from socket and return it in JSON format"""
reply = sock.recv(buf_len).decode()
@@ -140,9 +264,7 @@ def handle_socket(args, path):
try:
text = input(prompt).strip()
while text != "quit":
- if text.startswith("/"):
- sock.send(text.encode())
- read_socket(sock, output_buf_len, pretty=prompt)
+ handle_command(sock, output_buf_len, text, pretty=prompt)
text = input(prompt).strip()
except EOFError:
pass
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH 2/2] usertools/telemetry: support using aliases for long commands
2026-05-21 15:39 [PATCH 0/2] extend interactive telemetry script Bruce Richardson
2026-05-21 15:39 ` [PATCH 1/2] usertools/telemetry: add a FOREACH command Bruce Richardson
@ 2026-05-21 15:39 ` Bruce Richardson
2026-05-22 0:44 ` [PATCH 0/2] extend interactive telemetry script fengchengwen
` (2 subsequent siblings)
4 siblings, 0 replies; 20+ messages in thread
From: Bruce Richardson @ 2026-05-21 15:39 UTC (permalink / raw)
To: dev; +Cc: Bruce Richardson
Similarly to how shell aliases work, allow specifying of short alias
commands for dpdk-telemetry.py script. The aliases are read from
"$HOME/.dpdk_telemetry_aliases" at startup.
Some examples of use from the docs. Alias file contents:
# Basic shortcuts
ls=/ethdev/list
names=FOREACH i /ethdev/list /ethdev/info,$i .name
q=quit
Alias use:
--> ls
{"/ethdev/list": [0, 1]}
--> names
[{"i": 0, "name": "0000:
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
doc/guides/howto/telemetry.rst | 34 +++++++++++++++
usertools/dpdk-telemetry.py | 75 ++++++++++++++++++++++++++++++++--
2 files changed, 105 insertions(+), 4 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index 4bf48c635e..072289aec8 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -130,6 +130,40 @@ and query information using the telemetry client python script.
- With loop variable: returns an array of objects containing the loop
variable field and requested value fields.
+ * Use command aliases.
+
+ The telemetry script can load aliases at startup from::
+
+ $HOME/.dpdk_telemetry_aliases
+
+ Each alias entry must be in ``alias=command`` format.
+ Empty lines and lines starting with ``#`` are ignored.
+
+ Example alias file::
+
+ # Basic shortcuts
+ ls=/ethdev/list
+ names=FOREACH i /ethdev/list /ethdev/info,$i .name
+ q=quit
+
+ Alias behavior is intentionally similar to shell aliases:
+
+ - The first token of the entered input is checked for an alias match.
+ - If matched, that first token is replaced with its expansion.
+ - Alias expansion is recursive (aliases can expand to other aliases).
+ - Expansion has a safety limit to prevent infinite loops.
+
+ Examples::
+
+ --> ls
+ {"/ethdev/list": [0, 1]}
+
+ --> names
+ [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
+
+ --> q
+ # exits the client
+
Connecting to Different DPDK Processes
--------------------------------------
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 2de10cff69..8b976160e0 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -21,6 +21,70 @@
SOCKET_NAME = "dpdk_telemetry.{}".format(TELEMETRY_VERSION)
DEFAULT_PREFIX = "rte"
CMDS = []
+ALIASES = {}
+ALIAS_FILE = ".dpdk_telemetry_aliases"
+MAX_ALIAS_EXPANSIONS = 32
+
+
+def load_aliases():
+ """Load aliases from $HOME/.dpdk_telemetry_aliases"""
+ aliases = {}
+ home = os.environ.get("HOME")
+ if not home:
+ return aliases
+
+ alias_path = os.path.join(home, ALIAS_FILE)
+ if not os.path.isfile(alias_path):
+ return aliases
+
+ try:
+ with open(alias_path) as alias_file:
+ for line_num, line in enumerate(alias_file, start=1):
+ entry = line.strip()
+ if not entry or entry.startswith("#"):
+ continue
+ if "=" not in entry:
+ print(
+ "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
+ file=sys.stderr,
+ )
+ continue
+ name, command = entry.split("=", 1)
+ name = name.strip()
+ command = command.strip()
+ if not name or not command:
+ print(
+ "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
+ file=sys.stderr,
+ )
+ continue
+ aliases[name] = command
+ except OSError as e:
+ print("Warning: failed to read {}: {}".format(alias_path, e), file=sys.stderr)
+
+ return aliases
+
+
+def expand_aliases(text, aliases):
+ """Expand aliases similarly to shell aliases on the first token"""
+ expanded = text
+ for _ in range(MAX_ALIAS_EXPANSIONS):
+ stripped = expanded.lstrip()
+ if not stripped:
+ return expanded
+
+ parts = stripped.split(None, 1)
+ first = parts[0]
+ rest = parts[1] if len(parts) > 1 else ""
+
+ if first not in aliases:
+ return expanded
+
+ alias_value = aliases[first]
+ expanded = "{} {}".format(alias_value, rest).strip() if rest else alias_value
+
+ print("Warning: alias expansion limit reached", file=sys.stderr)
+ return expanded
def send_command(sock, cmd, output_buf_len, echo=False, pretty=False):
@@ -262,10 +326,12 @@ def handle_socket(args, path):
# interactive prompt
try:
- text = input(prompt).strip()
- while text != "quit":
- handle_command(sock, output_buf_len, text, pretty=prompt)
+ while True:
text = input(prompt).strip()
+ expanded = expand_aliases(text, ALIASES)
+ if expanded == "quit":
+ break
+ handle_command(sock, output_buf_len, expanded, pretty=prompt)
except EOFError:
pass
finally:
@@ -274,7 +340,7 @@ def handle_socket(args, path):
def readline_complete(text, state):
"""Find any matching commands from the list based on user input"""
- all_cmds = ["quit"] + CMDS
+ all_cmds = ["quit"] + list(ALIASES.keys()) + CMDS
if text:
matches = [c for c in all_cmds if c.startswith(text)]
else:
@@ -304,6 +370,7 @@ def readline_complete(text, state):
help="List all possible file-prefixes and exit",
)
args = parser.parse_args()
+ALIASES = load_aliases()
if args.list:
list_fp()
sys.exit(0)
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* Re: [PATCH 0/2] extend interactive telemetry script
2026-05-21 15:39 [PATCH 0/2] extend interactive telemetry script Bruce Richardson
2026-05-21 15:39 ` [PATCH 1/2] usertools/telemetry: add a FOREACH command Bruce Richardson
2026-05-21 15:39 ` [PATCH 2/2] usertools/telemetry: support using aliases for long commands Bruce Richardson
@ 2026-05-22 0:44 ` fengchengwen
2026-05-22 7:51 ` Bruce Richardson
2026-05-22 13:37 ` [PATCH v2 0/3] " Bruce Richardson
2026-06-09 16:13 ` [PATCH v3 0/3] extend interactive telemetry script Bruce Richardson
4 siblings, 1 reply; 20+ messages in thread
From: fengchengwen @ 2026-05-22 0:44 UTC (permalink / raw)
To: Bruce Richardson, dev
On 5/21/2026 11:39 PM, Bruce Richardson wrote:
> To simplify interactive telemetry script for general use, i.e. not from
> other scripts, we can add two new features to it:
>
> 1. Support for FOREACH to allow gathering a set of output values across
> a list of ports or devices, e.g. ethdevs or rawdevs.
> 2. Support having predefined aliases in a file in the user's home
> directory to simplify the use of more complicated FOREACH commands.
+1
How about add help command to introducing above FOREACH and aliases?
>
> Putting these together, we can create new commands such as "eth_names".
>
> bruce@host:$ cat ~/.dpdk_telemetry_aliases
> eth_names=FOREACH index /ethdev/list /ethdev/info,$index .name
>
> bruce@host:$ echo eth_names | ./usertools/dpdk-telemetry.py | jq
> [
> {
> "index": 0,
> "name": "0000:16:00.0"
> },
> {
> "index": 1,
> "name": "0000:16:00.1"
> }
> ]
>
> Bruce Richardson (2):
> usertools/telemetry: add a FOREACH command
> usertools/telemetry: support using aliases for long commands
>
> doc/guides/howto/telemetry.rst | 76 +++++++++++++
> usertools/dpdk-telemetry.py | 201 ++++++++++++++++++++++++++++++++-
> 2 files changed, 271 insertions(+), 6 deletions(-)
>
> --
> 2.53.0
>
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH 0/2] extend interactive telemetry script
2026-05-22 0:44 ` [PATCH 0/2] extend interactive telemetry script fengchengwen
@ 2026-05-22 7:51 ` Bruce Richardson
0 siblings, 0 replies; 20+ messages in thread
From: Bruce Richardson @ 2026-05-22 7:51 UTC (permalink / raw)
To: fengchengwen; +Cc: dev
On Fri, May 22, 2026 at 08:44:55AM +0800, fengchengwen wrote:
> On 5/21/2026 11:39 PM, Bruce Richardson wrote:
> > To simplify interactive telemetry script for general use, i.e. not from
> > other scripts, we can add two new features to it:
> >
> > 1. Support for FOREACH to allow gathering a set of output values across
> > a list of ports or devices, e.g. ethdevs or rawdevs.
> > 2. Support having predefined aliases in a file in the user's home
> > directory to simplify the use of more complicated FOREACH commands.
>
> +1
> How about add help command to introducing above FOREACH and aliases?
>
Sure, not a bad idea if we are extending the script to make it more
powerful. Will add in a V2.
/Bruce
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v2 0/3] extend interactive telemetry script
2026-05-21 15:39 [PATCH 0/2] extend interactive telemetry script Bruce Richardson
` (2 preceding siblings ...)
2026-05-22 0:44 ` [PATCH 0/2] extend interactive telemetry script fengchengwen
@ 2026-05-22 13:37 ` Bruce Richardson
2026-05-22 13:37 ` [PATCH v2 1/3] usertools/telemetry: add a FOREACH command Bruce Richardson
` (2 more replies)
2026-06-09 16:13 ` [PATCH v3 0/3] extend interactive telemetry script Bruce Richardson
4 siblings, 3 replies; 20+ messages in thread
From: Bruce Richardson @ 2026-05-22 13:37 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
To simplify interactive telemetry script for general use, i.e. not from
other scripts, we can add two new features to it:
1. Support for FOREACH to allow gathering a set of output values across
a list of ports or devices, e.g. ethdevs or rawdevs.
2. Support having predefined aliases in a file in the user's home
directory to simplify the use of more complicated FOREACH commands.
Putting these together, we can create new commands such as "eth_names".
bruce@host:$ cat ~/.dpdk_telemetry_aliases
eth_names=FOREACH index /ethdev/list /ethdev/info,$index .name
bruce@host:$ echo eth_names | ./usertools/dpdk-telemetry.py | jq
[
{
"index": 0,
"name": "0000:16:00.0"
},
{
"index": 1,
"name": "0000:16:00.1"
}
]
---
v2: added third patch with "help" command giving more details on
how to use the various commands.
Bruce Richardson (3):
usertools/telemetry: add a FOREACH command
usertools/telemetry: support using aliases for long commands
usertools/telemetry: add help support
doc/guides/howto/telemetry.rst | 76 ++++++++++
usertools/dpdk-telemetry.py | 260 ++++++++++++++++++++++++++++++++-
2 files changed, 330 insertions(+), 6 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v2 1/3] usertools/telemetry: add a FOREACH command
2026-05-22 13:37 ` [PATCH v2 0/3] " Bruce Richardson
@ 2026-05-22 13:37 ` Bruce Richardson
2026-05-29 0:17 ` fengchengwen
2026-05-22 13:37 ` [PATCH v2 2/3] usertools/telemetry: support using aliases for long commands Bruce Richardson
2026-05-22 13:37 ` [PATCH v2 3/3] usertools/telemetry: add help support Bruce Richardson
2 siblings, 1 reply; 20+ messages in thread
From: Bruce Richardson @ 2026-05-22 13:37 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
To simplify querying data from multiple devices, e.g. across ethdevs, or
dmadevs, add a FOREACH command to the python script, allowing you to
run, e.g. /ethdev/list, and then run a second command for each item in
the list, gathering the relevant output values, optionally including an
index counter.
Simple examples are given in the documentation:
--> FOREACH /ethdev/list /ethdev/stats .opackets
[0, 0]
--> FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
[{"ipackets": 0, "opackets": 0}, {"ipackets": 0, "opackets": 0}]
--> FOREACH i /ethdev/list /ethdev/info,$i .name
[{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
--> FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
[{"i": 0, "ipackets": 0, "opackets": 0}, {"i": 1, "ipackets": 0, "opackets": 0}]
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
doc/guides/howto/telemetry.rst | 42 +++++++++++
usertools/dpdk-telemetry.py | 128 ++++++++++++++++++++++++++++++++-
2 files changed, 167 insertions(+), 3 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index 0464c431fe..4bf48c635e 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -88,6 +88,48 @@ and query information using the telemetry client python script.
{"/help": {"/ethdev/xstats": "Returns the extended stats for a port.
Parameters: int port_id"}}
+ * Run a compound query using ``FOREACH``.
+
+ The ``FOREACH`` command runs a list command, iterates each returned item,
+ runs a second command for each item, and emits combined JSON output.
+
+ Start with the simplest form (no loop variable)::
+
+ FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
+
+ To include numbered output, use a loop variable::
+
+ FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
+
+ Notes:
+
+ - Field selectors are whitespace-separated tokens, each starting with ``.``.
+ - In no-variable mode, the iter command is called as ``/<iter_cmd>,<item>``.
+ - In loop-variable mode, use ``$<var>`` in the iter command where the
+ item value should be substituted.
+
+ Examples::
+
+ --> FOREACH /ethdev/list /ethdev/stats .opackets
+ [0, 0]
+
+ --> FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
+ [{"ipackets": 0, "opackets": 0}, {"ipackets": 0, "opackets": 0}]
+
+ --> FOREACH i /ethdev/list /ethdev/info,$i .name
+ [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
+
+ --> FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
+ [{"i": 0, "ipackets": 0, "opackets": 0}, {"i": 1, "ipackets": 0, "opackets": 0}]
+
+ Output behavior:
+
+ - Without loop variable and one field: returns an array of values.
+ - Without loop variable and multiple fields: returns an array of objects
+ containing named value fields.
+ - With loop variable: returns an array of objects containing the loop
+ variable field and requested value fields.
+
Connecting to Different DPDK Processes
--------------------------------------
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 09258a1f7e..2de10cff69 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -23,6 +23,130 @@
CMDS = []
+def send_command(sock, cmd, output_buf_len, echo=False, pretty=False):
+ """Send a telemetry command and return the parsed JSON reply"""
+ sock.send(cmd.encode())
+ return read_socket(sock, output_buf_len, echo, pretty)
+
+
+def get_cmd_payload(reply, cmd):
+ """Return the payload for a command response if present"""
+ if isinstance(reply, dict) and len(reply) == 1:
+ return next(iter(reply.values()))
+ return None
+
+
+def get_path_value(payload, path):
+ """Resolve a dotted path (e.g. '.name' or '.a.b') from a JSON payload"""
+ if not path:
+ return payload
+
+ keys = [k for k in path.lstrip(".").split(".") if k]
+ val = payload
+ for key in keys:
+ if not isinstance(val, dict) or key not in val:
+ return None
+ val = val[key]
+ return val
+
+
+def parse_selectors(selector_text):
+ """Parse whitespace-separated dotted selectors"""
+ selectors = selector_text.split()
+ if not selectors:
+ print("Invalid FOREACH syntax: missing selector")
+ return None
+ if any(not selector.startswith(".") for selector in selectors):
+ print("Invalid FOREACH syntax: selector must start with '.'")
+ return None
+ return selectors
+
+
+def parse_foreach(text):
+ """Parse FOREACH [<var>] /<cmd> /<parameterized cmd> .<value> [.<value> ...]"""
+ try:
+ tokens = text.split(None, 3)
+ except ValueError:
+ print("Invalid FOREACH syntax")
+ return None
+
+ if len(tokens) != 4:
+ print("Invalid FOREACH syntax")
+ return None
+
+ _, arg1, arg2, arg3 = tokens
+ if arg1.startswith("/"):
+ var_name = None
+ list_cmd = arg1
+ iter_cmd = arg2
+ selector_text = arg3
+ else:
+ var_name = arg1
+ list_cmd = arg2
+ try:
+ iter_cmd, selector_text = arg3.split(None, 1)
+ except ValueError:
+ print("Invalid FOREACH syntax")
+ return None
+
+ if not list_cmd.startswith("/") or not iter_cmd.startswith("/"):
+ print("Invalid FOREACH syntax: commands must start with '/'")
+ return None
+
+ selectors = parse_selectors(selector_text)
+ if selectors is None:
+ return None
+
+ return var_name, list_cmd, iter_cmd, selectors
+
+
+def build_foreach_result(item, var_name, payload, selectors):
+ """Build one FOREACH result entry based on selector count and index mode"""
+ values = {selector.lstrip("."): get_path_value(payload, selector) for selector in selectors}
+
+ if var_name is None and len(selectors) == 1:
+ return next(iter(values.values()))
+ if var_name is None:
+ return values
+
+ return {var_name: item, **values}
+
+
+def handle_foreach(sock, output_buf_len, text, pretty=False):
+ """Handle FOREACH queries and print telemetry-like JSON array output"""
+ parsed = parse_foreach(text)
+ if parsed is None:
+ return
+ var_name, list_cmd, iter_cmd, selectors = parsed
+
+ list_reply = send_command(sock, list_cmd, output_buf_len)
+ values = get_cmd_payload(list_reply, list_cmd)
+ if not isinstance(values, list):
+ print("FOREACH source command did not return a JSON array")
+ return
+
+ output = []
+ for item in values:
+ if var_name is None:
+ cmd = "{},{}".format(iter_cmd, item)
+ else:
+ cmd = iter_cmd.replace("$" + var_name, str(item))
+ item_reply = send_command(sock, cmd, output_buf_len)
+ item_payload = get_cmd_payload(item_reply, cmd)
+ output.append(build_foreach_result(item, var_name, item_payload, selectors))
+
+ indent = 2 if pretty else None
+ print(json.dumps(output, indent=indent))
+
+
+def handle_command(sock, output_buf_len, text, pretty=False):
+ """Execute a user command if recognized"""
+ if text.startswith("/"):
+ send_command(sock, text, output_buf_len, echo=True, pretty=pretty)
+ elif text.startswith("FOREACH "):
+ handle_foreach(sock, output_buf_len, text, pretty)
+
+
def read_socket(sock, buf_len, echo=True, pretty=False):
"""Read data from socket and return it in JSON format"""
reply = sock.recv(buf_len).decode()
@@ -140,9 +264,7 @@ def handle_socket(args, path):
try:
text = input(prompt).strip()
while text != "quit":
- if text.startswith("/"):
- sock.send(text.encode())
- read_socket(sock, output_buf_len, pretty=prompt)
+ handle_command(sock, output_buf_len, text, pretty=prompt)
text = input(prompt).strip()
except EOFError:
pass
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 2/3] usertools/telemetry: support using aliases for long commands
2026-05-22 13:37 ` [PATCH v2 0/3] " Bruce Richardson
2026-05-22 13:37 ` [PATCH v2 1/3] usertools/telemetry: add a FOREACH command Bruce Richardson
@ 2026-05-22 13:37 ` Bruce Richardson
2026-05-29 0:25 ` fengchengwen
2026-05-22 13:37 ` [PATCH v2 3/3] usertools/telemetry: add help support Bruce Richardson
2 siblings, 1 reply; 20+ messages in thread
From: Bruce Richardson @ 2026-05-22 13:37 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
Similarly to how shell aliases work, allow specifying of short alias
commands for dpdk-telemetry.py script. The aliases are read from
"$HOME/.dpdk_telemetry_aliases" at startup.
Some examples of use from the docs. Alias file contents:
# Basic shortcuts
ls=/ethdev/list
names=FOREACH i /ethdev/list /ethdev/info,$i .name
q=quit
Alias use:
--> ls
{"/ethdev/list": [0, 1]}
--> names
[{"i": 0, "name": "0000:
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
doc/guides/howto/telemetry.rst | 34 +++++++++++++++
usertools/dpdk-telemetry.py | 75 ++++++++++++++++++++++++++++++++--
2 files changed, 105 insertions(+), 4 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index 4bf48c635e..072289aec8 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -130,6 +130,40 @@ and query information using the telemetry client python script.
- With loop variable: returns an array of objects containing the loop
variable field and requested value fields.
+ * Use command aliases.
+
+ The telemetry script can load aliases at startup from::
+
+ $HOME/.dpdk_telemetry_aliases
+
+ Each alias entry must be in ``alias=command`` format.
+ Empty lines and lines starting with ``#`` are ignored.
+
+ Example alias file::
+
+ # Basic shortcuts
+ ls=/ethdev/list
+ names=FOREACH i /ethdev/list /ethdev/info,$i .name
+ q=quit
+
+ Alias behavior is intentionally similar to shell aliases:
+
+ - The first token of the entered input is checked for an alias match.
+ - If matched, that first token is replaced with its expansion.
+ - Alias expansion is recursive (aliases can expand to other aliases).
+ - Expansion has a safety limit to prevent infinite loops.
+
+ Examples::
+
+ --> ls
+ {"/ethdev/list": [0, 1]}
+
+ --> names
+ [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
+
+ --> q
+ # exits the client
+
Connecting to Different DPDK Processes
--------------------------------------
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 2de10cff69..8b976160e0 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -21,6 +21,70 @@
SOCKET_NAME = "dpdk_telemetry.{}".format(TELEMETRY_VERSION)
DEFAULT_PREFIX = "rte"
CMDS = []
+ALIASES = {}
+ALIAS_FILE = ".dpdk_telemetry_aliases"
+MAX_ALIAS_EXPANSIONS = 32
+
+
+def load_aliases():
+ """Load aliases from $HOME/.dpdk_telemetry_aliases"""
+ aliases = {}
+ home = os.environ.get("HOME")
+ if not home:
+ return aliases
+
+ alias_path = os.path.join(home, ALIAS_FILE)
+ if not os.path.isfile(alias_path):
+ return aliases
+
+ try:
+ with open(alias_path) as alias_file:
+ for line_num, line in enumerate(alias_file, start=1):
+ entry = line.strip()
+ if not entry or entry.startswith("#"):
+ continue
+ if "=" not in entry:
+ print(
+ "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
+ file=sys.stderr,
+ )
+ continue
+ name, command = entry.split("=", 1)
+ name = name.strip()
+ command = command.strip()
+ if not name or not command:
+ print(
+ "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
+ file=sys.stderr,
+ )
+ continue
+ aliases[name] = command
+ except OSError as e:
+ print("Warning: failed to read {}: {}".format(alias_path, e), file=sys.stderr)
+
+ return aliases
+
+
+def expand_aliases(text, aliases):
+ """Expand aliases similarly to shell aliases on the first token"""
+ expanded = text
+ for _ in range(MAX_ALIAS_EXPANSIONS):
+ stripped = expanded.lstrip()
+ if not stripped:
+ return expanded
+
+ parts = stripped.split(None, 1)
+ first = parts[0]
+ rest = parts[1] if len(parts) > 1 else ""
+
+ if first not in aliases:
+ return expanded
+
+ alias_value = aliases[first]
+ expanded = "{} {}".format(alias_value, rest).strip() if rest else alias_value
+
+ print("Warning: alias expansion limit reached", file=sys.stderr)
+ return expanded
def send_command(sock, cmd, output_buf_len, echo=False, pretty=False):
@@ -262,10 +326,12 @@ def handle_socket(args, path):
# interactive prompt
try:
- text = input(prompt).strip()
- while text != "quit":
- handle_command(sock, output_buf_len, text, pretty=prompt)
+ while True:
text = input(prompt).strip()
+ expanded = expand_aliases(text, ALIASES)
+ if expanded == "quit":
+ break
+ handle_command(sock, output_buf_len, expanded, pretty=prompt)
except EOFError:
pass
finally:
@@ -274,7 +340,7 @@ def handle_socket(args, path):
def readline_complete(text, state):
"""Find any matching commands from the list based on user input"""
- all_cmds = ["quit"] + CMDS
+ all_cmds = ["quit"] + list(ALIASES.keys()) + CMDS
if text:
matches = [c for c in all_cmds if c.startswith(text)]
else:
@@ -304,6 +370,7 @@ def readline_complete(text, state):
help="List all possible file-prefixes and exit",
)
args = parser.parse_args()
+ALIASES = load_aliases()
if args.list:
list_fp()
sys.exit(0)
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 3/3] usertools/telemetry: add help support
2026-05-22 13:37 ` [PATCH v2 0/3] " Bruce Richardson
2026-05-22 13:37 ` [PATCH v2 1/3] usertools/telemetry: add a FOREACH command Bruce Richardson
2026-05-22 13:37 ` [PATCH v2 2/3] usertools/telemetry: support using aliases for long commands Bruce Richardson
@ 2026-05-22 13:37 ` Bruce Richardson
2026-05-29 0:27 ` fengchengwen
2 siblings, 1 reply; 20+ messages in thread
From: Bruce Richardson @ 2026-05-22 13:37 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
While the telemetry infrastructure supported using "/help,/<cmd>" to get
help on specific commands, with the addition of script-specific
commands, we needed better help support to explain the use of FOREACH,
and to allow e.g. "help /<cmd>" using space separation, which is more
intuitive. This patch adds that help support.
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
usertools/dpdk-telemetry.py | 61 ++++++++++++++++++++++++++++++++++++-
1 file changed, 60 insertions(+), 1 deletion(-)
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 8b976160e0..2c88b91517 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -25,6 +25,26 @@
ALIAS_FILE = ".dpdk_telemetry_aliases"
MAX_ALIAS_EXPANSIONS = 32
+BASIC_HELP_TEXT = """Basic usage:
+ /<command>[,<params>] Send a telemetry command to the app
+ FOREACH ... Run a compound query over list items
+ help Show this help
+ help /<command> Show app-provided help for a command
+ help FOREACH Show FOREACH usage and examples
+ quit Exit the client
+"""
+
+FOREACH_HELP_TEXT = """FOREACH usage:
+ FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
+ FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
+
+Examples:
+ FOREACH /ethdev/list /ethdev/stats .opackets
+ FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
+ FOREACH i /ethdev/list /ethdev/info,$i .name
+ FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
+"""
+
def load_aliases():
"""Load aliases from $HOME/.dpdk_telemetry_aliases"""
@@ -203,10 +223,49 @@ def handle_foreach(sock, output_buf_len, text, pretty=False):
print(json.dumps(output, indent=indent))
+def command_exists(cmd):
+ """Check if a telemetry command exists in the command list"""
+ return cmd in CMDS
+
+
+def app_help_command_for(target_cmd):
+ """Build a '/help,<command>' query for app-side command help"""
+ if not target_cmd:
+ return None
+ normalized = target_cmd.strip()
+ if not normalized.startswith("/"):
+ return None
+ if not command_exists(normalized):
+ print("Unknown command for help: {}".format(normalized))
+ return None
+ return "/help,{}".format(normalized)
+
+
+def handle_user_help(sock, output_buf_len, text, pretty=False):
+ """Handle local 'help' command and command-specific help lookup"""
+ parts = text.split(None, 1)
+ if len(parts) == 1:
+ print(BASIC_HELP_TEXT, end="")
+ return
+
+ help_arg = parts[1].strip()
+ if help_arg.upper() == "FOREACH":
+ print(FOREACH_HELP_TEXT, end="")
+ return
+
+ cmd = app_help_command_for(help_arg)
+ if cmd is None:
+ print("Usage: help [FOREACH|/<command>]")
+ return
+ send_command(sock, cmd, output_buf_len, echo=True, pretty=pretty)
+
+
def handle_command(sock, output_buf_len, text, pretty=False):
"""Execute a user command if recognized"""
if text.startswith("/"):
send_command(sock, text, output_buf_len, echo=True, pretty=pretty)
+ elif text == "help" or text.startswith("help "):
+ handle_user_help(sock, output_buf_len, text, pretty)
elif text.startswith("FOREACH "):
handle_foreach(sock, output_buf_len, text, pretty)
@@ -340,7 +399,7 @@ def handle_socket(args, path):
def readline_complete(text, state):
"""Find any matching commands from the list based on user input"""
- all_cmds = ["quit"] + list(ALIASES.keys()) + CMDS
+ all_cmds = ["quit", "help"] + list(ALIASES.keys()) + CMDS
if text:
matches = [c for c in all_cmds if c.startswith(text)]
else:
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* Re: [PATCH v2 1/3] usertools/telemetry: add a FOREACH command
2026-05-22 13:37 ` [PATCH v2 1/3] usertools/telemetry: add a FOREACH command Bruce Richardson
@ 2026-05-29 0:17 ` fengchengwen
0 siblings, 0 replies; 20+ messages in thread
From: fengchengwen @ 2026-05-29 0:17 UTC (permalink / raw)
To: Bruce Richardson, dev
Acked-by: Chengwen Feng <fengchengwen@huawei.com>
On 5/22/2026 9:37 PM, Bruce Richardson wrote:
> To simplify querying data from multiple devices, e.g. across ethdevs, or
> dmadevs, add a FOREACH command to the python script, allowing you to
> run, e.g. /ethdev/list, and then run a second command for each item in
> the list, gathering the relevant output values, optionally including an
> index counter.
>
> Simple examples are given in the documentation:
>
> --> FOREACH /ethdev/list /ethdev/stats .opackets
> [0, 0]
>
> --> FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
> [{"ipackets": 0, "opackets": 0}, {"ipackets": 0, "opackets": 0}]
>
> --> FOREACH i /ethdev/list /ethdev/info,$i .name
> [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
>
> --> FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
> [{"i": 0, "ipackets": 0, "opackets": 0}, {"i": 1, "ipackets": 0, "opackets": 0}]
>
> Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v2 2/3] usertools/telemetry: support using aliases for long commands
2026-05-22 13:37 ` [PATCH v2 2/3] usertools/telemetry: support using aliases for long commands Bruce Richardson
@ 2026-05-29 0:25 ` fengchengwen
0 siblings, 0 replies; 20+ messages in thread
From: fengchengwen @ 2026-05-29 0:25 UTC (permalink / raw)
To: Bruce Richardson, dev
On 5/22/2026 9:37 PM, Bruce Richardson wrote:
> Similarly to how shell aliases work, allow specifying of short alias
> commands for dpdk-telemetry.py script. The aliases are read from
> "$HOME/.dpdk_telemetry_aliases" at startup.
>
> Some examples of use from the docs. Alias file contents:
>
> # Basic shortcuts
> ls=/ethdev/list
> names=FOREACH i /ethdev/list /ethdev/info,$i .name
> q=quit
>
> Alias use:
>
> --> ls
> {"/ethdev/list": [0, 1]}
>
> --> names
> [{"i": 0, "name": "0000:
>
> Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
> ---
> doc/guides/howto/telemetry.rst | 34 +++++++++++++++
> usertools/dpdk-telemetry.py | 75 ++++++++++++++++++++++++++++++++--
> 2 files changed, 105 insertions(+), 4 deletions(-)
>
> diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
> index 4bf48c635e..072289aec8 100644
> --- a/doc/guides/howto/telemetry.rst
> +++ b/doc/guides/howto/telemetry.rst
> @@ -130,6 +130,40 @@ and query information using the telemetry client python script.
> - With loop variable: returns an array of objects containing the loop
> variable field and requested value fields.
>
> + * Use command aliases.
> +
> + The telemetry script can load aliases at startup from::
> +
> + $HOME/.dpdk_telemetry_aliases
How about add one parameter to specific aliases file location
> +
> + Each alias entry must be in ``alias=command`` format.
> + Empty lines and lines starting with ``#`` are ignored.
> +
> + Example alias file::
> +
> + # Basic shortcuts
> + ls=/ethdev/list
> + names=FOREACH i /ethdev/list /ethdev/info,$i .name
> + q=quit
> +
> + Alias behavior is intentionally similar to shell aliases:
> +
> + - The first token of the entered input is checked for an alias match.
> + - If matched, that first token is replaced with its expansion.
> + - Alias expansion is recursive (aliases can expand to other aliases).
> + - Expansion has a safety limit to prevent infinite loops.
> +
> + Examples::
> +
> + --> ls
> + {"/ethdev/list": [0, 1]}
> +
> + --> names
> + [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
> +
> + --> q
> + # exits the client
> +
>
> Connecting to Different DPDK Processes
> --------------------------------------
> diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
> index 2de10cff69..8b976160e0 100755
> --- a/usertools/dpdk-telemetry.py
> +++ b/usertools/dpdk-telemetry.py
> @@ -21,6 +21,70 @@
> SOCKET_NAME = "dpdk_telemetry.{}".format(TELEMETRY_VERSION)
> DEFAULT_PREFIX = "rte"
> CMDS = []
> +ALIASES = {}
> +ALIAS_FILE = ".dpdk_telemetry_aliases"
> +MAX_ALIAS_EXPANSIONS = 32
> +
> +
> +def load_aliases():
> + """Load aliases from $HOME/.dpdk_telemetry_aliases"""
> + aliases = {}
> + home = os.environ.get("HOME")
> + if not home:
> + return aliases
> +
> + alias_path = os.path.join(home, ALIAS_FILE)
> + if not os.path.isfile(alias_path):
> + return aliases
> +
> + try:
> + with open(alias_path) as alias_file:
> + for line_num, line in enumerate(alias_file, start=1):
> + entry = line.strip()
> + if not entry or entry.startswith("#"):
> + continue
> + if "=" not in entry:
> + print(
> + "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
> + file=sys.stderr,
> + )
> + continue
> + name, command = entry.split("=", 1)
> + name = name.strip()
> + command = command.strip()
> + if not name or not command:
> + print(
> + "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
> + file=sys.stderr,
> + )
> + continue
> + aliases[name] = command
> + except OSError as e:
> + print("Warning: failed to read {}: {}".format(alias_path, e), file=sys.stderr)
If load OK, please add one promote at begin:
Loaded xxx command aliases, see `help alias` for detail
> +
> + return aliases
> +
> +
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v2 3/3] usertools/telemetry: add help support
2026-05-22 13:37 ` [PATCH v2 3/3] usertools/telemetry: add help support Bruce Richardson
@ 2026-05-29 0:27 ` fengchengwen
0 siblings, 0 replies; 20+ messages in thread
From: fengchengwen @ 2026-05-29 0:27 UTC (permalink / raw)
To: Bruce Richardson, dev
On 5/22/2026 9:37 PM, Bruce Richardson wrote:
> While the telemetry infrastructure supported using "/help,/<cmd>" to get
> help on specific commands, with the addition of script-specific
> commands, we needed better help support to explain the use of FOREACH,
> and to allow e.g. "help /<cmd>" using space separation, which is more
> intuitive. This patch adds that help support.
>
> Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
> ---
> usertools/dpdk-telemetry.py | 61 ++++++++++++++++++++++++++++++++++++-
> 1 file changed, 60 insertions(+), 1 deletion(-)
>
> diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
> index 8b976160e0..2c88b91517 100755
> --- a/usertools/dpdk-telemetry.py
> +++ b/usertools/dpdk-telemetry.py
> @@ -25,6 +25,26 @@
> ALIAS_FILE = ".dpdk_telemetry_aliases"
> MAX_ALIAS_EXPANSIONS = 32
>
> +BASIC_HELP_TEXT = """Basic usage:
> + /<command>[,<params>] Send a telemetry command to the app
> + FOREACH ... Run a compound query over list items
> + help Show this help
> + help /<command> Show app-provided help for a command
> + help FOREACH Show FOREACH usage and examples
> + quit Exit the client
Please add help alias to show ALL loaded command alias
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v3 0/3] extend interactive telemetry script
2026-05-21 15:39 [PATCH 0/2] extend interactive telemetry script Bruce Richardson
` (3 preceding siblings ...)
2026-05-22 13:37 ` [PATCH v2 0/3] " Bruce Richardson
@ 2026-06-09 16:13 ` Bruce Richardson
2026-06-09 16:13 ` [PATCH v3 1/3] usertools/telemetry: add a FOREACH command Bruce Richardson
` (3 more replies)
4 siblings, 4 replies; 20+ messages in thread
From: Bruce Richardson @ 2026-06-09 16:13 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
To simplify interactive telemetry script for general use, i.e. not from
other scripts, we can add two new features to it:
1. Support for FOREACH to allow gathering a set of output values across
a list of ports or devices, e.g. ethdevs or rawdevs.
2. Support having predefined aliases in a file in the user's home
directory to simplify the use of more complicated FOREACH commands.
Putting these together, we can create new commands such as "eth_names".
bruce@host:$ cat ~/.dpdk_telemetry_aliases
eth_names=FOREACH index /ethdev/list /ethdev/info,$index .name
bruce@host:$ echo eth_names | ./usertools/dpdk-telemetry.py | jq
[
{
"index": 0,
"name": "0000:16:00.0"
},
{
"index": 1,
"name": "0000:16:00.1"
}
]
---
v3: updated based on review feedback from Chengwen:
- added arg to override alias file
- printed one-line summary of alias count loaded
- improved doc for "help" command
- added "help alias" to list aliases.
v2: added third patch with "help" command giving more details on
how to use the various commands.
Bruce Richardson (3):
usertools/telemetry: add a FOREACH command
usertools/telemetry: support using aliases for long commands
usertools/telemetry: add help support
doc/guides/howto/telemetry.rst | 106 ++++++++++++-
usertools/dpdk-telemetry.py | 278 ++++++++++++++++++++++++++++++++-
2 files changed, 373 insertions(+), 11 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v3 1/3] usertools/telemetry: add a FOREACH command
2026-06-09 16:13 ` [PATCH v3 0/3] extend interactive telemetry script Bruce Richardson
@ 2026-06-09 16:13 ` Bruce Richardson
2026-06-09 16:13 ` [PATCH v3 2/3] usertools/telemetry: support using aliases for long commands Bruce Richardson
` (2 subsequent siblings)
3 siblings, 0 replies; 20+ messages in thread
From: Bruce Richardson @ 2026-06-09 16:13 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
To simplify querying data from multiple devices, e.g. across ethdevs, or
dmadevs, add a FOREACH command to the python script, allowing you to
run, e.g. /ethdev/list, and then run a second command for each item in
the list, gathering the relevant output values, optionally including an
index counter.
Simple examples are given in the documentation:
--> FOREACH /ethdev/list /ethdev/stats .opackets
[0, 0]
--> FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
[{"ipackets": 0, "opackets": 0}, {"ipackets": 0, "opackets": 0}]
--> FOREACH i /ethdev/list /ethdev/info,$i .name
[{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
--> FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
[{"i": 0, "ipackets": 0, "opackets": 0}, {"i": 1, "ipackets": 0, "opackets": 0}]
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
Acked-by: Chengwen Feng <fengchengwen@huawei.com>
---
doc/guides/howto/telemetry.rst | 42 +++++++++++
usertools/dpdk-telemetry.py | 128 ++++++++++++++++++++++++++++++++-
2 files changed, 167 insertions(+), 3 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index 0464c431fe..4bf48c635e 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -88,6 +88,48 @@ and query information using the telemetry client python script.
{"/help": {"/ethdev/xstats": "Returns the extended stats for a port.
Parameters: int port_id"}}
+ * Run a compound query using ``FOREACH``.
+
+ The ``FOREACH`` command runs a list command, iterates each returned item,
+ runs a second command for each item, and emits combined JSON output.
+
+ Start with the simplest form (no loop variable)::
+
+ FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
+
+ To include numbered output, use a loop variable::
+
+ FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
+
+ Notes:
+
+ - Field selectors are whitespace-separated tokens, each starting with ``.``.
+ - In no-variable mode, the iter command is called as ``/<iter_cmd>,<item>``.
+ - In loop-variable mode, use ``$<var>`` in the iter command where the
+ item value should be substituted.
+
+ Examples::
+
+ --> FOREACH /ethdev/list /ethdev/stats .opackets
+ [0, 0]
+
+ --> FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
+ [{"ipackets": 0, "opackets": 0}, {"ipackets": 0, "opackets": 0}]
+
+ --> FOREACH i /ethdev/list /ethdev/info,$i .name
+ [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
+
+ --> FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
+ [{"i": 0, "ipackets": 0, "opackets": 0}, {"i": 1, "ipackets": 0, "opackets": 0}]
+
+ Output behavior:
+
+ - Without loop variable and one field: returns an array of values.
+ - Without loop variable and multiple fields: returns an array of objects
+ containing named value fields.
+ - With loop variable: returns an array of objects containing the loop
+ variable field and requested value fields.
+
Connecting to Different DPDK Processes
--------------------------------------
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 09258a1f7e..2de10cff69 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -23,6 +23,130 @@
CMDS = []
+def send_command(sock, cmd, output_buf_len, echo=False, pretty=False):
+ """Send a telemetry command and return the parsed JSON reply"""
+ sock.send(cmd.encode())
+ return read_socket(sock, output_buf_len, echo, pretty)
+
+
+def get_cmd_payload(reply, cmd):
+ """Return the payload for a command response if present"""
+ if isinstance(reply, dict) and len(reply) == 1:
+ return next(iter(reply.values()))
+ return None
+
+
+def get_path_value(payload, path):
+ """Resolve a dotted path (e.g. '.name' or '.a.b') from a JSON payload"""
+ if not path:
+ return payload
+
+ keys = [k for k in path.lstrip(".").split(".") if k]
+ val = payload
+ for key in keys:
+ if not isinstance(val, dict) or key not in val:
+ return None
+ val = val[key]
+ return val
+
+
+def parse_selectors(selector_text):
+ """Parse whitespace-separated dotted selectors"""
+ selectors = selector_text.split()
+ if not selectors:
+ print("Invalid FOREACH syntax: missing selector")
+ return None
+ if any(not selector.startswith(".") for selector in selectors):
+ print("Invalid FOREACH syntax: selector must start with '.'")
+ return None
+ return selectors
+
+
+def parse_foreach(text):
+ """Parse FOREACH [<var>] /<cmd> /<parameterized cmd> .<value> [.<value> ...]"""
+ try:
+ tokens = text.split(None, 3)
+ except ValueError:
+ print("Invalid FOREACH syntax")
+ return None
+
+ if len(tokens) != 4:
+ print("Invalid FOREACH syntax")
+ return None
+
+ _, arg1, arg2, arg3 = tokens
+ if arg1.startswith("/"):
+ var_name = None
+ list_cmd = arg1
+ iter_cmd = arg2
+ selector_text = arg3
+ else:
+ var_name = arg1
+ list_cmd = arg2
+ try:
+ iter_cmd, selector_text = arg3.split(None, 1)
+ except ValueError:
+ print("Invalid FOREACH syntax")
+ return None
+
+ if not list_cmd.startswith("/") or not iter_cmd.startswith("/"):
+ print("Invalid FOREACH syntax: commands must start with '/'")
+ return None
+
+ selectors = parse_selectors(selector_text)
+ if selectors is None:
+ return None
+
+ return var_name, list_cmd, iter_cmd, selectors
+
+
+def build_foreach_result(item, var_name, payload, selectors):
+ """Build one FOREACH result entry based on selector count and index mode"""
+ values = {selector.lstrip("."): get_path_value(payload, selector) for selector in selectors}
+
+ if var_name is None and len(selectors) == 1:
+ return next(iter(values.values()))
+ if var_name is None:
+ return values
+
+ return {var_name: item, **values}
+
+
+def handle_foreach(sock, output_buf_len, text, pretty=False):
+ """Handle FOREACH queries and print telemetry-like JSON array output"""
+ parsed = parse_foreach(text)
+ if parsed is None:
+ return
+ var_name, list_cmd, iter_cmd, selectors = parsed
+
+ list_reply = send_command(sock, list_cmd, output_buf_len)
+ values = get_cmd_payload(list_reply, list_cmd)
+ if not isinstance(values, list):
+ print("FOREACH source command did not return a JSON array")
+ return
+
+ output = []
+ for item in values:
+ if var_name is None:
+ cmd = "{},{}".format(iter_cmd, item)
+ else:
+ cmd = iter_cmd.replace("$" + var_name, str(item))
+ item_reply = send_command(sock, cmd, output_buf_len)
+ item_payload = get_cmd_payload(item_reply, cmd)
+ output.append(build_foreach_result(item, var_name, item_payload, selectors))
+
+ indent = 2 if pretty else None
+ print(json.dumps(output, indent=indent))
+
+
+def handle_command(sock, output_buf_len, text, pretty=False):
+ """Execute a user command if recognized"""
+ if text.startswith("/"):
+ send_command(sock, text, output_buf_len, echo=True, pretty=pretty)
+ elif text.startswith("FOREACH "):
+ handle_foreach(sock, output_buf_len, text, pretty)
+
+
def read_socket(sock, buf_len, echo=True, pretty=False):
"""Read data from socket and return it in JSON format"""
reply = sock.recv(buf_len).decode()
@@ -140,9 +264,7 @@ def handle_socket(args, path):
try:
text = input(prompt).strip()
while text != "quit":
- if text.startswith("/"):
- sock.send(text.encode())
- read_socket(sock, output_buf_len, pretty=prompt)
+ handle_command(sock, output_buf_len, text, pretty=prompt)
text = input(prompt).strip()
except EOFError:
pass
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v3 2/3] usertools/telemetry: support using aliases for long commands
2026-06-09 16:13 ` [PATCH v3 0/3] extend interactive telemetry script Bruce Richardson
2026-06-09 16:13 ` [PATCH v3 1/3] usertools/telemetry: add a FOREACH command Bruce Richardson
@ 2026-06-09 16:13 ` Bruce Richardson
2026-07-12 10:12 ` Thomas Monjalon
2026-06-09 16:14 ` [PATCH v3 3/3] usertools/telemetry: add help support Bruce Richardson
2026-06-10 1:39 ` [PATCH v3 0/3] extend interactive telemetry script fengchengwen
3 siblings, 1 reply; 20+ messages in thread
From: Bruce Richardson @ 2026-06-09 16:13 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
Similarly to how shell aliases work, allow specifying of short alias
commands for dpdk-telemetry.py script. The aliases are read from
"$HOME/.dpdk_telemetry_aliases" at startup.
Some examples of use from the docs. Alias file contents:
# Basic shortcuts
ls=/ethdev/list
names=FOREACH i /ethdev/list /ethdev/info,$i .name
q=quit
Alias use:
--> ls
{"/ethdev/list": [0, 1]}
--> names
[{"i": 0, "name": "0000:
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
v2: added support for providing an alias path on cmdline.
added summary printout of how many aliases were loaded.
---
doc/guides/howto/telemetry.rst | 35 ++++++++++++++
usertools/dpdk-telemetry.py | 85 ++++++++++++++++++++++++++++++++--
2 files changed, 116 insertions(+), 4 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index 4bf48c635e..bdefbdc6a6 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -130,6 +130,41 @@ and query information using the telemetry client python script.
- With loop variable: returns an array of objects containing the loop
variable field and requested value fields.
+ * Use command aliases.
+
+ The telemetry script can load aliases at startup from::
+
+ $HOME/.dpdk_telemetry_aliases
+
+ or from a custom path provided via the ``--alias-file`` script flag.
+ Each alias entry must be in ``alias=command`` format.
+ Empty lines and lines starting with ``#`` are ignored.
+
+ Example alias file::
+
+ # Basic shortcuts
+ ls=/ethdev/list
+ names=FOREACH i /ethdev/list /ethdev/info,$i .name
+ q=quit
+
+ Alias behavior is intentionally similar to shell aliases:
+
+ - The first token of the entered input is checked for an alias match.
+ - If matched, that first token is replaced with its expansion.
+ - Alias expansion is recursive (aliases can expand to other aliases).
+ - Expansion has a safety limit to prevent infinite loops.
+
+ Examples::
+
+ --> ls
+ {"/ethdev/list": [0, 1]}
+
+ --> names
+ [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
+
+ --> q
+ # exits the client
+
Connecting to Different DPDK Processes
--------------------------------------
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 2de10cff69..7a25b78730 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -21,6 +21,76 @@
SOCKET_NAME = "dpdk_telemetry.{}".format(TELEMETRY_VERSION)
DEFAULT_PREFIX = "rte"
CMDS = []
+ALIASES = {}
+ALIAS_FILE = ".dpdk_telemetry_aliases"
+MAX_ALIAS_EXPANSIONS = 32
+
+
+def load_aliases(alias_path=None):
+ """Load aliases from $HOME/.dpdk_telemetry_aliases or a custom path if provided"""
+ aliases = {}
+ if alias_path and not os.path.isfile(alias_path):
+ print("Warning: alias file {} not found, skipping".format(alias_path), file=sys.stderr)
+ return aliases
+
+ if not alias_path:
+ home = os.environ.get("HOME")
+ if not home:
+ return aliases
+
+ alias_path = os.path.join(home, ALIAS_FILE)
+ if not os.path.isfile(alias_path):
+ return aliases
+
+ try:
+ with open(alias_path) as alias_file:
+ for line_num, line in enumerate(alias_file, start=1):
+ entry = line.strip()
+ if not entry or entry.startswith("#"):
+ continue
+ if "=" not in entry:
+ print(
+ "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
+ file=sys.stderr,
+ )
+ continue
+ name, command = entry.split("=", 1)
+ name = name.strip()
+ command = command.strip()
+ if not name or not command:
+ print(
+ "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
+ file=sys.stderr,
+ )
+ continue
+ aliases[name] = command
+ except OSError as e:
+ print("Warning: failed to read {}: {}".format(alias_path, e), file=sys.stderr)
+
+ print("Loaded {} aliases from {}".format(len(aliases), alias_path))
+ return aliases
+
+
+def expand_aliases(text, aliases):
+ """Expand aliases similarly to shell aliases on the first token"""
+ expanded = text
+ for _ in range(MAX_ALIAS_EXPANSIONS):
+ stripped = expanded.lstrip()
+ if not stripped:
+ return expanded
+
+ parts = stripped.split(None, 1)
+ first = parts[0]
+ rest = parts[1] if len(parts) > 1 else ""
+
+ if first not in aliases:
+ return expanded
+
+ alias_value = aliases[first]
+ expanded = "{} {}".format(alias_value, rest).strip() if rest else alias_value
+
+ print("Warning: alias expansion limit reached", file=sys.stderr)
+ return expanded
def send_command(sock, cmd, output_buf_len, echo=False, pretty=False):
@@ -262,10 +332,12 @@ def handle_socket(args, path):
# interactive prompt
try:
- text = input(prompt).strip()
- while text != "quit":
- handle_command(sock, output_buf_len, text, pretty=prompt)
+ while True:
text = input(prompt).strip()
+ expanded = expand_aliases(text, ALIASES)
+ if expanded == "quit":
+ break
+ handle_command(sock, output_buf_len, expanded, pretty=prompt)
except EOFError:
pass
finally:
@@ -274,7 +346,7 @@ def handle_socket(args, path):
def readline_complete(text, state):
"""Find any matching commands from the list based on user input"""
- all_cmds = ["quit"] + CMDS
+ all_cmds = ["quit"] + list(ALIASES.keys()) + CMDS
if text:
matches = [c for c in all_cmds if c.startswith(text)]
else:
@@ -293,6 +365,10 @@ def readline_complete(text, state):
default=DEFAULT_PREFIX,
help="Provide file-prefix for DPDK runtime directory",
)
+parser.add_argument(
+ "--alias-file",
+ help=f"Provide a custom alias file instead of $HOME/{ALIAS_FILE}",
+)
parser.add_argument(
"-i", "--instance", default="0", type=int, help="Provide instance number for DPDK application"
)
@@ -304,6 +380,7 @@ def readline_complete(text, state):
help="List all possible file-prefixes and exit",
)
args = parser.parse_args()
+ALIASES = load_aliases(args.alias_file if args.alias_file else None)
if args.list:
list_fp()
sys.exit(0)
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v3 3/3] usertools/telemetry: add help support
2026-06-09 16:13 ` [PATCH v3 0/3] extend interactive telemetry script Bruce Richardson
2026-06-09 16:13 ` [PATCH v3 1/3] usertools/telemetry: add a FOREACH command Bruce Richardson
2026-06-09 16:13 ` [PATCH v3 2/3] usertools/telemetry: support using aliases for long commands Bruce Richardson
@ 2026-06-09 16:14 ` Bruce Richardson
2026-06-09 16:17 ` Bruce Richardson
2026-06-10 1:39 ` [PATCH v3 0/3] extend interactive telemetry script fengchengwen
3 siblings, 1 reply; 20+ messages in thread
From: Bruce Richardson @ 2026-06-09 16:14 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
While the telemetry infrastructure supported using "/help,/<cmd>" to get
help on specific commands, with the addition of script-specific
commands, we needed better help support to explain the use of FOREACH,
and to allow e.g. "help /<cmd>" using space separation, which is more
intuitive. This patch adds that help support.
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
v2: added "help aliases" to list defined aliases
updated docs for expanded help command
---
doc/guides/howto/telemetry.rst | 31 ++++++++++++---
usertools/dpdk-telemetry.py | 69 +++++++++++++++++++++++++++++++++-
2 files changed, 93 insertions(+), 7 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index bdefbdc6a6..00cfc1a1e1 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -81,12 +81,31 @@ and query information using the telemetry client python script.
...
"tx_priority7_xon_to_xoff_packets": 0}}
- * Get the help text for a command. This will indicate what parameters are
- required. Pass the command as a parameter::
-
- --> /help,/ethdev/xstats
- {"/help": {"/ethdev/xstats": "Returns the extended stats for a port.
- Parameters: int port_id"}}
+ * Get the help text for a command.
+ This will indicate what parameters are required.
+ Use the ``help`` keyword followed by the command or keyword of interest,
+ for example::
+
+ --> help FOREACH
+ FOREACH usage:
+ FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
+ FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
+
+ Examples:
+ FOREACH /ethdev/list /ethdev/stats .opackets
+ ...
+
+ --> help /ethdev/xstats
+ {"/help": {"/ethdev/xstats": "Returns the extended stats for a port. Parameters: int port_id"}}
+
+ .. Note::
+ For commands starting with ``/`` that are telemetry enpoints,
+ the help text can also be obtained by sending the ``/help`` command to the telemetry socket.
+ In this case, the parameter must be separated by a comma, not a space.
+ For example::
+
+ --> /help,/ethdev/xstats
+ {"/help": {"/ethdev/xstats": "Returns the extended stats for a port. Parameters: int port_id"}}
* Run a compound query using ``FOREACH``.
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 7a25b78730..20627b596b 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -25,6 +25,26 @@
ALIAS_FILE = ".dpdk_telemetry_aliases"
MAX_ALIAS_EXPANSIONS = 32
+BASIC_HELP_TEXT = """Basic usage:
+ /<command>[,<params>] Send a telemetry command to the app
+ FOREACH ... Run a compound query over list items
+ help Show this help
+ help /<command> Show app-provided help for a command
+ help FOREACH Show FOREACH usage and examples
+ quit Exit the client
+"""
+
+FOREACH_HELP_TEXT = """FOREACH usage:
+ FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
+ FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
+
+Examples:
+ FOREACH /ethdev/list /ethdev/stats .opackets
+ FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
+ FOREACH i /ethdev/list /ethdev/info,$i .name
+ FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
+"""
+
def load_aliases(alias_path=None):
"""Load aliases from $HOME/.dpdk_telemetry_aliases or a custom path if provided"""
@@ -209,10 +229,57 @@ def handle_foreach(sock, output_buf_len, text, pretty=False):
print(json.dumps(output, indent=indent))
+def command_exists(cmd):
+ """Check if a telemetry command exists in the command list"""
+ return cmd in CMDS
+
+
+def app_help_command_for(target_cmd):
+ """Build a '/help,<command>' query for app-side command help"""
+ if not target_cmd:
+ return None
+ normalized = target_cmd.strip()
+ if not normalized.startswith("/"):
+ return None
+ if not command_exists(normalized):
+ print("Unknown command for help: {}".format(normalized))
+ return None
+ return "/help,{}".format(normalized)
+
+
+def handle_user_help(sock, output_buf_len, text, pretty=False):
+ """Handle local 'help' command and command-specific help lookup"""
+ parts = text.split(None, 1)
+ if len(parts) == 1:
+ print(BASIC_HELP_TEXT, end="")
+ return
+
+ help_arg = parts[1].strip()
+ if help_arg.upper() == "FOREACH":
+ print(FOREACH_HELP_TEXT, end="")
+ return
+ elif help_arg.lower() == "alias" or help_arg.lower() == "aliases":
+ if not ALIASES:
+ print("No aliases defined")
+ return
+ print("Defined aliases:")
+ for name, command in ALIASES.items():
+ print(f" {name}='{command}'")
+ return
+
+ cmd = app_help_command_for(help_arg)
+ if cmd is None:
+ print("Usage: help [FOREACH|/<command>]")
+ return
+ send_command(sock, cmd, output_buf_len, echo=True, pretty=pretty)
+
+
def handle_command(sock, output_buf_len, text, pretty=False):
"""Execute a user command if recognized"""
if text.startswith("/"):
send_command(sock, text, output_buf_len, echo=True, pretty=pretty)
+ elif text == "help" or text.startswith("help "):
+ handle_user_help(sock, output_buf_len, text, pretty)
elif text.startswith("FOREACH "):
handle_foreach(sock, output_buf_len, text, pretty)
@@ -346,7 +413,7 @@ def handle_socket(args, path):
def readline_complete(text, state):
"""Find any matching commands from the list based on user input"""
- all_cmds = ["quit"] + list(ALIASES.keys()) + CMDS
+ all_cmds = ["quit", "help"] + list(ALIASES.keys()) + CMDS
if text:
matches = [c for c in all_cmds if c.startswith(text)]
else:
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* Re: [PATCH v3 3/3] usertools/telemetry: add help support
2026-06-09 16:14 ` [PATCH v3 3/3] usertools/telemetry: add help support Bruce Richardson
@ 2026-06-09 16:17 ` Bruce Richardson
0 siblings, 0 replies; 20+ messages in thread
From: Bruce Richardson @ 2026-06-09 16:17 UTC (permalink / raw)
To: dev; +Cc: fengchengwen
On Tue, Jun 09, 2026 at 05:14:00PM +0100, Bruce Richardson wrote:
> While the telemetry infrastructure supported using "/help,/<cmd>" to get
> help on specific commands, with the addition of script-specific
> commands, we needed better help support to explain the use of FOREACH,
> and to allow e.g. "help /<cmd>" using space separation, which is more
> intuitive. This patch adds that help support.
>
> Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
> ---
> v2: added "help aliases" to list defined aliases
> updated docs for expanded help command
> ---
> doc/guides/howto/telemetry.rst | 31 ++++++++++++---
> usertools/dpdk-telemetry.py | 69 +++++++++++++++++++++++++++++++++-
> 2 files changed, 93 insertions(+), 7 deletions(-)
>
> diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
> index bdefbdc6a6..00cfc1a1e1 100644
> --- a/doc/guides/howto/telemetry.rst
> +++ b/doc/guides/howto/telemetry.rst
> @@ -81,12 +81,31 @@ and query information using the telemetry client python script.
> ...
> "tx_priority7_xon_to_xoff_packets": 0}}
>
> - * Get the help text for a command. This will indicate what parameters are
> - required. Pass the command as a parameter::
> -
> - --> /help,/ethdev/xstats
> - {"/help": {"/ethdev/xstats": "Returns the extended stats for a port.
> - Parameters: int port_id"}}
> + * Get the help text for a command.
> + This will indicate what parameters are required.
> + Use the ``help`` keyword followed by the command or keyword of interest,
> + for example::
> +
> + --> help FOREACH
> + FOREACH usage:
> + FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
> + FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
> +
> + Examples:
> + FOREACH /ethdev/list /ethdev/stats .opackets
> + ...
> +
> + --> help /ethdev/xstats
> + {"/help": {"/ethdev/xstats": "Returns the extended stats for a port. Parameters: int port_id"}}
> +
> + .. Note::
> + For commands starting with ``/`` that are telemetry enpoints,
Checkpatch in CI correctly points out the typo for "endpoints" here. Please
fix on apply if no v4 is otherwise necessary.
thanks,
/Bruce
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v3 0/3] extend interactive telemetry script
2026-06-09 16:13 ` [PATCH v3 0/3] extend interactive telemetry script Bruce Richardson
` (2 preceding siblings ...)
2026-06-09 16:14 ` [PATCH v3 3/3] usertools/telemetry: add help support Bruce Richardson
@ 2026-06-10 1:39 ` fengchengwen
2026-07-12 14:23 ` Thomas Monjalon
3 siblings, 1 reply; 20+ messages in thread
From: fengchengwen @ 2026-06-10 1:39 UTC (permalink / raw)
To: Bruce Richardson, dev
Series-acked-by: Chengwen Feng <fengchengwen@huawei.com>
On 6/10/2026 12:13 AM, Bruce Richardson wrote:
> To simplify interactive telemetry script for general use, i.e. not from
> other scripts, we can add two new features to it:
>
> 1. Support for FOREACH to allow gathering a set of output values across
> a list of ports or devices, e.g. ethdevs or rawdevs.
> 2. Support having predefined aliases in a file in the user's home
> directory to simplify the use of more complicated FOREACH commands.
>
> Putting these together, we can create new commands such as "eth_names".
>
> bruce@host:$ cat ~/.dpdk_telemetry_aliases
> eth_names=FOREACH index /ethdev/list /ethdev/info,$index .name
>
> bruce@host:$ echo eth_names | ./usertools/dpdk-telemetry.py | jq
> [
> {
> "index": 0,
> "name": "0000:16:00.0"
> },
> {
> "index": 1,
> "name": "0000:16:00.1"
> }
> ]
>
> ---
> v3: updated based on review feedback from Chengwen:
> - added arg to override alias file
> - printed one-line summary of alias count loaded
> - improved doc for "help" command
> - added "help alias" to list aliases.
> v2: added third patch with "help" command giving more details on
> how to use the various commands.
>
> Bruce Richardson (3):
> usertools/telemetry: add a FOREACH command
> usertools/telemetry: support using aliases for long commands
> usertools/telemetry: add help support
>
> doc/guides/howto/telemetry.rst | 106 ++++++++++++-
> usertools/dpdk-telemetry.py | 278 ++++++++++++++++++++++++++++++++-
> 2 files changed, 373 insertions(+), 11 deletions(-)
>
> --
> 2.53.0
>
>
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v3 2/3] usertools/telemetry: support using aliases for long commands
2026-06-09 16:13 ` [PATCH v3 2/3] usertools/telemetry: support using aliases for long commands Bruce Richardson
@ 2026-07-12 10:12 ` Thomas Monjalon
0 siblings, 0 replies; 20+ messages in thread
From: Thomas Monjalon @ 2026-07-12 10:12 UTC (permalink / raw)
To: Bruce Richardson; +Cc: dev, fengchengwen
09/06/2026 18:13, Bruce Richardson:
> Similarly to how shell aliases work, allow specifying of short alias
> commands for dpdk-telemetry.py script. The aliases are read from
> "$HOME/.dpdk_telemetry_aliases" at startup.
I don't like polluting the home directory of the user.
We already have these files and directories:
~/.config/dpdk/devel.config
~/.dpdk_telemetry_aliases
~/dpdk-traces/
~/dpdk_graph_pcap_capture_XXXXXX.pcapng
/usr/share/dpdk/telemetry-endpoints
I think ~/.config/dpdk/ is the best place for our config files.
It is not blocking the series but we should work on unifying these files.
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v3 0/3] extend interactive telemetry script
2026-06-10 1:39 ` [PATCH v3 0/3] extend interactive telemetry script fengchengwen
@ 2026-07-12 14:23 ` Thomas Monjalon
0 siblings, 0 replies; 20+ messages in thread
From: Thomas Monjalon @ 2026-07-12 14:23 UTC (permalink / raw)
To: Bruce Richardson; +Cc: dev, fengchengwen
> > Bruce Richardson (3):
> > usertools/telemetry: add a FOREACH command
> > usertools/telemetry: support using aliases for long commands
> > usertools/telemetry: add help support
>
> Series-acked-by: Chengwen Feng <fengchengwen@huawei.com>
Applied, thanks.
^ permalink raw reply [flat|nested] 20+ messages in thread
end of thread, other threads:[~2026-07-12 14:24 UTC | newest]
Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-21 15:39 [PATCH 0/2] extend interactive telemetry script Bruce Richardson
2026-05-21 15:39 ` [PATCH 1/2] usertools/telemetry: add a FOREACH command Bruce Richardson
2026-05-21 15:39 ` [PATCH 2/2] usertools/telemetry: support using aliases for long commands Bruce Richardson
2026-05-22 0:44 ` [PATCH 0/2] extend interactive telemetry script fengchengwen
2026-05-22 7:51 ` Bruce Richardson
2026-05-22 13:37 ` [PATCH v2 0/3] " Bruce Richardson
2026-05-22 13:37 ` [PATCH v2 1/3] usertools/telemetry: add a FOREACH command Bruce Richardson
2026-05-29 0:17 ` fengchengwen
2026-05-22 13:37 ` [PATCH v2 2/3] usertools/telemetry: support using aliases for long commands Bruce Richardson
2026-05-29 0:25 ` fengchengwen
2026-05-22 13:37 ` [PATCH v2 3/3] usertools/telemetry: add help support Bruce Richardson
2026-05-29 0:27 ` fengchengwen
2026-06-09 16:13 ` [PATCH v3 0/3] extend interactive telemetry script Bruce Richardson
2026-06-09 16:13 ` [PATCH v3 1/3] usertools/telemetry: add a FOREACH command Bruce Richardson
2026-06-09 16:13 ` [PATCH v3 2/3] usertools/telemetry: support using aliases for long commands Bruce Richardson
2026-07-12 10:12 ` Thomas Monjalon
2026-06-09 16:14 ` [PATCH v3 3/3] usertools/telemetry: add help support Bruce Richardson
2026-06-09 16:17 ` Bruce Richardson
2026-06-10 1:39 ` [PATCH v3 0/3] extend interactive telemetry script fengchengwen
2026-07-12 14:23 ` Thomas Monjalon
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox