Skip to content

Commit 3771ae5

Browse files
committed
exporter: Allow logging of serial traffic
Lab admins running shared boards have no independent record of what is happening on the serial console. The per-client --logfile option captures output for the user who set it, but the lab owner has no way to see traffic across all sessions, which hampers post-mortem when a board ends up in a bad state and the question is which commands got it there. Add support for centralised serial-traffic logging on the exporter host. When LG_SERIAL_TRACE_DIR is set, the exporter passes ser2net's trace-both option for each acquire, capturing every byte that flows in either direction to a per-board, per-user file under that directory. ser2net is started fresh on each resource acquire and stopped on release, so each instance can include both the board (group name) and the acquiring user in the filename: <board>-<user>.log (e.g. bbb-okaro_sjg.log). Repeated acquires by the same user on the same board append to the same file. The user identity comes from the coordinator via the new 'user' field in ExporterSetAcquiredRequest (added in a previous commit). The group name is plumbed from the resource config through add_resource() into the ResourceExport so the trace path can use the human-readable board name instead of the device path. trace-both-timestamp is enabled so each line is timestamped, which makes correlating with other audit logs straightforward. Signed-off-by: Simon Glass <sjg@chromium.org> Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 37871dd commit 3771ae5

5 files changed

Lines changed: 174 additions & 3 deletions

File tree

doc/man/exporter.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,29 @@ for more information.
7777

7878
ENVIRONMENT VARIABLES
7979
---------------------
80-
The following environment variable can be used to configure labgrid-exporter.
80+
The following environment variables can be used to configure
81+
labgrid-exporter.
8182

8283
LG_COORDINATOR
8384
~~~~~~~~~~~~~~
8485
This variable can be used to set the default coordinator in the format
8586
``HOST[:PORT]`` (instead of using the ``-x`` option).
8687

88+
LG_SERIAL_TRACE_DIR
89+
~~~~~~~~~~~~~~~~~~~
90+
When set, the exporter records all serial-port traffic for each
91+
acquired resource into ``<LG_SERIAL_TRACE_DIR>/<board>-<user>.log``,
92+
where ``<board>`` is the resource group name and ``<user>`` is the
93+
acquiring user identity reported by the coordinator (in
94+
``host/user`` form, with ``/`` rewritten to ``_``). Both directions
95+
are captured and each line is timestamped, giving the lab admin an
96+
independent record of every session that is not affected by
97+
per-client logging options.
98+
99+
The directory is created on demand. A fresh ser2net instance is
100+
started for each acquire, so repeated acquires by the same user on
101+
the same board append to the same file.
102+
87103
EXAMPLES
88104
--------
89105

doc/usage.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,43 @@ allocated before returning.
166166
A reservation will time out after a short time, if it is neither refreshed nor
167167
used by locked places.
168168

169+
Logging Serial Traffic on the Exporter
170+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
171+
172+
When several developers (or CI jobs) share the same boards through a
173+
single coordinator, it is often useful for the lab admin to keep an
174+
independent record of every serial-console session. The per-client
175+
``--logfile`` option only records output for the user who set it, so
176+
it does not help the operator answer questions like *which commands
177+
left this board in a bad state?* after the fact.
178+
179+
To enable a centralised log on the exporter host, set the
180+
``LG_SERIAL_TRACE_DIR`` environment variable before starting
181+
``labgrid-exporter``:
182+
183+
.. code-block:: bash
184+
185+
$ export LG_SERIAL_TRACE_DIR=/var/log/labgrid/serial
186+
$ labgrid-exporter my-config.yaml
187+
188+
For each acquire, the exporter then asks ``ser2net`` to record both
189+
sides of the serial connection to a file named ``<board>-<user>.log``
190+
under that directory, where ``<board>`` is the resource group name
191+
and ``<user>`` is the host/user identity reported by the coordinator
192+
(slashes are rewritten to underscores so the result is filesystem
193+
safe). Each line is timestamped, which makes it easy to correlate a
194+
trace with other audit logs.
195+
196+
Because ``ser2net`` is started fresh on each acquire and stopped on
197+
release, the trace covers exactly one session per acquire. Repeated
198+
acquires by the same user on the same board append to the same file,
199+
so a long-running developer ends up with a single, continuous log per
200+
board.
201+
202+
This feature complements rather than replaces the per-client
203+
``--logfile``: the client log is what the developer sees in their
204+
terminal, the exporter trace is the operator's independent record.
205+
169206
Library
170207
-------
171208
labgrid can be used directly as a Python library, without the infrastructure

labgrid/remote/exporter.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class ResourceExport(ResourceEntry):
6767
host = attr.ib(default=gethostname(), validator=attr.validators.instance_of(str))
6868
proxy = attr.ib(default=None)
6969
proxy_required = attr.ib(default=False)
70+
group_name = attr.ib(default="")
7071
user = attr.ib(default=None, init=False)
7172
local = attr.ib(init=False)
7273
local_params = attr.ib(init=False)
@@ -231,6 +232,34 @@ def _get_params(self):
231232
},
232233
}
233234

235+
@staticmethod
236+
def _build_trace_args(group_name, user, path):
237+
"""Return ser2net YAML args for trace logging, or [] if disabled
238+
239+
Reads LG_SERIAL_TRACE_DIR; when set, creates the directory and
240+
builds a per-board, per-user file path under it, then returns
241+
the YAML option pairs needed to enable trace-both for that
242+
file. The board comes from the resource's group name when
243+
available, falling back to the basename of the device path so
244+
the filename is still meaningful. The user is the host/user
245+
identity passed in by the coordinator (slashes are rewritten
246+
to underscores so it is filesystem-safe), or ``unknown`` when
247+
the coordinator did not send one.
248+
"""
249+
trace_dir = os.environ.get("LG_SERIAL_TRACE_DIR")
250+
if not trace_dir:
251+
return []
252+
os.makedirs(trace_dir, exist_ok=True)
253+
board = group_name or os.path.basename(path)
254+
user_label = (user or "unknown").replace("/", "_")
255+
trace_path = os.path.join(trace_dir, f"{board}-{user_label}.log")
256+
return [
257+
"-Y",
258+
f" trace-both: {trace_path}",
259+
"-Y",
260+
" trace-both-timestamp: true",
261+
]
262+
234263
def _start(self, start_params):
235264
"""Start ``ser2net`` subprocess"""
236265
assert self.local.avail
@@ -264,6 +293,12 @@ def _start(self, start_params):
264293
"-Y",
265294
" max-connections: 10",
266295
]
296+
# If LG_SERIAL_TRACE_DIR is set, ask ser2net to log all
297+
# serial traffic for this device. Useful for centralised
298+
# audit on the exporter host. ser2net is started fresh
299+
# on each acquire and stopped on release, so the trace
300+
# file scope is one acquire session.
301+
cmd += self._build_trace_args(self.group_name, self.user, start_params["path"])
267302
else:
268303
cmd = [
269304
self.ser2net_bin,
@@ -1030,7 +1065,11 @@ async def add_resource(self, group_name, resource_name, cls, params):
10301065
proxy_req = self.isolated
10311066
if issubclass(export_cls, ResourceExport):
10321067
res = group[resource_name] = export_cls(
1033-
config, host=self.hostname, proxy=getfqdn(), proxy_required=proxy_req
1068+
config,
1069+
host=self.hostname,
1070+
proxy=getfqdn(),
1071+
proxy_required=proxy_req,
1072+
group_name=group_name,
10341073
)
10351074
res.poll()
10361075
else:

man/labgrid-exporter.1

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,26 @@ See <\X'tty: link https://labgrid.readthedocs.io/en/latest/configuration.html#ex
100100
for more information.
101101
.SS ENVIRONMENT VARIABLES
102102
.sp
103-
The following environment variable can be used to configure labgrid\-exporter.
103+
The following environment variables can be used to configure
104+
labgrid\-exporter.
104105
.SS LG_COORDINATOR
105106
.sp
106107
This variable can be used to set the default coordinator in the format
107108
\fBHOST[:PORT]\fP (instead of using the \fB\-x\fP option).
109+
.SS LG_SERIAL_TRACE_DIR
110+
.sp
111+
When set, the exporter records all serial\-port traffic for each
112+
acquired resource into \fB<LG_SERIAL_TRACE_DIR>/<board>\-<user>.log\fP,
113+
where \fB<board>\fP is the resource group name and \fB<user>\fP is the
114+
acquiring user identity reported by the coordinator (in
115+
\fBhost/user\fP form, with \fB/\fP rewritten to \fB_\fP). Both directions
116+
are captured and each line is timestamped, giving the lab admin an
117+
independent record of every session that is not affected by
118+
per\-client logging options.
119+
.sp
120+
The directory is created on demand. A fresh ser2net instance is
121+
started for each acquire, so repeated acquires by the same user on
122+
the same board append to the same file.
108123
.SS EXAMPLES
109124
.sp
110125
Start the exporter with the configuration file \fBmy\-config.yaml\fP:

tests/test_exporter_trace.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Tests for the LG_SERIAL_TRACE_DIR feature in labgrid-exporter."""
2+
3+
import pytest
4+
5+
from labgrid.remote.exporter import SerialPortExport
6+
7+
8+
@pytest.fixture
9+
def trace_args(monkeypatch):
10+
"""Return a builder bound to whatever LG_SERIAL_TRACE_DIR is set."""
11+
12+
def _build(group_name="bbb", user="okaro/sjg", path="/dev/ttyUSB0"):
13+
return SerialPortExport._build_trace_args(group_name, user, path)
14+
15+
return _build
16+
17+
18+
def test_returns_empty_when_env_unset(trace_args, monkeypatch):
19+
monkeypatch.delenv("LG_SERIAL_TRACE_DIR", raising=False)
20+
assert trace_args() == []
21+
22+
23+
def test_builds_yaml_args(trace_args, monkeypatch, tmp_path):
24+
monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path))
25+
args = trace_args(group_name="bbb", user="okaro/sjg")
26+
assert args == [
27+
"-Y",
28+
f" trace-both: {tmp_path}/bbb-okaro_sjg.log",
29+
"-Y",
30+
" trace-both-timestamp: true",
31+
]
32+
33+
34+
def test_creates_missing_directory(trace_args, monkeypatch, tmp_path):
35+
target = tmp_path / "newdir"
36+
assert not target.exists()
37+
monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(target))
38+
trace_args()
39+
assert target.is_dir()
40+
41+
42+
def test_user_slashes_rewritten(trace_args, monkeypatch, tmp_path):
43+
monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path))
44+
args = trace_args(user="myhost/alice")
45+
assert args[1] == f" trace-both: {tmp_path}/bbb-myhost_alice.log"
46+
47+
48+
def test_unknown_user_when_none(trace_args, monkeypatch, tmp_path):
49+
monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path))
50+
args = trace_args(user=None)
51+
assert args[1] == f" trace-both: {tmp_path}/bbb-unknown.log"
52+
53+
54+
def test_falls_back_to_path_basename(trace_args, monkeypatch, tmp_path):
55+
monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path))
56+
args = trace_args(group_name="", path="/dev/ttyUSB7")
57+
assert args[1] == f" trace-both: {tmp_path}/ttyUSB7-okaro_sjg.log"
58+
59+
60+
def test_existing_dir_is_reused(trace_args, monkeypatch, tmp_path):
61+
"""If the dir already exists, makedirs(exist_ok=True) must not fail."""
62+
monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path))
63+
trace_args()
64+
trace_args() # second call would error if exist_ok wasn't honoured

0 commit comments

Comments
 (0)