From 62f08776eb599b170a8bd29fcc811c37552c1f74 Mon Sep 17 00:00:00 2001 From: zosimovaa Date: Sat, 2 May 2026 23:14:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20trace=20html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/app_runtime/control/trace_presenter.py | 236 ++++++--------------- tests/test_trace_endpoint.py | 22 +- 3 files changed, 83 insertions(+), 177 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ab9814a..1c6f8fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plba" -version = "0.3.7" +version = "0.3.8" description = "Platform runtime for business applications" readme = "README.md" requires-python = ">=3.11" diff --git a/src/app_runtime/control/trace_presenter.py b/src/app_runtime/control/trace_presenter.py index 48b69af..cb5d815 100644 --- a/src/app_runtime/control/trace_presenter.py +++ b/src/app_runtime/control/trace_presenter.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -from dataclasses import dataclass from html import escape from urllib.parse import urlencode @@ -12,12 +11,6 @@ from app_runtime.control.base import TraceQueryRequest from app_runtime.contracts.trace import TraceLevel, TraceLogRecord, TraceLogView -@dataclass(frozen=True) -class TraceStepGroup: - step: str - records: tuple[TraceLogRecord, ...] - - class TraceRequestParser: def parse(self, request: Request) -> TraceQueryRequest: raw_levels = request.query_params.get("levels") @@ -94,8 +87,7 @@ class TraceResponseRenderer: def _render_html(self, trace_view: TraceLogView, request: TraceQueryRequest) -> HTMLResponse: title = escape(f"Trace {trace_view.trace_id}") - header_links = self._related_trace_links(trace_view, request) - sections = self._step_sections(trace_view, request) + lines = self._html_lines(trace_view, request) html = f""" @@ -104,146 +96,55 @@ class TraceResponseRenderer: {title}
-
-

{title}

-

Parent: {escape(trace_view.parent_id or "")}

-

Children: {len(trace_view.child_ids)}

-
-
-

Related Traces

- {header_links} -
-
-

Logs By Step

- {sections} -
+ {lines}
""" @@ -259,18 +160,6 @@ class TraceResponseRenderer: return record.message return f"{record.message}, {json.dumps(record.attrs_json, ensure_ascii=False, separators=(',', ':'))}" - def _related_trace_links(self, trace_view: TraceLogView, request: TraceQueryRequest) -> str: - links: list[str] = [] - if trace_view.parent_id: - links.append(self._trace_link_item("parent", trace_view.parent_id, request)) - links.extend(self._trace_link_item("child", child_id, request) for child_id in trace_view.child_ids) - return f"" - - def _trace_link_item(self, label: str, trace_id: str, request: TraceQueryRequest) -> str: - href = escape(self._trace_href(trace_id, request), quote=True) - text = escape(f"{label}: {trace_id}") - return f"
  • {text}
  • " - def _trace_href(self, trace_id: str, request: TraceQueryRequest) -> str: params = urlencode( { @@ -281,41 +170,54 @@ class TraceResponseRenderer: ) return f"/traces/{trace_id}?{params}" - def _step_sections(self, trace_view: TraceLogView, request: TraceQueryRequest) -> str: - groups = self._step_groups(trace_view.records) - if not groups: - return "

    No step

    No log records for selected filters.
    " - return "".join(self._step_section(group, request.include_attrs_json) for group in groups) + def _html_lines(self, trace_view: TraceLogView, request: TraceQueryRequest) -> str: + lines = [ + self._html_plain_line(f"trace_id: {self._trace_link(trace_view.trace_id, request)}"), + self._html_plain_line(f"parent_id: {self._optional_trace_link(trace_view.parent_id, request)}"), + self._html_plain_line("child_ids:"), + *(self._html_plain_line(f" - {self._trace_link(child_id, request)}") for child_id in trace_view.child_ids), + self._html_plain_line("--------------------------------------------------"), + ] + previous_step: str | None = None + for record in trace_view.records: + current_step = str(record.step or "") + if previous_step is None: + lines.append(self._html_plain_line(escape(current_step))) + lines.append(self._html_plain_line("")) + elif current_step != previous_step: + lines.append(self._html_plain_line("")) + lines.append(self._html_plain_line(escape(current_step))) + lines.append(self._html_plain_line("")) + previous_step = current_step + lines.extend(self._html_message_lines(record, request.include_attrs_json)) + return "".join(lines) - def _step_groups(self, records: tuple[TraceLogRecord, ...]) -> tuple[TraceStepGroup, ...]: - grouped: dict[str, list[TraceLogRecord]] = {} - order: list[str] = [] - for record in records: - step = str(record.step or "") - if step not in grouped: - grouped[step] = [] - order.append(step) - grouped[step].append(record) - return tuple(TraceStepGroup(step=step, records=tuple(grouped[step])) for step in order) + def _html_message_lines(self, record: TraceLogRecord, include_attrs_json: bool) -> list[str]: + lines = [self._html_colored_line(self._text_message(record, include_attrs_json), record.level)] + return lines - def _step_section(self, group: TraceStepGroup, include_attrs_json: bool) -> str: - step_label = escape(group.step) if group.step else "No step" - entries = "".join(self._log_entry(record, include_attrs_json) for record in group.records) - return f"

    {step_label}

    {entries}
    " + def _html_plain_line(self, content: str) -> str: + return f"
    {content or ' '}
    " - def _log_entry(self, record: TraceLogRecord, include_attrs_json: bool) -> str: - level = escape(record.level) - status = escape(record.status or "") - timestamp = escape(record.event_time.isoformat()) - message = escape(record.message) - attrs = "" - if include_attrs_json: - attrs_payload = escape(json.dumps(record.attrs_json, ensure_ascii=False, indent=2, sort_keys=True)) - attrs = f"
    {attrs_payload}
    " - return ( - f"
    " - f"
    {timestamp} | {level} | {status}
    " - f"
    {message}
    " - f"{attrs}" - f"
    " - ) + def _html_colored_line(self, content: str, level: str) -> str: + level_class = self._level_class(level) + return f"
    {escape(content)}
    " + + def _trace_link(self, trace_id: str, request: TraceQueryRequest) -> str: + href = escape(self._trace_href(trace_id, request), quote=True) + text = escape(trace_id) + return f"{text}" + + def _optional_trace_link(self, trace_id: str | None, request: TraceQueryRequest) -> str: + if not trace_id: + return "" + return self._trace_link(trace_id, request) + + def _level_class(self, level: str) -> str: + if level == "ERROR": + return "msg-error" + if level == "WARNING": + return "msg-warning" + if level == "DEBUG": + return "msg-debug" + return "msg-info" diff --git a/tests/test_trace_endpoint.py b/tests/test_trace_endpoint.py index 188f63e..e61f6f0 100644 --- a/tests/test_trace_endpoint.py +++ b/tests/test_trace_endpoint.py @@ -201,17 +201,21 @@ def test_trace_endpoint_returns_html_page_with_related_links() -> None: assert response.status_code == 200 assert response.headers["content-type"].startswith("text/html") - assert "

    Logs By Step

    " in response.text - assert '

    load_stocks

    ' in response.text - assert '

    filter_stocks

    ' in response.text + assert "background: var(--bg);" in response.text + assert "--bg: #000000;" in response.text + assert "--fg: #cfcfc2;" in response.text + assert '
    trace_id: trace-1
    ' in response.text + assert '
    parent_id: parent-1
    ' in response.text + assert '
    child_ids:
    ' in response.text + assert '
    - child-1
    ' in response.text + assert '
    - child-2
    ' in response.text + assert '
    load_stocks
    ' in response.text + assert '
    filter_stocks
    ' in response.text assert "loaded prices" in response.text assert "filtered suspicious ticker" in response.text - assert "2026-04-28T10:11:12+00:00 | INFO | ok" in response.text - assert "2026-04-28T10:11:12+00:00 | WARNING | degraded" in response.text - assert 'href="/traces/trace-1?format=html&levels=ERROR%2CWARNING%2CINFO&attrs_json=true"' not in response.text - assert 'href="/traces/parent-1?format=html&levels=ERROR%2CWARNING%2CINFO&attrs_json=true"' in response.text - assert 'href="/traces/child-1?format=html&levels=ERROR%2CWARNING%2CINFO&attrs_json=true"' in response.text - assert 'href="/traces/child-2?format=html&levels=ERROR%2CWARNING%2CINFO&attrs_json=true"' in response.text + assert "2026-04-28T10:11:12+00:00 | INFO | ok" not in response.text + assert "2026-04-28T10:11:12+00:00 | WARNING | degraded" not in response.text + assert "Related Traces" not in response.text def test_trace_endpoint_validates_query_params() -> None: