DPDK-dev Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [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