Одноколоночный трейс
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "plba"
|
name = "plba"
|
||||||
version = "0.3.5"
|
version = "0.3.6"
|
||||||
description = "Platform runtime for business applications"
|
description = "Platform runtime for business applications"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class TraceResponseRenderer:
|
|||||||
def _render_html(self, trace_view: TraceLogView, request: TraceQueryRequest) -> HTMLResponse:
|
def _render_html(self, trace_view: TraceLogView, request: TraceQueryRequest) -> HTMLResponse:
|
||||||
title = escape(f"Trace {trace_view.trace_id}")
|
title = escape(f"Trace {trace_view.trace_id}")
|
||||||
header_links = self._related_trace_links(trace_view, request)
|
header_links = self._related_trace_links(trace_view, request)
|
||||||
rows = self._step_rows(trace_view, request)
|
sections = self._step_sections(trace_view, request)
|
||||||
html = f"""<!DOCTYPE html>
|
html = f"""<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -166,26 +166,16 @@ class TraceResponseRenderer:
|
|||||||
a:hover {{
|
a:hover {{
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}}
|
}}
|
||||||
table {{
|
.step-section + .step-section {{
|
||||||
width: 100%;
|
margin-top: 24px;
|
||||||
border-collapse: collapse;
|
padding-top: 24px;
|
||||||
}}
|
|
||||||
th, td {{
|
|
||||||
vertical-align: top;
|
|
||||||
padding: 14px 12px;
|
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
}}
|
}}
|
||||||
th {{
|
.step-title {{
|
||||||
text-align: left;
|
margin: 0 0 14px;
|
||||||
color: var(--muted);
|
font-size: 22px;
|
||||||
font-size: 13px;
|
line-height: 1.2;
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}}
|
|
||||||
td.step {{
|
|
||||||
width: 240px;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
white-space: nowrap;
|
|
||||||
}}
|
}}
|
||||||
.log-entry + .log-entry {{
|
.log-entry + .log-entry {{
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
@@ -222,6 +212,21 @@ class TraceResponseRenderer:
|
|||||||
.level-DEBUG {{
|
.level-DEBUG {{
|
||||||
color: var(--debug);
|
color: var(--debug);
|
||||||
}}
|
}}
|
||||||
|
@media (max-width: 640px) {{
|
||||||
|
.page {{
|
||||||
|
padding: 14px;
|
||||||
|
}}
|
||||||
|
.card {{
|
||||||
|
padding: 16px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}}
|
||||||
|
h1 {{
|
||||||
|
font-size: 22px;
|
||||||
|
}}
|
||||||
|
.step-title {{
|
||||||
|
font-size: 20px;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -237,14 +242,7 @@ class TraceResponseRenderer:
|
|||||||
</section>
|
</section>
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Logs By Step</h2>
|
<h2>Logs By Step</h2>
|
||||||
<table>
|
{sections}
|
||||||
<thead>
|
|
||||||
<tr><th>Step</th><th>Log</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@@ -283,11 +281,11 @@ class TraceResponseRenderer:
|
|||||||
)
|
)
|
||||||
return f"/traces/{trace_id}?{params}"
|
return f"/traces/{trace_id}?{params}"
|
||||||
|
|
||||||
def _step_rows(self, trace_view: TraceLogView, request: TraceQueryRequest) -> str:
|
def _step_sections(self, trace_view: TraceLogView, request: TraceQueryRequest) -> str:
|
||||||
groups = self._step_groups(trace_view.records)
|
groups = self._step_groups(trace_view.records)
|
||||||
if not groups:
|
if not groups:
|
||||||
return "<tr><td class=\"step\"> </td><td>No log records for selected filters.</td></tr>"
|
return "<section class=\"step-section\"><h3 class=\"step-title\">No step</h3><div>No log records for selected filters.</div></section>"
|
||||||
return "".join(self._step_row(group, request.include_attrs_json) for group in groups)
|
return "".join(self._step_section(group, request.include_attrs_json) for group in groups)
|
||||||
|
|
||||||
def _step_groups(self, records: tuple[TraceLogRecord, ...]) -> tuple[TraceStepGroup, ...]:
|
def _step_groups(self, records: tuple[TraceLogRecord, ...]) -> tuple[TraceStepGroup, ...]:
|
||||||
grouped: dict[str, list[TraceLogRecord]] = {}
|
grouped: dict[str, list[TraceLogRecord]] = {}
|
||||||
@@ -300,10 +298,10 @@ class TraceResponseRenderer:
|
|||||||
grouped[step].append(record)
|
grouped[step].append(record)
|
||||||
return tuple(TraceStepGroup(step=step, records=tuple(grouped[step])) for step in order)
|
return tuple(TraceStepGroup(step=step, records=tuple(grouped[step])) for step in order)
|
||||||
|
|
||||||
def _step_row(self, group: TraceStepGroup, include_attrs_json: bool) -> str:
|
def _step_section(self, group: TraceStepGroup, include_attrs_json: bool) -> str:
|
||||||
step_label = escape(group.step) if group.step else " "
|
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)
|
entries = "".join(self._log_entry(record, include_attrs_json) for record in group.records)
|
||||||
return f"<tr><td class=\"step\">{step_label}</td><td>{entries}</td></tr>"
|
return f"<section class=\"step-section\"><h3 class=\"step-title\">{step_label}</h3>{entries}</section>"
|
||||||
|
|
||||||
def _log_entry(self, record: TraceLogRecord, include_attrs_json: bool) -> str:
|
def _log_entry(self, record: TraceLogRecord, include_attrs_json: bool) -> str:
|
||||||
level = escape(record.level)
|
level = escape(record.level)
|
||||||
|
|||||||
@@ -201,11 +201,13 @@ def test_trace_endpoint_returns_html_page_with_related_links() -> None:
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.headers["content-type"].startswith("text/html")
|
assert response.headers["content-type"].startswith("text/html")
|
||||||
assert "<th>Step</th><th>Log</th>" in response.text
|
assert "<h2>Logs By Step</h2>" in response.text
|
||||||
assert ">load_stocks<" in response.text
|
assert '<h3 class="step-title">load_stocks</h3>' in response.text
|
||||||
assert ">filter_stocks<" in response.text
|
assert '<h3 class="step-title">filter_stocks</h3>' in response.text
|
||||||
assert "loaded prices" in response.text
|
assert "loaded prices" in response.text
|
||||||
assert "filtered suspicious ticker" 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"' in response.text
|
assert 'href="/traces/trace-1?format=html&levels=ERROR%2CWARNING%2CINFO&attrs_json=true"' in response.text
|
||||||
assert 'href="/traces/parent-1?format=html&levels=ERROR%2CWARNING%2CINFO&attrs_json=true"' 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-1?format=html&levels=ERROR%2CWARNING%2CINFO&attrs_json=true"' in response.text
|
||||||
|
|||||||
Reference in New Issue
Block a user