44import datetime
55import json
66import logging
7+ import re
8+ import hashlib
9+ from typing import Any
10+
711import folder_paths
812
913# Get the logger instance
1014logger = logging .getLogger (__name__ )
1115
16+
1217def get_log_directory ():
13- """
14- Ensures the API log directory exists within ComfyUI's temp directory
15- and returns its path.
16- """
18+ """Ensures the API log directory exists within ComfyUI's temp directory and returns its path."""
1719 base_temp_dir = folder_paths .get_temp_directory ()
1820 log_dir = os .path .join (base_temp_dir , "api_logs" )
1921 try :
@@ -24,42 +26,77 @@ def get_log_directory():
2426 return base_temp_dir
2527 return log_dir
2628
27- def _format_data_for_logging (data ):
29+
30+ def _sanitize_filename_component (name : str ) -> str :
31+ if not name :
32+ return "log"
33+ sanitized = re .sub (r"[^A-Za-z0-9._-]+" , "_" , name ) # Replace disallowed characters with underscore
34+ sanitized = sanitized .strip (" ._" ) # Windows: trailing dots or spaces are not allowed
35+ if not sanitized :
36+ sanitized = "log"
37+ return sanitized
38+
39+
40+ def _short_hash (* parts : str , length : int = 10 ) -> str :
41+ return hashlib .sha1 (("|" .join (parts )).encode ("utf-8" )).hexdigest ()[:length ]
42+
43+
44+ def _build_log_filepath (log_dir : str , operation_id : str , request_url : str ) -> str :
45+ """Build log filepath. We keep it well under common path length limits aiming for <= 240 characters total."""
46+ timestamp = datetime .datetime .now ().strftime ("%Y%m%d_%H%M%S_%f" )
47+ slug = _sanitize_filename_component (operation_id ) # Best-effort human-readable slug from operation_id
48+ h = _short_hash (operation_id or "" , request_url or "" ) # Short hash ties log to the full operation and URL
49+
50+ # Compute how much room we have for the slug given the directory length
51+ # Keep total path length reasonably below ~260 on Windows.
52+ max_total_path = 240
53+ prefix = f"{ timestamp } _"
54+ suffix = f"_{ h } .log"
55+ if not slug :
56+ slug = "op"
57+ max_filename_len = max (60 , max_total_path - len (log_dir ) - 1 )
58+ max_slug_len = max (8 , max_filename_len - len (prefix ) - len (suffix ))
59+ if len (slug ) > max_slug_len :
60+ slug = slug [:max_slug_len ].rstrip (" ._-" )
61+ return os .path .join (log_dir , f"{ prefix } { slug } { suffix } " )
62+
63+
64+ def _format_data_for_logging (data : Any ) -> str :
2865 """Helper to format data (dict, str, bytes) for logging."""
2966 if isinstance (data , bytes ):
3067 try :
31- return data .decode (' utf-8' ) # Try to decode as text
68+ return data .decode (" utf-8" ) # Try to decode as text
3269 except UnicodeDecodeError :
3370 return f"[Binary data of length { len (data )} bytes]"
3471 elif isinstance (data , (dict , list )):
3572 try :
3673 return json .dumps (data , indent = 2 , ensure_ascii = False )
3774 except TypeError :
38- return str (data ) # Fallback for non-serializable objects
75+ return str (data ) # Fallback for non-serializable objects
3976 return str (data )
4077
78+
4179def log_request_response (
4280 operation_id : str ,
4381 request_method : str ,
4482 request_url : str ,
4583 request_headers : dict | None = None ,
4684 request_params : dict | None = None ,
47- request_data : any = None ,
85+ request_data : Any = None ,
4886 response_status_code : int | None = None ,
4987 response_headers : dict | None = None ,
50- response_content : any = None ,
51- error_message : str | None = None
88+ response_content : Any = None ,
89+ error_message : str | None = None ,
5290):
5391 """
5492 Logs API request and response details to a file in the temp/api_logs directory.
93+ Filenames are sanitized and length-limited for cross-platform safety.
94+ If we still fail to write, we fall back to appending into api.log.
5595 """
5696 log_dir = get_log_directory ()
57- timestamp = datetime .datetime .now ().strftime ("%Y%m%d_%H%M%S_%f" )
58- filename = f"{ timestamp } _{ operation_id .replace ('/' , '_' ).replace (':' , '_' )} .log"
59- filepath = os .path .join (log_dir , filename )
60-
61- log_content = []
97+ filepath = _build_log_filepath (log_dir , operation_id , request_url )
6298
99+ log_content : list [str ] = []
63100 log_content .append (f"Timestamp: { datetime .datetime .now ().isoformat ()} " )
64101 log_content .append (f"Operation ID: { operation_id } " )
65102 log_content .append ("-" * 30 + " REQUEST " + "-" * 30 )
@@ -69,15 +106,15 @@ def log_request_response(
69106 log_content .append (f"Headers:\n { _format_data_for_logging (request_headers )} " )
70107 if request_params :
71108 log_content .append (f"Params:\n { _format_data_for_logging (request_params )} " )
72- if request_data :
109+ if request_data is not None :
73110 log_content .append (f"Data/Body:\n { _format_data_for_logging (request_data )} " )
74111
75112 log_content .append ("\n " + "-" * 30 + " RESPONSE " + "-" * 30 )
76113 if response_status_code is not None :
77114 log_content .append (f"Status Code: { response_status_code } " )
78115 if response_headers :
79116 log_content .append (f"Headers:\n { _format_data_for_logging (response_headers )} " )
80- if response_content :
117+ if response_content is not None :
81118 log_content .append (f"Content:\n { _format_data_for_logging (response_content )} " )
82119 if error_message :
83120 log_content .append (f"Error:\n { error_message } " )
@@ -89,6 +126,7 @@ def log_request_response(
89126 except Exception as e :
90127 logger .error (f"Error writing API log to { filepath } : { e } " )
91128
129+
92130if __name__ == '__main__' :
93131 # Example usage (for testing the logger directly)
94132 logger .setLevel (logging .DEBUG )
0 commit comments