diff --git a/src/app_runtime/contracts/trace.py b/src/app_runtime/contracts/trace.py index 71964ba..92d4422 100644 --- a/src/app_runtime/contracts/trace.py +++ b/src/app_runtime/contracts/trace.py @@ -88,6 +88,7 @@ class TraceLogRecord: @dataclass(frozen=True) class TraceLogView: + trace_id: str parent_id: str | None records: tuple[TraceLogRecord, ...] = () diff --git a/src/app_runtime/control/http_app.py b/src/app_runtime/control/http_app.py index e7c4601..02b7bd5 100644 --- a/src/app_runtime/control/http_app.py +++ b/src/app_runtime/control/http_app.py @@ -135,11 +135,16 @@ class HttpControlAppFactory: if request.response_format == "json": return JSONResponse( content={ + "trace_id": trace_view.trace_id, "parent_id": trace_view.parent_id or "", "messages": [record.as_dict(include_attrs_json=request.include_attrs_json) for record in trace_view.records], } ) - lines = [trace_view.parent_id or ""] + lines = [ + f"trace_id: {trace_view.trace_id}", + f"parent_id: {trace_view.parent_id or ''}", + "--------------------------------------------------", + ] for record in trace_view.records: line = record.message if request.include_attrs_json: diff --git a/src/app_runtime/tracing/reader.py b/src/app_runtime/tracing/reader.py index b6f7e77..e480656 100644 --- a/src/app_runtime/tracing/reader.py +++ b/src/app_runtime/tracing/reader.py @@ -16,7 +16,7 @@ class MySqlTraceLogReader(TraceLogReader): if parent_id is None and not self._trace_exists(trace_id): return None records = self._read_records(trace_id, levels) - return TraceLogView(parent_id=parent_id, records=tuple(records)) + return TraceLogView(trace_id=trace_id, parent_id=parent_id, records=tuple(records)) def _trace_exists(self, trace_id: str) -> bool: query = "SELECT 1 FROM trace_contexts WHERE trace_id = %s" diff --git a/tests/test_trace_endpoint.py b/tests/test_trace_endpoint.py index 470f1a4..2e09f4c 100644 --- a/tests/test_trace_endpoint.py +++ b/tests/test_trace_endpoint.py @@ -50,6 +50,7 @@ def test_trace_endpoint_returns_text_with_default_levels() -> None: async def trace_provider(trace_id: str, request: TraceQueryRequest) -> TraceLogView: captured.append((trace_id, request)) return TraceLogView( + trace_id="trace-1", parent_id="root-trace", records=( _trace_record(row_id=1, level="ERROR", message="first error"), @@ -64,13 +65,20 @@ def test_trace_endpoint_returns_text_with_default_levels() -> None: client.close() assert response.status_code == 200 - assert response.text == "root-trace\nfirst error\nsecond warning" + assert response.text == ( + "trace_id: trace-1\n" + "parent_id: root-trace\n" + "--------------------------------------------------\n" + "first error\n" + "second warning" + ) assert captured == [("trace-1", TraceQueryRequest(levels=("ERROR", "WARNING"), include_attrs_json=False, response_format="text"))] def test_trace_endpoint_appends_attrs_json_in_text_mode() -> None: async def trace_provider(_trace_id: str, _request: TraceQueryRequest) -> TraceLogView: return TraceLogView( + trace_id="trace-1", parent_id=None, records=( _trace_record(row_id=1, level="ERROR", message="failure", attrs_json={"attempt": 2, "source": "crm"}), @@ -84,12 +92,18 @@ def test_trace_endpoint_appends_attrs_json_in_text_mode() -> None: client.close() assert response.status_code == 200 - assert response.text == '\nfailure, {"attempt":2,"source":"crm"}' + assert response.text == ( + "trace_id: trace-1\n" + "parent_id: \n" + "--------------------------------------------------\n" + 'failure, {"attempt":2,"source":"crm"}' + ) def test_trace_endpoint_returns_json_payload() -> None: async def trace_provider(_trace_id: str, _request: TraceQueryRequest) -> TraceLogView: return TraceLogView( + trace_id="trace-1", parent_id="parent-1", records=( _trace_record(row_id=3, level="INFO", message="done", attrs_json={"batch": 7}), @@ -104,6 +118,7 @@ def test_trace_endpoint_returns_json_payload() -> None: assert response.status_code == 200 assert response.json() == { + "trace_id": "trace-1", "parent_id": "parent-1", "messages": [ { @@ -135,7 +150,7 @@ def test_trace_endpoint_validates_query_params() -> None: def test_runtime_trace_logs_uses_configured_reader(monkeypatch) -> None: - expected = TraceLogView(parent_id="root", records=(_trace_record(row_id=1, level="ERROR", message="boom"),)) + expected = TraceLogView(trace_id="trace-1", parent_id="root", records=(_trace_record(row_id=1, level="ERROR", message="boom"),)) class StubReader: def read_trace(self, trace_id: str, levels: tuple[str, ...]) -> TraceLogView | None: @@ -208,6 +223,7 @@ def test_mysql_trace_log_reader_maps_db_rows() -> None: view = reader.read_trace("trace-1", ("ERROR", "WARNING")) assert view == TraceLogView( + trace_id="trace-1", parent_id="root-77", records=( TraceLogRecord(