From 72f2d5455300fee78de4fa893712728cb0594d6e Mon Sep 17 00:00:00 2001 From: zosimovaa Date: Tue, 28 Apr 2026 15:48:23 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=87=D0=B5=D1=80=D0=BD=D0=B8=D0=B5=20trace=5Fids?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/app_runtime/contracts/trace.py | 1 + src/app_runtime/control/http_app.py | 11 ++++++++++- src/app_runtime/tracing/reader.py | 21 ++++++++++++++++++++- tests/test_trace_endpoint.py | 25 ++++++++++++++++++++++--- 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f335950..d4e2ddc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plba" -version = "0.3.1" +version = "0.3.2" 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 92d4422..3c7a64b 100644 --- a/src/app_runtime/contracts/trace.py +++ b/src/app_runtime/contracts/trace.py @@ -90,6 +90,7 @@ class TraceLogRecord: class TraceLogView: trace_id: str parent_id: str | None + child_ids: tuple[str, ...] = () records: tuple[TraceLogRecord, ...] = () diff --git a/src/app_runtime/control/http_app.py b/src/app_runtime/control/http_app.py index 02b7bd5..ff8991f 100644 --- a/src/app_runtime/control/http_app.py +++ b/src/app_runtime/control/http_app.py @@ -137,17 +137,26 @@ class HttpControlAppFactory: 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], } ) lines = [ f"trace_id: {trace_view.trace_id}", f"parent_id: {trace_view.parent_id or ''}", - "--------------------------------------------------", ] + lines.extend(self._child_id_lines(trace_view.child_ids)) + lines.append("--------------------------------------------------") for record in trace_view.records: line = record.message if request.include_attrs_json: line = f"{line}, {json.dumps(record.attrs_json, ensure_ascii=False, separators=(',', ':'))}" lines.append(line) return PlainTextResponse(content="\n".join(lines)) + + def _child_id_lines(self, child_ids: tuple[str, ...]) -> list[str]: + lines = ["child_ids:"] + if not child_ids: + return lines + lines.extend(f" - {child_id}" for child_id in child_ids) + return lines diff --git a/src/app_runtime/tracing/reader.py b/src/app_runtime/tracing/reader.py index e480656..ea8f261 100644 --- a/src/app_runtime/tracing/reader.py +++ b/src/app_runtime/tracing/reader.py @@ -15,8 +15,14 @@ class MySqlTraceLogReader(TraceLogReader): parent_id = self._read_parent_id(trace_id) if parent_id is None and not self._trace_exists(trace_id): return None + child_ids = self._read_child_ids(trace_id) records = self._read_records(trace_id, levels) - return TraceLogView(trace_id=trace_id, parent_id=parent_id, records=tuple(records)) + return TraceLogView( + trace_id=trace_id, + parent_id=parent_id, + child_ids=tuple(child_ids), + records=tuple(records), + ) def _trace_exists(self, trace_id: str) -> bool: query = "SELECT 1 FROM trace_contexts WHERE trace_id = %s" @@ -50,6 +56,19 @@ class MySqlTraceLogReader(TraceLogReader): rows = cursor.fetchall() return [self._build_record(row) for row in rows] + def _read_child_ids(self, trace_id: str) -> list[str]: + query = """ + SELECT trace_id + FROM trace_contexts + WHERE parent_id = %s + ORDER BY event_time ASC, trace_id ASC + """ + with self._connection_factory.connect() as connection: + with connection.cursor() as cursor: + cursor.execute(query, (trace_id,)) + rows = cursor.fetchall() + return [str(row["trace_id"]) for row in rows] + def _build_record(self, row: dict[str, Any]) -> TraceLogRecord: return TraceLogRecord( id=int(row["id"]), diff --git a/tests/test_trace_endpoint.py b/tests/test_trace_endpoint.py index 2e09f4c..c903348 100644 --- a/tests/test_trace_endpoint.py +++ b/tests/test_trace_endpoint.py @@ -52,6 +52,7 @@ def test_trace_endpoint_returns_text_with_default_levels() -> None: return TraceLogView( trace_id="trace-1", parent_id="root-trace", + child_ids=("child-1", "child-2"), records=( _trace_record(row_id=1, level="ERROR", message="first error"), _trace_record(row_id=2, level="WARNING", message="second warning"), @@ -68,6 +69,9 @@ def test_trace_endpoint_returns_text_with_default_levels() -> None: assert response.text == ( "trace_id: trace-1\n" "parent_id: root-trace\n" + "child_ids:\n" + " - child-1\n" + " - child-2\n" "--------------------------------------------------\n" "first error\n" "second warning" @@ -80,6 +84,7 @@ def test_trace_endpoint_appends_attrs_json_in_text_mode() -> None: return TraceLogView( trace_id="trace-1", parent_id=None, + child_ids=(), records=( _trace_record(row_id=1, level="ERROR", message="failure", attrs_json={"attempt": 2, "source": "crm"}), ), @@ -95,6 +100,7 @@ def test_trace_endpoint_appends_attrs_json_in_text_mode() -> None: assert response.text == ( "trace_id: trace-1\n" "parent_id: \n" + "child_ids:\n" "--------------------------------------------------\n" 'failure, {"attempt":2,"source":"crm"}' ) @@ -105,6 +111,7 @@ def test_trace_endpoint_returns_json_payload() -> None: return TraceLogView( trace_id="trace-1", parent_id="parent-1", + child_ids=("child-1",), records=( _trace_record(row_id=3, level="INFO", message="done", attrs_json={"batch": 7}), ), @@ -120,6 +127,7 @@ def test_trace_endpoint_returns_json_payload() -> None: assert response.json() == { "trace_id": "trace-1", "parent_id": "parent-1", + "child_ids": ["child-1"], "messages": [ { "id": 3, @@ -150,7 +158,12 @@ def test_trace_endpoint_validates_query_params() -> None: def test_runtime_trace_logs_uses_configured_reader(monkeypatch) -> None: - expected = TraceLogView(trace_id="trace-1", parent_id="root", records=(_trace_record(row_id=1, level="ERROR", message="boom"),)) + expected = TraceLogView( + trace_id="trace-1", + parent_id="root", + child_ids=("child-1",), + records=(_trace_record(row_id=1, level="ERROR", message="boom"),), + ) class StubReader: def read_trace(self, trace_id: str, levels: tuple[str, ...]) -> TraceLogView | None: @@ -170,14 +183,18 @@ def test_mysql_trace_log_reader_maps_db_rows() -> None: class FakeCursor: def __init__(self) -> None: self.executed: list[tuple[str, tuple[object, ...]]] = [] + self._current_query = "" def execute(self, query: str, params: tuple[object, ...]) -> None: self.executed.append((query, params)) + self._current_query = query def fetchone(self) -> dict[str, object] | None: return {"parent_id": "root-77"} def fetchall(self) -> list[dict[str, object]]: + if "WHERE parent_id = %s" in self._current_query: + return [{"trace_id": "child-1"}, {"trace_id": "child-2"}] return [ { "id": 8, @@ -225,6 +242,7 @@ def test_mysql_trace_log_reader_maps_db_rows() -> None: assert view == TraceLogView( trace_id="trace-1", parent_id="root-77", + child_ids=("child-1", "child-2"), records=( TraceLogRecord( id=8, @@ -238,5 +256,6 @@ def test_mysql_trace_log_reader_maps_db_rows() -> None: ), ), ) - assert len(factory.cursor.executed) == 2 - assert factory.cursor.executed[1][1] == ("trace-1", "ERROR", "WARNING") + assert len(factory.cursor.executed) == 3 + assert factory.cursor.executed[1][1] == ("trace-1",) + assert factory.cursor.executed[2][1] == ("trace-1", "ERROR", "WARNING")