| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """ |
| Debug logging module for requests. |
| |
| Supports three modes (DEBUG_MODE): |
| - off: logging disabled |
| - errors: logs are saved only on errors (4xx, 5xx) |
| - all: logs are overwritten on each request |
| |
| In "errors" mode, data is buffered in memory and flushed to files |
| only when flush_on_error() is called. |
| |
| Also captures application logs (loguru) for each request and saves |
| them to app_logs.txt file for debugging convenience. |
| """ |
|
|
| import io |
| import json |
| import shutil |
| from pathlib import Path |
| from typing import Optional |
| from loguru import logger |
|
|
| from kiro.config import DEBUG_MODE, DEBUG_DIR |
|
|
|
|
| class DebugLogger: |
| """ |
| Singleton for managing debug request logs. |
| |
| Operating modes: |
| - off: does nothing |
| - errors: buffers data, flushes to files only on errors |
| - all: writes data immediately to files (as before) |
| """ |
| _instance = None |
|
|
| def __new__(cls): |
| if cls._instance is None: |
| cls._instance = super(DebugLogger, cls).__new__(cls) |
| cls._instance._initialized = False |
| return cls._instance |
|
|
| def __init__(self): |
| if self._initialized: |
| return |
| self.debug_dir = Path(DEBUG_DIR) |
| self._initialized = True |
| |
| |
| self._request_body_buffer: Optional[bytes] = None |
| self._kiro_request_body_buffer: Optional[bytes] = None |
| self._raw_chunks_buffer: bytearray = bytearray() |
| self._modified_chunks_buffer: bytearray = bytearray() |
| |
| |
| self._app_logs_buffer: io.StringIO = io.StringIO() |
| self._loguru_sink_id: Optional[int] = None |
| |
| def _is_enabled(self) -> bool: |
| """Checks if logging is enabled.""" |
| return DEBUG_MODE in ("errors", "all") |
| |
| def _is_immediate_write(self) -> bool: |
| """Checks if immediate file writing is needed (all mode).""" |
| return DEBUG_MODE == "all" |
| |
| def _clear_buffers(self): |
| """Clears all buffers.""" |
| self._request_body_buffer = None |
| self._kiro_request_body_buffer = None |
| self._raw_chunks_buffer.clear() |
| self._modified_chunks_buffer.clear() |
| self._clear_app_logs_buffer() |
| |
| def _clear_app_logs_buffer(self): |
| """Clears the application logs buffer and removes sink.""" |
| |
| if self._loguru_sink_id is not None: |
| try: |
| logger.remove(self._loguru_sink_id) |
| except ValueError: |
| |
| pass |
| self._loguru_sink_id = None |
| |
| |
| self._app_logs_buffer = io.StringIO() |
| |
| def _setup_app_logs_capture(self): |
| """ |
| Sets up application log capture to buffer. |
| |
| Adds a temporary sink to loguru that writes to StringIO buffer. |
| Captures ALL logs without filtering, as sink is active only |
| during processing of a specific request. |
| """ |
| |
| self._clear_app_logs_buffer() |
| |
| |
| |
| self._loguru_sink_id = logger.add( |
| self._app_logs_buffer, |
| format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} | {message}", |
| level="DEBUG", |
| colorize=False, |
| |
| ) |
|
|
| def prepare_new_request(self): |
| """ |
| Prepares the logger for a new request. |
| |
| In "all" mode: clears the logs folder. |
| In "errors" mode: clears buffers. |
| In both modes: sets up application log capture. |
| """ |
| if not self._is_enabled(): |
| return |
| |
| |
| self._clear_buffers() |
| |
| |
| self._setup_app_logs_capture() |
|
|
| if self._is_immediate_write(): |
| |
| try: |
| if self.debug_dir.exists(): |
| shutil.rmtree(self.debug_dir) |
| self.debug_dir.mkdir(parents=True, exist_ok=True) |
| logger.debug(f"[DebugLogger] Directory {self.debug_dir} cleared for new request.") |
| except Exception as e: |
| logger.error(f"[DebugLogger] Error preparing directory: {e}") |
|
|
| def log_request_body(self, body: bytes): |
| """ |
| Saves the request body (from client, OpenAI format). |
| |
| In "all" mode: writes immediately to file. |
| In "errors" mode: buffers. |
| """ |
| if not self._is_enabled(): |
| return |
|
|
| if self._is_immediate_write(): |
| self._write_request_body_to_file(body) |
| else: |
| |
| self._request_body_buffer = body |
|
|
| def log_kiro_request_body(self, body: bytes): |
| """ |
| Saves the modified request body (to Kiro API). |
| |
| In "all" mode: writes immediately to file. |
| In "errors" mode: buffers. |
| """ |
| if not self._is_enabled(): |
| return |
|
|
| if self._is_immediate_write(): |
| self._write_kiro_request_body_to_file(body) |
| else: |
| |
| self._kiro_request_body_buffer = body |
|
|
| def log_raw_chunk(self, chunk: bytes): |
| """ |
| Appends raw response chunk (from provider). |
| |
| In "all" mode: writes immediately to file. |
| In "errors" mode: buffers. |
| """ |
| if not self._is_enabled(): |
| return |
|
|
| if self._is_immediate_write(): |
| self._append_raw_chunk_to_file(chunk) |
| else: |
| |
| self._raw_chunks_buffer.extend(chunk) |
|
|
| def log_modified_chunk(self, chunk: bytes): |
| """ |
| Appends modified chunk (to client). |
| |
| In "all" mode: writes immediately to file. |
| In "errors" mode: buffers. |
| """ |
| if not self._is_enabled(): |
| return |
|
|
| if self._is_immediate_write(): |
| self._append_modified_chunk_to_file(chunk) |
| else: |
| |
| self._modified_chunks_buffer.extend(chunk) |
| |
| def log_error_info(self, status_code: int, error_message: str = ""): |
| """ |
| Writes error information to file. |
| |
| Works in both modes (errors and all). |
| In "all" mode writes immediately to file. |
| In "errors" mode called from flush_on_error(). |
| |
| Args: |
| status_code: HTTP error status code |
| error_message: Error message (optional) |
| """ |
| if not self._is_enabled(): |
| return |
| |
| try: |
| |
| self.debug_dir.mkdir(parents=True, exist_ok=True) |
| |
| error_info = { |
| "status_code": status_code, |
| "error_message": error_message |
| } |
| error_file = self.debug_dir / "error_info.json" |
| with open(error_file, "w", encoding="utf-8") as f: |
| json.dump(error_info, f, indent=2, ensure_ascii=False) |
| |
| logger.debug(f"[DebugLogger] Error info saved (status={status_code})") |
| except Exception as e: |
| logger.error(f"[DebugLogger] Error writing error_info: {e}") |
|
|
| def flush_on_error(self, status_code: int, error_message: str = ""): |
| """ |
| Flushes buffers to files on error. |
| |
| In "errors" mode: flushes buffers and saves error_info. |
| In "all" mode: only saves error_info (data already written). |
| |
| Args: |
| status_code: HTTP error status code |
| error_message: Error message (optional) |
| """ |
| if not self._is_enabled(): |
| return |
| |
| |
| if self._is_immediate_write(): |
| self.log_error_info(status_code, error_message) |
| self._write_app_logs_to_file() |
| self._clear_app_logs_buffer() |
| return |
| |
| |
| if not any([ |
| self._request_body_buffer, |
| self._kiro_request_body_buffer, |
| self._raw_chunks_buffer, |
| self._modified_chunks_buffer |
| ]): |
| return |
| |
| try: |
| |
| if self.debug_dir.exists(): |
| shutil.rmtree(self.debug_dir) |
| self.debug_dir.mkdir(parents=True, exist_ok=True) |
| |
| |
| if self._request_body_buffer: |
| self._write_request_body_to_file(self._request_body_buffer) |
| |
| if self._kiro_request_body_buffer: |
| self._write_kiro_request_body_to_file(self._kiro_request_body_buffer) |
| |
| if self._raw_chunks_buffer: |
| file_path = self.debug_dir / "response_stream_raw.txt" |
| with open(file_path, "wb") as f: |
| f.write(self._raw_chunks_buffer) |
| |
| if self._modified_chunks_buffer: |
| file_path = self.debug_dir / "response_stream_modified.txt" |
| with open(file_path, "wb") as f: |
| f.write(self._modified_chunks_buffer) |
| |
| |
| self.log_error_info(status_code, error_message) |
| |
| |
| self._write_app_logs_to_file() |
| |
| logger.info(f"[DebugLogger] Error logs flushed to {self.debug_dir} (status={status_code})") |
| |
| except Exception as e: |
| logger.error(f"[DebugLogger] Error flushing buffers: {e}") |
| finally: |
| |
| self._clear_buffers() |
| |
| def discard_buffers(self): |
| """ |
| Clears buffers without writing to files. |
| |
| Called when request completed successfully in "errors" mode. |
| Also called in "all" mode to save logs of successful request. |
| """ |
| if DEBUG_MODE == "errors": |
| self._clear_buffers() |
| elif DEBUG_MODE == "all": |
| |
| self._write_app_logs_to_file() |
| self._clear_app_logs_buffer() |
| |
| |
| |
| def _write_request_body_to_file(self, body: bytes): |
| """Writes request body to file.""" |
| try: |
| file_path = self.debug_dir / "request_body.json" |
| try: |
| json_obj = json.loads(body) |
| with open(file_path, "w", encoding="utf-8") as f: |
| json.dump(json_obj, f, indent=2, ensure_ascii=False) |
| except json.JSONDecodeError: |
| with open(file_path, "wb") as f: |
| f.write(body) |
| except Exception as e: |
| logger.error(f"[DebugLogger] Error writing request_body: {e}") |
| |
| def _write_kiro_request_body_to_file(self, body: bytes): |
| """Writes Kiro request body to file.""" |
| try: |
| file_path = self.debug_dir / "kiro_request_body.json" |
| try: |
| json_obj = json.loads(body) |
| with open(file_path, "w", encoding="utf-8") as f: |
| json.dump(json_obj, f, indent=2, ensure_ascii=False) |
| except json.JSONDecodeError: |
| with open(file_path, "wb") as f: |
| f.write(body) |
| except Exception as e: |
| logger.error(f"[DebugLogger] Error writing kiro_request_body: {e}") |
| |
| def _append_raw_chunk_to_file(self, chunk: bytes): |
| """Appends raw chunk to file.""" |
| try: |
| file_path = self.debug_dir / "response_stream_raw.txt" |
| with open(file_path, "ab") as f: |
| f.write(chunk) |
| except Exception: |
| pass |
| |
| def _append_modified_chunk_to_file(self, chunk: bytes): |
| """Appends modified chunk to file.""" |
| try: |
| file_path = self.debug_dir / "response_stream_modified.txt" |
| with open(file_path, "ab") as f: |
| f.write(chunk) |
| except Exception: |
| pass |
| |
| def _write_app_logs_to_file(self): |
| """Writes captured application logs to file.""" |
| try: |
| |
| logs_content = self._app_logs_buffer.getvalue() |
| |
| if not logs_content.strip(): |
| return |
| |
| |
| self.debug_dir.mkdir(parents=True, exist_ok=True) |
| |
| file_path = self.debug_dir / "app_logs.txt" |
| with open(file_path, "w", encoding="utf-8") as f: |
| f.write(logs_content) |
| |
| logger.debug(f"[DebugLogger] App logs saved to {file_path}") |
| except Exception as e: |
| |
| pass |
|
|
|
|
| |
| debug_logger = DebugLogger() |