diff --git a/src/app_runtime/contracts/trace.py b/src/app_runtime/contracts/trace.py index fe339d0..38ff248 100644 --- a/src/app_runtime/contracts/trace.py +++ b/src/app_runtime/contracts/trace.py @@ -6,7 +6,7 @@ from datetime import datetime, timezone from typing import Any, Literal, Protocol -TraceLevel = Literal["INFO", "WARNING", "ERROR"] +TraceLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"] def utc_now() -> datetime: diff --git a/src/app_runtime/tracing/service.py b/src/app_runtime/tracing/service.py index 6b5721a..99dd143 100644 --- a/src/app_runtime/tracing/service.py +++ b/src/app_runtime/tracing/service.py @@ -90,6 +90,9 @@ class TraceService(TraceContextFactory): def info(self, message: str, *, status: str | None = None, attrs: dict[str, Any] | None = None) -> None: self._write_message("INFO", message, status, attrs) + def debug(self, message: str, *, status: str | None = None, attrs: dict[str, Any] | None = None) -> None: + self._write_message("DEBUG", message, status, attrs) + def warning(self, message: str, *, status: str | None = None, attrs: dict[str, Any] | None = None) -> None: self._write_message("WARNING", message, status, attrs) diff --git a/src/app_runtime/workflow/engine/workflow_engine.py b/src/app_runtime/workflow/engine/workflow_engine.py index 8a6e82e..461a80a 100644 --- a/src/app_runtime/workflow/engine/workflow_engine.py +++ b/src/app_runtime/workflow/engine/workflow_engine.py @@ -34,7 +34,11 @@ class WorkflowEngine: self._hooks.on_step_started(context, current_name) self._persistence.start_step(run_id, current_name, context.snapshot()) self._traces.step(current_name) - self._traces.info(f"Step '{current_name}' started.", status="started") + self._traces.debug( + f"Step '{current_name}' started.", + status="started", + attrs={"workflow_run_id": run_id, "node": current_name}, + ) try: result = node.step.run(context) except Exception as error: @@ -56,9 +60,16 @@ class WorkflowEngine: result.transition, context.snapshot(), ) - self._traces.info( + next_node = self._transition_resolver.resolve(node, result) + self._traces.debug( f"Step '{current_name}' completed with transition '{result.transition}'.", status=result.status, + attrs={ + "workflow_run_id": run_id, + "from_node": current_name, + "transition": result.transition, + "to_node": next_node, + }, ) self._logger.info( "Workflow run %s: step '%s' completed with transition '%s'.", @@ -67,7 +78,7 @@ class WorkflowEngine: result.transition, ) self._hooks.on_step_finished(context, current_name) - current_name = self._transition_resolver.resolve(node, result) + current_name = next_node self._persistence.complete_run(run_id, context.snapshot()) self._traces.step("workflow") self._traces.info("Workflow completed.", status="completed", attrs={"workflow_run_id": run_id}) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index dfd8f9c..49db511 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -259,7 +259,7 @@ def test_trace_service_writes_contexts_and_messages() -> None: assert transport.messages[0].step == "parse" -def test_trace_service_supports_warning_and_error_levels() -> None: +def test_trace_service_supports_debug_warning_and_error_levels() -> None: from app_runtime.tracing.service import TraceService transport = RecordingTransport() @@ -267,12 +267,13 @@ def test_trace_service_supports_warning_and_error_levels() -> None: with manager.open_context(alias="worker", kind="worker", attrs={"routine": "incoming"}): manager.step("validate") + manager.debug("validation details", attrs={"rule": "basic"}) manager.warning("validation warning", status="degraded", attrs={"attempt": 1}) manager.error("integration failed", status="failed", attrs={"integration": "crm"}) manager.exception("caught exception", attrs={"exception_type": "RuntimeError"}) levels = [message.level for message in transport.messages] - assert levels == ["WARNING", "ERROR", "ERROR"] + assert levels == ["DEBUG", "WARNING", "ERROR", "ERROR"] def test_trace_service_allows_messages_without_status() -> None: @@ -283,11 +284,12 @@ def test_trace_service_allows_messages_without_status() -> None: with manager.open_context(alias="worker", kind="worker"): manager.step("optional-status") + manager.debug("debug without status") manager.info("info without status") manager.warning("warning without status") manager.error("error without status") - assert [message.status for message in transport.messages] == ["", "", ""] + assert [message.status for message in transport.messages] == ["", "", "", ""] assert all(message.trace_id == transport.contexts[0].trace_id for message in transport.messages)