API / kiro-gateway /kiro /debug_logger.py
sshinmen's picture
Clean deploy to HF Space
bf9e111
Raw
History Blame Contribute Delete
14.1 kB
# -*- coding: utf-8 -*-
# Kiro Gateway
# https://github.com/jwadow/kiro-gateway
# Copyright (C) 2025 Jwadow
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
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
# Buffers for "errors" mode
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()
# Buffer for application logs (loguru)
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."""
# Remove sink from loguru
if self._loguru_sink_id is not None:
try:
logger.remove(self._loguru_sink_id)
except ValueError:
# Sink already removed
pass
self._loguru_sink_id = None
# Clear buffer
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.
"""
# Remove previous sink if exists
self._clear_app_logs_buffer()
# Add new sink to capture ALL logs
# Format: time | level | module:function:line | message
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", # Capture all levels from DEBUG and above
colorize=False, # No ANSI colors in file
# No filter - capture ALL logs during request processing
)
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
# Clear buffers in any case
self._clear_buffers()
# Set up application log capture
self._setup_app_logs_capture()
if self._is_immediate_write():
# "all" mode - clear folder and recreate
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:
# "errors" mode - buffer
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:
# "errors" mode - buffer
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:
# "errors" mode - buffer
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:
# "errors" mode - buffer
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:
# Ensure directory exists
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
# In "all" mode data is already written, add error_info and app logs
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
# Check if there's anything to flush
if not any([
self._request_body_buffer,
self._kiro_request_body_buffer,
self._raw_chunks_buffer,
self._modified_chunks_buffer
]):
return
try:
# Create directory if not exists
if self.debug_dir.exists():
shutil.rmtree(self.debug_dir)
self.debug_dir.mkdir(parents=True, exist_ok=True)
# Flush buffers to files
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)
# Save error information
self.log_error_info(status_code, error_message)
# Save application logs
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:
# Clear buffers after flush
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":
# In "all" mode save logs even for successful requests
self._write_app_logs_to_file()
self._clear_app_logs_buffer()
# ==================== Private file writing methods ====================
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:
# Get buffer contents
logs_content = self._app_logs_buffer.getvalue()
if not logs_content.strip():
return
# Ensure directory exists
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:
# Don't log error via logger to avoid recursion
pass
# Global instance
debug_logger = DebugLogger()