4
4
import datetime
5
5
import json
6
6
import logging
7
+ import re
8
+ import hashlib
9
+ from typing import Any
10
+
7
11
import folder_paths
8
12
9
13
# Get the logger instance
10
14
logger = logging .getLogger (__name__ )
11
15
16
+
12
17
def 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."""
17
19
base_temp_dir = folder_paths .get_temp_directory ()
18
20
log_dir = os .path .join (base_temp_dir , "api_logs" )
19
21
try :
@@ -24,42 +26,77 @@ def get_log_directory():
24
26
return base_temp_dir
25
27
return log_dir
26
28
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 :
28
65
"""Helper to format data (dict, str, bytes) for logging."""
29
66
if isinstance (data , bytes ):
30
67
try :
31
- return data .decode (' utf-8' ) # Try to decode as text
68
+ return data .decode (" utf-8" ) # Try to decode as text
32
69
except UnicodeDecodeError :
33
70
return f"[Binary data of length { len (data )} bytes]"
34
71
elif isinstance (data , (dict , list )):
35
72
try :
36
73
return json .dumps (data , indent = 2 , ensure_ascii = False )
37
74
except TypeError :
38
- return str (data ) # Fallback for non-serializable objects
75
+ return str (data ) # Fallback for non-serializable objects
39
76
return str (data )
40
77
78
+
41
79
def log_request_response (
42
80
operation_id : str ,
43
81
request_method : str ,
44
82
request_url : str ,
45
83
request_headers : dict | None = None ,
46
84
request_params : dict | None = None ,
47
- request_data : any = None ,
85
+ request_data : Any = None ,
48
86
response_status_code : int | None = None ,
49
87
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 ,
52
90
):
53
91
"""
54
92
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.
55
95
"""
56
96
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 )
62
98
99
+ log_content : list [str ] = []
63
100
log_content .append (f"Timestamp: { datetime .datetime .now ().isoformat ()} " )
64
101
log_content .append (f"Operation ID: { operation_id } " )
65
102
log_content .append ("-" * 30 + " REQUEST " + "-" * 30 )
@@ -69,15 +106,15 @@ def log_request_response(
69
106
log_content .append (f"Headers:\n { _format_data_for_logging (request_headers )} " )
70
107
if request_params :
71
108
log_content .append (f"Params:\n { _format_data_for_logging (request_params )} " )
72
- if request_data :
109
+ if request_data is not None :
73
110
log_content .append (f"Data/Body:\n { _format_data_for_logging (request_data )} " )
74
111
75
112
log_content .append ("\n " + "-" * 30 + " RESPONSE " + "-" * 30 )
76
113
if response_status_code is not None :
77
114
log_content .append (f"Status Code: { response_status_code } " )
78
115
if response_headers :
79
116
log_content .append (f"Headers:\n { _format_data_for_logging (response_headers )} " )
80
- if response_content :
117
+ if response_content is not None :
81
118
log_content .append (f"Content:\n { _format_data_for_logging (response_content )} " )
82
119
if error_message :
83
120
log_content .append (f"Error:\n { error_message } " )
@@ -89,6 +126,7 @@ def log_request_response(
89
126
except Exception as e :
90
127
logger .error (f"Error writing API log to { filepath } : { e } " )
91
128
129
+
92
130
if __name__ == '__main__' :
93
131
# Example usage (for testing the logger directly)
94
132
logger .setLevel (logging .DEBUG )
0 commit comments