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