diff --git a/README.md b/README.md index 4dacd66..765d5be 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ ## Установка +Быстрая установка через SSH-доступ к Git: + +```bash +pip install "plba @ git+ssh://git@git.lesha.spb.ru/alex/plba.git@main" +``` + Установка пакета напрямую из Git-репозитория через `pip`: ```bash diff --git a/pyproject.toml b/pyproject.toml index 1e8fa9e..cace5e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plba" -version = "0.4.0" +version = "0.5.0" description = "Platform runtime for business applications" readme = "README.md" requires-python = ">=3.11" diff --git a/src/app_runtime/contracts/trace.py b/src/app_runtime/contracts/trace.py index e877af3..a074d9d 100644 --- a/src/app_runtime/contracts/trace.py +++ b/src/app_runtime/contracts/trace.py @@ -90,9 +90,12 @@ class TraceLogRecord: class TraceLogView: trace_id: str parent_id: str | None + alias: str = "" child_ids: tuple[str, ...] = () records: tuple[TraceLogRecord, ...] = () ancestors: tuple[TraceLogView, ...] = () + descendants: tuple[TraceLogView, ...] = () + requested_trace_id: str | None = None class TraceLogReader(Protocol): @@ -101,5 +104,7 @@ class TraceLogReader(Protocol): trace_id: str, levels: tuple[TraceLevel, ...], ancestor_depth: int | None = 0, + *, + view: Literal["tree", "single"] = "tree", ) -> TraceLogView | None: """Load trace context and filtered log records.""" diff --git a/src/app_runtime/control/base.py b/src/app_runtime/control/base.py index 3776a2e..8deefb3 100644 --- a/src/app_runtime/control/base.py +++ b/src/app_runtime/control/base.py @@ -20,6 +20,7 @@ ActionResult = str | dict[str, object] ActionHandler = Callable[[ControlActionRequest], Awaitable[ActionResult]] HealthHandler = Callable[[], Awaitable[HealthPayload]] TraceResponseFormat = Literal["json", "text", "html"] +TraceViewMode = Literal["tree", "single"] @dataclass(slots=True) @@ -27,7 +28,8 @@ class TraceQueryRequest: levels: tuple[TraceLevel, ...] = ("ERROR", "WARNING", "INFO") include_attrs_json: bool = False response_format: TraceResponseFormat = "html" - ancestor_depth: int | None = 0 + ancestor_depth: int | None = None + view: TraceViewMode = "tree" TraceLookupHandler = Callable[[str, TraceQueryRequest], Awaitable[TraceLogView]] diff --git a/src/app_runtime/control/trace_presenter.py b/src/app_runtime/control/trace_presenter.py index 2df7236..6d12056 100644 --- a/src/app_runtime/control/trace_presenter.py +++ b/src/app_runtime/control/trace_presenter.py @@ -21,11 +21,16 @@ class TraceRequestParser: response_format = raw_format.strip().lower() if response_format not in {"json", "text", "html"}: raise ValueError(f"unsupported trace format: {raw_format}") + raw_view = request.query_params.get("view", "tree") + view = raw_view.strip().lower() + if view not in {"tree", "single"}: + raise ValueError(f"unsupported trace view: {raw_view}") return TraceQueryRequest( levels=self._trace_levels(raw_levels), include_attrs_json=self._bool_param(request, "attrs_json") or False, response_format=response_format, - ancestor_depth=self._ancestor_depth(request), + ancestor_depth=self._ancestor_depth(request, default_all=view == "tree"), + view=view, ) def _trace_levels(self, raw_levels: str | None) -> tuple[TraceLevel, ...]: @@ -51,10 +56,10 @@ class TraceRequestParser: return False raise ValueError(f"invalid boolean query parameter: {name}={raw_value}") - def _ancestor_depth(self, request: Request) -> int | None: + def _ancestor_depth(self, request: Request, *, default_all: bool) -> int | None: raw_value = request.query_params.get("ancestor_depth") if raw_value is None: - return 0 + return None if default_all else 0 normalized = raw_value.strip().lower() if normalized == "all": return None @@ -69,26 +74,35 @@ class TraceRequestParser: class TraceResponseRenderer: def render(self, trace_view: TraceLogView, request: TraceQueryRequest) -> Response: + if request.view == "single": + if request.response_format == "json": + return self._render_json_single(trace_view, request) + if request.response_format == "html": + return self._render_html_single(trace_view, request) + return self._render_text_single(trace_view, request) if request.response_format == "json": - return self._render_json(trace_view, request) + return self._render_json_tree(trace_view, request) if request.response_format == "html": - return self._render_html(trace_view, request) - return self._render_text(trace_view, request) + return self._render_html_tree(trace_view, request) + return self._render_text_tree(trace_view, request) - def _render_json(self, trace_view: TraceLogView, request: TraceQueryRequest) -> JSONResponse: + def _render_json_single(self, trace_view: TraceLogView, request: TraceQueryRequest) -> JSONResponse: return JSONResponse( content={ "trace_id": trace_view.trace_id, "parent_id": trace_view.parent_id or "", "child_ids": list(trace_view.child_ids), "messages": [record.as_dict(include_attrs_json=request.include_attrs_json) for record in trace_view.records], - "ancestors": [self._trace_payload(view, request) for view in trace_view.ancestors], + "ancestors": [self._trace_payload_single(view, request) for view in trace_view.ancestors], } ) - def _render_text(self, trace_view: TraceLogView, request: TraceQueryRequest) -> PlainTextResponse: + def _render_json_tree(self, trace_view: TraceLogView, request: TraceQueryRequest) -> JSONResponse: + return JSONResponse(content=self._trace_payload_tree(trace_view, request)) + + def _render_text_single(self, trace_view: TraceLogView, request: TraceQueryRequest) -> PlainTextResponse: lineage = [*trace_view.ancestors, trace_view] - lines = self._text_trace_summary_lines(trace_view) + lines = self._text_trace_summary_lines_single(trace_view) for index, entry in enumerate(lineage): if index == 0: lines.append("") @@ -97,10 +111,29 @@ class TraceResponseRenderer: lines.extend(self._text_trace_log_lines(entry, request)) return PlainTextResponse(content="\n".join(lines)) - def _render_html(self, trace_view: TraceLogView, request: TraceQueryRequest) -> HTMLResponse: + def _render_text_tree(self, trace_view: TraceLogView, request: TraceQueryRequest) -> PlainTextResponse: + lines = self._text_trace_summary_lines_tree(trace_view) + for index, entry in enumerate(self._tree_walk(trace_view)): + if index == 0: + lines.append("") + else: + lines.extend(["", ""]) + lines.extend(self._text_trace_log_lines(entry, request)) + return PlainTextResponse(content="\n".join(lines)) + + def _render_html_single(self, trace_view: TraceLogView, request: TraceQueryRequest) -> HTMLResponse: title = escape(f"Trace {trace_view.trace_id}") - lines = self._html_lines(trace_view, request) - html = f""" + lines = self._html_lines_single(trace_view, request) + return HTMLResponse(content=self._html_document(title, lines)) + + def _render_html_tree(self, trace_view: TraceLogView, request: TraceQueryRequest) -> HTMLResponse: + requested = trace_view.requested_trace_id or trace_view.trace_id + title = escape(f"Trace {requested}") + lines = self._html_lines_tree(trace_view, request) + return HTMLResponse(content=self._html_document(title, lines)) + + def _html_document(self, title: str, lines: str) -> str: + return f'''
@@ -160,8 +193,7 @@ class TraceResponseRenderer: {lines} -""" - return HTMLResponse(content=html) +