@@ -54,6 +54,8 @@ def __init__(
54
54
self .url : str = trace_agent_url
55
55
self ._api_key : Optional [str ] = api_key
56
56
self .ddconfig = ddconfig
57
+ # Use a fixed boundary for consistency
58
+ self ._BOUNDARY = "83CAD6AA-8A24-462C-8B3D-FF9CC683B51B"
57
59
58
60
def prepare (self , log_level : str ):
59
61
"""
@@ -63,32 +65,15 @@ def prepare(self, log_level: str):
63
65
try :
64
66
self .flare_dir .mkdir (exist_ok = True )
65
67
except Exception as e :
66
- log .error ("Failed to create %s directory: %s" , self .flare_dir , e )
68
+ log .error ("Flare prepare: failed to create %s directory: %s" , self .flare_dir , e )
67
69
return
68
70
69
71
flare_log_level_int = logging .getLevelName (log_level )
70
72
if type (flare_log_level_int ) != int :
71
- raise TypeError ("Invalid log level provided: %s" , log_level )
72
-
73
- ddlogger = get_logger ("ddtrace" )
74
- pid = os .getpid ()
75
- flare_file_path = self .flare_dir / f"tracer_python_{ pid } .log"
76
- self .original_log_level = ddlogger .level
77
-
78
- # Set the logger level to the more verbose between original and flare
79
- # We do this valid_original_level check because if the log level is NOTSET, the value is 0
80
- # which is the minimum value. In this case, we just want to use the flare level, but still
81
- # retain the original state as NOTSET/0
82
- valid_original_level = (
83
- logging .CRITICAL if self .original_log_level == logging .NOTSET else self .original_log_level
84
- )
85
- logger_level = min (valid_original_level , flare_log_level_int )
86
- ddlogger .setLevel (logger_level )
87
- self .file_handler = _add_file_handler (
88
- ddlogger , flare_file_path .__str__ (), flare_log_level_int , TRACER_FLARE_FILE_HANDLER_NAME
89
- )
73
+ raise TypeError ("Flare prepare: Invalid log level provided: %s" , log_level )
90
74
91
- # Create and add config file
75
+ # Setup logging and create config file
76
+ pid = self ._setup_flare_logging (flare_log_level_int )
92
77
self ._generate_config_file (pid )
93
78
94
79
def send (self , flare_send_req : FlareSendRequest ):
@@ -101,18 +86,9 @@ def send(self, flare_send_req: FlareSendRequest):
101
86
# Ensure the flare directory exists (it might have been deleted by clean_up_files)
102
87
self .flare_dir .mkdir (exist_ok = True )
103
88
104
- # # Validate case_id (must be a digit and cannot be 0 according to spec)
105
- if flare_send_req .case_id in ("0" , 0 ):
106
- log .warning ("Case ID cannot be 0, skipping flare send" )
107
- self .clean_up_files ()
108
- return
109
-
110
- if not flare_send_req .case_id .isdigit ():
111
- log .warning ("Case ID string must contain a digit, skipping flare send" )
112
- self .clean_up_files ()
113
- return
114
-
115
89
try :
90
+ if not self ._validate_case_id (flare_send_req .case_id ):
91
+ return
116
92
self ._send_flare_request (flare_send_req )
117
93
finally :
118
94
self .clean_up_files ()
@@ -149,69 +125,97 @@ def revert_configs(self):
149
125
log .debug ("Could not find %s to remove" , TRACER_FLARE_FILE_HANDLER_NAME )
150
126
ddlogger .setLevel (self .original_log_level )
151
127
152
- def _generate_payload (self , flare_send_req ) :
128
+ def _validate_case_id (self , case_id : str ) -> bool :
153
129
"""
154
- Generate the multipart form-data payload for the flare request.
130
+ Validate case_id (must be a digit and cannot be 0 according to spec).
131
+ Returns True if valid, False otherwise. Cleans up files if invalid.
155
132
"""
156
- body = io .BytesIO ()
157
-
158
- # Use a fixed boundary for consistency
159
- boundary = "83CAD6AA-8A24-462C-8B3D-FF9CC683B51B"
133
+ if case_id in ("0" , 0 ):
134
+ log .warning ("Case ID cannot be 0, skipping flare send" )
135
+ return False
136
+
137
+ if not case_id .isdigit ():
138
+ log .warning ("Case ID string must contain a digit, skipping flare send" )
139
+ return False
140
+
141
+ return True
160
142
161
- # Create the multipart form data in the same order:
162
- # source, case_id, hostname, email, uuid, flare_file
143
+ def _setup_flare_logging (self , flare_log_level_int : int ) -> int :
144
+ """
145
+ Setup flare logging configuration.
146
+ Returns the process ID.
147
+ """
148
+ ddlogger = get_logger ("ddtrace" )
149
+ pid = os .getpid ()
150
+ flare_file_path = self .flare_dir / f"tracer_python_{ pid } .log"
151
+ self .original_log_level = ddlogger .level
163
152
164
- # 1. source field
165
- body .write (f"--{ boundary } \r \n " .encode ())
166
- body .write (b'Content-Disposition: form-data; name="source"\r \n \r \n ' )
167
- body .write (b"tracer_python\r \n " )
153
+ # Set the logger level to the more verbose between original and flare
154
+ # We do this valid_original_level check because if the log level is NOTSET, the value is 0
155
+ # which is the minimum value. In this case, we just want to use the flare level, but still
156
+ # retain the original state as NOTSET/0
157
+ valid_original_level = (
158
+ logging .CRITICAL if self .original_log_level == logging .NOTSET else self .original_log_level
159
+ )
160
+ logger_level = min (valid_original_level , flare_log_level_int )
161
+ ddlogger .setLevel (logger_level )
162
+ self .file_handler = _add_file_handler (
163
+ ddlogger , flare_file_path .__str__ (), flare_log_level_int , TRACER_FLARE_FILE_HANDLER_NAME
164
+ )
165
+ return pid
168
166
169
- # 2. case_id field
170
- body .write (f"--{ boundary } \r \n " .encode ())
171
- body .write (b'Content-Disposition: form-data; name="case_id"\r \n \r \n ' )
172
- body .write (f"{ flare_send_req .case_id } \r \n " .encode ())
167
+ def _create_zip_content (self ) -> bytes :
168
+ """
169
+ Create ZIP file content containing all flare files.
170
+ Returns the ZIP file content as bytes.
171
+ """
172
+ zip_stream = io .BytesIO ()
173
+ with zipfile .ZipFile (zip_stream , mode = "w" , compression = zipfile .ZIP_DEFLATED ) as zipf :
174
+ for flare_file_name in self .flare_dir .iterdir ():
175
+ zipf .write (flare_file_name , arcname = flare_file_name .name )
176
+ zip_stream .seek (0 )
177
+ return zip_stream .getvalue ()
173
178
174
- # 3. hostname field
175
- body .write (f"--{ boundary } \r \n " .encode ())
176
- body .write (b'Content-Disposition: form-data; name="hostname"\r \n \r \n ' )
177
- body .write (f"{ flare_send_req .hostname } \r \n " .encode ())
179
+ def _write_body_field (self , body : io .BytesIO , name : str , value : str ):
180
+ """Write a form field to the multipart body."""
181
+ body .write (f"--{ self ._BOUNDARY } \r \n " .encode ())
182
+ body .write (f'Content-Disposition: form-data; name="{ name } "\r \n \r \n ' .encode ())
183
+ body .write (f"{ value } \r \n " .encode ())
178
184
179
- # 4. email field
180
- body . write ( f"-- { boundary } \r \n " . encode ())
181
- body . write ( b'Content-Disposition: form-data; name="email" \r \n \r \n ' )
182
- body . write ( f" { flare_send_req . email } \r \n " . encode ())
185
+ def _generate_payload ( self , flare_send_req ):
186
+ """
187
+ Generate the multipart form-data payload for the flare request.
188
+ """
183
189
184
- # 5. uuid field (new, per spec)
185
- body .write (f"--{ boundary } \r \n " .encode ())
186
- body .write (b'Content-Disposition: form-data; name="uuid"\r \n \r \n ' )
187
- body .write (f"{ flare_send_req .uuid } \r \n " .encode ())
190
+ # Create the multipart form data in the same order:
191
+ # source, case_id, hostname, email, uuid, flare_file
192
+ body = io .BytesIO ()
193
+ self ._write_body_field (body , "source" , "tracer_python" )
194
+ self ._write_body_field (body , "case_id" , flare_send_req .case_id )
195
+ self ._write_body_field (body , "hostname" , flare_send_req .hostname )
196
+ self ._write_body_field (body , "email" , flare_send_req .email )
197
+ self ._write_body_field (body , "uuid" , flare_send_req .uuid )
188
198
189
- # 6. flare_file field with descriptive filename
199
+ # flare_file field with descriptive filename
190
200
timestamp = int (time .time () * 1000 )
191
201
filename = f"tracer-python-{ flare_send_req .case_id } -{ timestamp } -debug.zip"
192
- body .write (f"--{ boundary } \r \n " .encode ())
202
+ body .write (f"--{ self . _BOUNDARY } \r \n " .encode ())
193
203
body .write (f'Content-Disposition: form-data; name="flare_file"; filename="{ filename } "\r \n ' .encode ())
194
204
body .write (b"Content-Type: application/octet-stream\r \n \r \n " )
195
205
196
206
# Create the zip file content separately
197
- zip_stream = io .BytesIO ()
198
- with zipfile .ZipFile (zip_stream , mode = "w" , compression = zipfile .ZIP_DEFLATED ) as zipf :
199
- for flare_file_name in self .flare_dir .iterdir ():
200
- zipf .write (flare_file_name , arcname = flare_file_name .name )
201
- zip_stream .seek (0 )
202
- body .write (zip_stream .getvalue ())
203
- body .write (b"\r \n " )
207
+ body .write (self ._create_zip_content () + b"\r \n " )
204
208
205
- # End boundary
206
- body .write (f"--{ boundary } --\r \n " .encode ())
209
+ # Ending boundary
210
+ body .write (f"--{ self . _BOUNDARY } --\r \n " .encode ())
207
211
208
212
# Set headers
209
213
headers = {
210
- "Content-Type" : f"multipart/form-data; boundary={ boundary } " ,
214
+ "Content-Type" : f"multipart/form-data; boundary={ self . _BOUNDARY } " ,
211
215
"Content-Length" : str (body .tell ()),
212
216
}
213
217
214
- # Don 't send DD-API-KEY or Host Header - the agent should add it when forwarding to backend
218
+ # Note: don 't send DD-API-KEY or Host Header - the agent should add it when forwarding to backend
215
219
return headers , body .getvalue ()
216
220
217
221
def _get_valid_logger_level (self , flare_log_level : int ) -> int :
0 commit comments