Skip to content

src/genro_asgi/applications/server_sections/monitor_section.py

Source from this local checkout, regenerated when the reader rebuilds. Line links use #L<number>; a GitHub line range opens its first line.

1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 #     https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """The ``_server/monitor`` section: the live view of the running server.16 17 ``MonitorSection`` is a ``RoutingClass`` the ``ServerApplication`` attaches18 under ``monitor``, so the whole monitor lives at one address:19 20     ``/_server/monitor/``           the page (``index``, the shell)21     ``/_server/monitor/snapshot``   the polled data22     ``/_server/monitor/panels``     the panel descriptors, fetched once23     ``/_server/monitor/panel``      one contributor's own panel module24 25 Every route is gated ``auth_rule="SERVER_ADMIN"``: the monitor exposes the26 whole server, so it is closed to anyone the operator has not admitted.27 28 The shell composes ONE panel per mounted application. What an app IS at this29 instant comes from its ``app_snapshot`` (polled, aggregated here under30 ``apps``); WHO draws it comes from its ``app_panel`` (a class constant, so it31 is fetched once at load). Both are inherited from ``BaseApplication``, so an32 app that declares nothing still shows up — rendered by the generic panel, its33 raw snapshot as key/value rows and tables.34 35 An app whose panel the shell does not know SHIPS it: the optional36 ``panel_source`` hands over the ES module as text, and ``panels`` fills the37 descriptor's ``src`` with this section's ``panel`` route. So a panel travels38 with the app that needs it — an application installed from another39 distribution publishes no route and writes nothing into the core.40 41 Two kinds of contributor are aggregated:42 43 - the mounted applications, keyed by their mount (the ``_server`` app itself44   is left out: this section IS its monitor face);45 - the system sections that declare the same two names, keyed ``_server/<name>``46   — a section is not an application, so it opts in by declaring them rather47   than by inheritance. This section never lists itself.48 49 The server's own facts (listener address, pid, what is mounted) sit alongside50 the apps under ``server``.51 52 Parent (dual relationship): the ServerApplication, stored as53 ``self.application``.54 """55 56 from __future__ import annotations57 58 import os59 from pathlib import Path60 from typing import TYPE_CHECKING, Any61 from urllib.parse import quote62 63 from genro_routes import RoutingClass, route64 65 from ...exceptions import HTTPNotFound66 67 if TYPE_CHECKING:68     from ..server_app import ServerApplication69 70 __all__ = ["MonitorSection"]71 72 MONITOR_RULE = "SERVER_ADMIN"73 74 75 class MonitorSection(RoutingClass):76     """The ``_server/monitor`` mount: the shell, the snapshot, the panels.77 78     Note:79         Parent (dual relationship): the ServerApplication, stored as80         ``self.application``. The server is ``self.application.server``.81     """82 83     def __init__(self, application: ServerApplication) -> None:84         """Bind the section to its ServerApplication (dual relationship)."""85         self.application = application86 87     @property88     def monitored_apps(self) -> dict[str, Any]:89         """The mounted applications the monitor shows, keyed by mount.90 91         The ``_server`` application is left out: this section is its monitor92         face, and a tab of its own would show the observer observing itself.93         """94         applications = self.application.server.applications95         return {96             app.mount: app for app in applications.values() if app is not self.application97         }98 99     @property100     def monitored_sections(self) -> dict[str, Any]:101         """Sibling sections that declare the panel contract, keyed ``_server/<name>``.102 103         A section is a ``RoutingClass``, not an application: it inherits104         nothing, so it takes part by declaring ``app_snapshot`` and105         ``app_panel`` itself. This section is never in the result.106         """107         return {108             f"_server/{name}": section109             for name, section in self.application.sections.items()110             if section is not self111             and hasattr(section, "app_snapshot")112             and hasattr(section, "app_panel")113         }114 115     @property116     def server_facts(self) -> dict[str, Any]:117         """What the server is, as the header of the page reads it.118 119         ``host``/``port`` are the CONFIGURED listener (the address ``serve``120         defaults to), not the bound socket: a server booted with ``port=0``121         shows the configuration, and the reader is looking at it through the122         real one anyway.123         """124         server = self.application.server125         return {126             "host": server.config_host,127             "port": server.config_port,128             "pid": os.getpid(),129             "applications": sorted(self.monitored_apps),130             "sections": sorted(self.application.sections),131         }132 133     @route(media_type="text/html", auth_rule=MONITOR_RULE)134     def index(self) -> str:135         """The monitor page: the shell that composes one panel per app.136 137         Note:138             Route: GET /_server/monitor/139         """140         return (Path(__file__).parent / "resources" / "monitor.html").read_text()141 142     @property143     def monitor_contributors(self) -> dict[str, Any]:144         """Everything the monitor shows, keyed as the shell keys its tabs.145 146         The mounted applications by mount, then the monitorable sections by147         ``_server/<name>``. One map, so the snapshot, the descriptors and the148         panel sources can never disagree on who is in the picture.149         """150         return {**self.monitored_apps, **self.monitored_sections}151 152     def panel_url(self, key: str) -> str:153         """The address serving ``key``'s own panel module.154 155         Absolute, not page-relative: the shell imports it with a dynamic156         ``import()``, which resolves against the document — and the document157         answers at two addresses, with and without the trailing slash.158         """159         return f"/{self.application.mount}/monitor/panel?app={quote(key, safe='')}"160 161     @route(media_type="application/json", auth_rule=MONITOR_RULE)162     def snapshot(self) -> dict[str, Any]:163         """The whole server at this instant: its own facts plus every app's.164 165         What the shell polls. Each mounted application and each monitorable166         section contributes its ``app_snapshot``.167 168         Note:169             Route: GET /_server/monitor/snapshot170         """171         apps = {key: item.app_snapshot for key, item in self.monitor_contributors.items()}172         return {"server": self.server_facts, "apps": apps}173 174     @route(media_type="application/json", auth_rule=MONITOR_RULE)175     def panels(self) -> dict[str, Any]:176         """One panel descriptor per contributor: who draws what.177 178         The static complement of ``snapshot`` — descriptors are class179         constants, so the shell fetches this once at load and polls the other.180 181         A contributor that also ships a ``panel_source`` gets its ``src``182         filled in here, pointing at ``panel``: declaring the module is enough,183         the app never publishes a route of its own for it. An explicit ``src``184         in the descriptor wins — an app is free to serve the module itself.185 186         Note:187             Route: GET /_server/monitor/panels188         """189         descriptors: dict[str, Any] = {}190         for key, item in self.monitor_contributors.items():191             descriptor = dict(item.app_panel)192             if hasattr(item, "panel_source") and "src" not in descriptor:193                 descriptor["src"] = self.panel_url(key)194             descriptors[key] = descriptor195         return descriptors196 197     @route(media_type="text/javascript", auth_rule=MONITOR_RULE)198     def panel(self, app: str = "") -> str:199         """The panel module one contributor ships, as an ES module.200 201         The shell imports this when a descriptor names a panel it does not202         know. ``app`` is the contributor's key — the mount, ``""`` for the site203         root, or ``_server/<name>`` for a section. An unknown key, or one that204         ships no module, is a 404: the app renders generically.205 206         Note:207             Route: GET /_server/monitor/panel?app=<key>208         """209         item = self.monitor_contributors.get(app)210         source = getattr(item, "panel_source", None)211         if source is None:212             raise HTTPNotFound(f"no panel module for '{app}'")213         return source