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        }
class LogsService(jmri_server.service.JsonService):
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.

topic = 'logs'
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        }