jmri_server.services.logs
LogsService — JSON topic logs.
Tails JMRI_LOG_FILE (when configured) so scripts and the /home page
can surface recent activity without shelling into the Pi. There's no
JMRI-Java precedent; the wire shape is ours to define.
Wire shape (single-object, no list)::
{
"path": "/var/log/jmri/jmri.log", // or null when not configured
"tail": 20, // number of lines returned
"lines": ["2026-08-04 15:03:07 INFO [jmri_nce.connection] ...", ...]
}
Request-side: method=get, optional data.tail (default 50,
cap 500). Missing log file returns lines=[] with a path=null
so clients see the "not configured" state explicitly instead of a
generic 404.
1"""``LogsService`` — JSON topic ``logs``. 2 3Tails ``JMRI_LOG_FILE`` (when configured) so scripts and the /home page 4can surface recent activity without shelling into the Pi. There's no 5JMRI-Java precedent; the wire shape is ours to define. 6 7Wire shape (single-object, no ``list``):: 8 9 { 10 "path": "/var/log/jmri/jmri.log", // or null when not configured 11 "tail": 20, // number of lines returned 12 "lines": ["2026-08-04 15:03:07 INFO [jmri_nce.connection] ...", ...] 13 } 14 15Request-side: ``method=get``, optional ``data.tail`` (default 50, 16cap 500). Missing log file returns ``lines=[]`` with a ``path=null`` 17so clients see the "not configured" state explicitly instead of a 18generic 404. 19""" 20from __future__ import annotations 21 22from typing import Any 23 24from jmri_core.log import tail as _tail 25 26from jmri_server.service import JsonService 27 28_DEFAULT_TAIL = 50 29_MAX_TAIL = 500 30 31 32class LogsService(JsonService): 33 topic = "logs" 34 35 async def on_get(self, data: dict[str, Any] | None) -> dict[str, Any]: 36 data = data or {} 37 try: 38 requested = int(data.get("tail", _DEFAULT_TAIL)) 39 except (TypeError, ValueError): 40 requested = _DEFAULT_TAIL 41 n = max(0, min(requested, _MAX_TAIL)) 42 43 path = getattr(self.ctx, "_log_file", None) 44 if path is None: 45 return {"path": None, "tail": n, "lines": []} 46 return { 47 "path": str(path), 48 "tail": n, 49 "lines": _tail(path, n), 50 }
33class LogsService(JsonService): 34 topic = "logs" 35 36 async def on_get(self, data: dict[str, Any] | None) -> dict[str, Any]: 37 data = data or {} 38 try: 39 requested = int(data.get("tail", _DEFAULT_TAIL)) 40 except (TypeError, ValueError): 41 requested = _DEFAULT_TAIL 42 n = max(0, min(requested, _MAX_TAIL)) 43 44 path = getattr(self.ctx, "_log_file", None) 45 if path is None: 46 return {"path": None, "tail": n, "lines": []} 47 return { 48 "path": str(path), 49 "tail": n, 50 "lines": _tail(path, n), 51 }
Base class for a topic's service handlers.
Subclasses set topic and override the methods they support;
unsupported methods raise 405 by default. Subclasses can also
override REQUIRED_ROLE to change per-method role gates.
async def
on_get(self, data: dict[str, typing.Any] | None) -> dict[str, typing.Any]:
36 async def on_get(self, data: dict[str, Any] | None) -> dict[str, Any]: 37 data = data or {} 38 try: 39 requested = int(data.get("tail", _DEFAULT_TAIL)) 40 except (TypeError, ValueError): 41 requested = _DEFAULT_TAIL 42 n = max(0, min(requested, _MAX_TAIL)) 43 44 path = getattr(self.ctx, "_log_file", None) 45 if path is None: 46 return {"path": None, "tail": n, "lines": []} 47 return { 48 "path": str(path), 49 "tail": n, 50 "lines": _tail(path, n), 51 }