observability.py 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Shared observability helpers for data modules.
  5. """
  6. from __future__ import annotations
  7. import json
  8. import logging
  9. from datetime import datetime
  10. from pathlib import Path
  11. from typing import Any, Dict, Optional
  12. logger = logging.getLogger(__name__)
  13. def safe_log_tool_call(
  14. tool_logger,
  15. *,
  16. tool_name: str,
  17. success: bool,
  18. retry_count: int = 0,
  19. error_code: Optional[str] = None,
  20. error_message: Optional[str] = None,
  21. chapter: Optional[int] = None,
  22. ) -> None:
  23. try:
  24. tool_logger.log_tool_call(
  25. tool_name,
  26. success,
  27. retry_count=retry_count,
  28. error_code=error_code,
  29. error_message=error_message,
  30. chapter=chapter,
  31. )
  32. except Exception as exc:
  33. logger.warning(
  34. "failed to log tool call %s: %s",
  35. tool_name,
  36. exc,
  37. )
  38. def safe_append_perf_timing(
  39. project_root: str | Path,
  40. *,
  41. tool_name: str,
  42. success: bool,
  43. elapsed_ms: int,
  44. chapter: Optional[int] = None,
  45. error_code: Optional[str] = None,
  46. error_message: Optional[str] = None,
  47. meta: Optional[Dict[str, Any]] = None,
  48. ) -> None:
  49. """
  50. Append timing trace for profiling long-running data-agent pipeline steps.
  51. Output path:
  52. - {project_root}/.webnovel/observability/data_agent_timing.jsonl
  53. """
  54. try:
  55. root = Path(project_root).resolve()
  56. obs_dir = root / ".webnovel" / "observability"
  57. obs_dir.mkdir(parents=True, exist_ok=True)
  58. log_path = obs_dir / "data_agent_timing.jsonl"
  59. payload: Dict[str, Any] = {
  60. "timestamp": datetime.now().isoformat(),
  61. "tool_name": tool_name,
  62. "success": bool(success),
  63. "elapsed_ms": int(max(0, elapsed_ms)),
  64. }
  65. if chapter is not None:
  66. payload["chapter"] = int(chapter)
  67. if error_code:
  68. payload["error_code"] = error_code
  69. if error_message:
  70. payload["error_message"] = error_message
  71. if meta:
  72. payload["meta"] = meta
  73. with open(log_path, "a", encoding="utf-8") as f:
  74. f.write(json.dumps(payload, ensure_ascii=False) + "\n")
  75. except Exception as exc:
  76. logger.warning("failed to append perf timing for %s: %s", tool_name, exc)