diff --git a/src/app_runtime/control/http_app.py b/src/app_runtime/control/http_app.py index ef2dec3..826d03a 100644 --- a/src/app_runtime/control/http_app.py +++ b/src/app_runtime/control/http_app.py @@ -147,7 +147,15 @@ class HttpControlAppFactory: ] lines.extend(self._child_id_lines(trace_view.child_ids)) lines.append("--------------------------------------------------") + previous_step: str | None = None for record in trace_view.records: + current_step = str(record.step or "") + if previous_step is None: + lines.append(f"step: {current_step}") + elif current_step != previous_step: + lines.append("--------------------------------------------------") + lines.append(f"step: {current_step}") + previous_step = current_step line = record.message if request.include_attrs_json: line = f"{line}, {json.dumps(record.attrs_json, ensure_ascii=False, separators=(',', ':'))}" diff --git a/tests/test_trace_endpoint.py b/tests/test_trace_endpoint.py index 1685e75..63fc670 100644 --- a/tests/test_trace_endpoint.py +++ b/tests/test_trace_endpoint.py @@ -19,14 +19,16 @@ def _trace_record( row_id: int, level: str, message: str, + step: str = "process", + status: str = "failed", attrs_json: object | None = None, ) -> TraceLogRecord: return TraceLogRecord( id=row_id, trace_id="trace-1", event_time=datetime(2026, 4, 28, 10, 11, 12, tzinfo=timezone.utc), - step="process", - status="failed", + step=step, + status=status, level=level, # type: ignore[arg-type] message=message, attrs_json=attrs_json if attrs_json is not None else {}, @@ -73,6 +75,7 @@ def test_trace_endpoint_returns_text_with_default_levels() -> None: " - child-1\n" " - child-2\n" "--------------------------------------------------\n" + "step: process\n" "first error\n" "second warning" ) @@ -102,10 +105,45 @@ def test_trace_endpoint_appends_attrs_json_in_text_mode() -> None: "parent_id: \n" "child_ids:\n" "--------------------------------------------------\n" + "step: process\n" 'failure, {"attempt":2,"source":"crm"}' ) +def test_trace_endpoint_separates_messages_by_step_in_text_mode() -> None: + async def trace_provider(_trace_id: str, _request: TraceQueryRequest) -> TraceLogView: + return TraceLogView( + trace_id="trace-1", + parent_id=None, + child_ids=(), + records=( + _trace_record(row_id=1, level="INFO", message="load first", step="load_stocks"), + _trace_record(row_id=2, level="INFO", message="load second", step="load_stocks"), + _trace_record(row_id=3, level="INFO", message="filter first", step="filter_stocks"), + ), + ) + + client = _build_client(trace_provider) + try: + response = client.get("/traces/trace-1") + finally: + client.close() + + assert response.status_code == 200 + assert response.text == ( + "trace_id: trace-1\n" + "parent_id: \n" + "child_ids:\n" + "--------------------------------------------------\n" + "step: load_stocks\n" + "load first\n" + "load second\n" + "--------------------------------------------------\n" + "step: filter_stocks\n" + "filter first" + ) + + def test_trace_endpoint_returns_json_payload() -> None: async def trace_provider(_trace_id: str, _request: TraceQueryRequest) -> TraceLogView: return TraceLogView(