77# @author bnbong bbbong9@gmail.com
88# --------------------------------------------------------------------------
99import logging
10+ import os
11+ import sys
12+ from datetime import datetime
1013from typing import Union
1114
1215from rich .console import Console
1518from fastapi_fastkit .core .settings import FastkitConfig
1619
1720
21+ class DebugFileHandler (logging .Handler ):
22+ """Custom logging handler for debug mode that captures all output."""
23+
24+ def __init__ (self , log_file_path : str ):
25+ super ().__init__ ()
26+ self .log_file_path = log_file_path
27+ # Ensure log directory exists
28+ os .makedirs (os .path .dirname (log_file_path ), exist_ok = True )
29+
30+ def emit (self , record : logging .LogRecord ) -> None :
31+ try :
32+ log_entry = self .format (record )
33+ with open (self .log_file_path , "a" , encoding = "utf-8" ) as f :
34+ f .write (f"{ log_entry } \n " )
35+ except Exception :
36+ self .handleError (record )
37+
38+
39+ class DebugOutputCapture :
40+ """Captures stdout and stderr to log file in debug mode."""
41+
42+ def __init__ (self , log_file_path : str ):
43+ self .log_file_path = log_file_path
44+ self .original_stdout = sys .stdout
45+ self .original_stderr = sys .stderr
46+
47+ def __enter__ (self ) -> "DebugOutputCapture" :
48+ # Create a custom writer that writes to both original output and log file
49+ class TeeWriter :
50+ def __init__ (
51+ self , original : object , log_file : str , stream_type : str
52+ ) -> None :
53+ self .original = original
54+ self .log_file = log_file
55+ self .stream_type = stream_type
56+
57+ def write (self , text : str ) -> None :
58+ # Write to original stream
59+ if hasattr (self .original , "write" ) and hasattr (self .original , "flush" ):
60+ self .original .write (text )
61+ self .original .flush ()
62+
63+ # Write to log file with timestamp and stream type
64+ if text .strip (): # Only log non-empty lines
65+ timestamp = datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
66+ try :
67+ with open (self .log_file , "a" , encoding = "utf-8" ) as f :
68+ f .write (f"[{ timestamp } ] [{ self .stream_type } ] { text } " )
69+ if not text .endswith ("\n " ):
70+ f .write ("\n " )
71+ except Exception :
72+ pass # Fail silently if logging fails
73+
74+ def flush (self ) -> None :
75+ if hasattr (self .original , "flush" ):
76+ self .original .flush ()
77+
78+ sys .stdout = TeeWriter (self .original_stdout , self .log_file_path , "STDOUT" )
79+ sys .stderr = TeeWriter (self .original_stderr , self .log_file_path , "STDERR" )
80+ return self
81+
82+ def __exit__ (self , exc_type : object , exc_val : object , exc_tb : object ) -> None :
83+ sys .stdout = self .original_stdout
84+ sys .stderr = self .original_stderr
85+
86+
1887def setup_logging (
1988 settings : FastkitConfig , terminal_width : Union [int , None ] = None
20- ) -> None :
89+ ) -> Union [DebugOutputCapture , None ]:
90+ """
91+ Setup logging for fastapi-fastkit.
92+
93+ :param settings: FastkitConfig instance
94+ :param terminal_width: Optional terminal width for Rich console
95+ :return: DebugOutputCapture instance if debug mode is enabled, None otherwise
96+ """
2197 logger = logging .getLogger ("fastapi-fastkit" )
2298 console = Console (width = terminal_width ) if terminal_width else None
2399 formatter = logging .Formatter ("%(message)s" )
@@ -34,3 +110,38 @@ def setup_logging(
34110
35111 logger .setLevel (settings .LOGGING_LEVEL )
36112 logger .propagate = False
113+
114+ # Clear existing handlers
115+ logger .handlers .clear ()
116+ logger .addHandler (rich_handler )
117+
118+ # If debug mode is enabled, add file logging
119+ debug_capture = None
120+ if settings .DEBUG_MODE :
121+ # Create logs directory in the package source
122+ logs_dir = os .path .join (os .path .dirname (os .path .dirname (__file__ )), "logs" )
123+ timestamp = datetime .now ().strftime ("%Y%m%d_%H%M%S" )
124+ log_file_path = os .path .join (logs_dir , f"fastkit_debug_{ timestamp } .log" )
125+
126+ # Add file handler for logger
127+ file_handler = DebugFileHandler (log_file_path )
128+ file_formatter = logging .Formatter (
129+ "[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s" ,
130+ datefmt = "%Y-%m-%d %H:%M:%S" ,
131+ )
132+ file_handler .setFormatter (file_formatter )
133+ logger .addHandler (file_handler )
134+
135+ # Create output capture for stdout/stderr
136+ debug_capture = DebugOutputCapture (log_file_path )
137+
138+ # Log the start of debug session
139+ logger .info (f"Debug mode enabled. Logging to: { log_file_path } " )
140+ logger .info (f"FastAPI-fastkit CLI session started at { datetime .now ()} " )
141+
142+ return debug_capture
143+
144+
145+ def get_logger (name : str = "fastapi-fastkit" ) -> logging .Logger :
146+ """Get a logger instance."""
147+ return logging .getLogger (name )
0 commit comments