@@ -3,6 +3,7 @@ package report
33import  (
44	"bytes" 
55	"context" 
6+ 	"encoding/json" 
67	"fmt" 
78	"io" 
89	"math/rand" 
@@ -104,6 +105,7 @@ func (s *Sender) StartDraining() {
104105}
105106
106107// drainLoop continuously drains the buffer with random delays 
108+ // Batches up to 5 reports per request for efficiency 
107109func  (s  * Sender ) drainLoop () {
108110	for  {
109111		// Check if context is cancelled 
@@ -128,19 +130,95 @@ func (s *Sender) drainLoop() {
128130			continue 
129131		}
130132
131- 		// Process oldest file 
132- 		filePath  :=  files [0 ]
133- 		if  err  :=  s .processBufferFile (filePath ); err  !=  nil  {
134- 			// Failed to send - keep file and retry after delay 
135- 			logger .Debug ("Failed to process buffer file, will retry" , logger .String ("file" , filePath ), logger .Err (err ))
133+ 		// Determine batch size: up to configured batch_size 
134+ 		batchSize  :=  len (files )
135+ 		if  batchSize  >  s .config .Buffer .BatchSize  {
136+ 			batchSize  =  s .config .Buffer .BatchSize 
137+ 		}
138+ 
139+ 		// Process batch of files (oldest first) 
140+ 		batch  :=  files [:batchSize ]
141+ 		if  err  :=  s .processBatch (batch ); err  !=  nil  {
142+ 			// Failed to send - keep files and retry after delay 
143+ 			logger .Debug ("Failed to process batch, will retry" , logger .Int ("batch_size" , batchSize ), logger .Err (err ))
136144		}
137145
138146		// Wait random delay before next attempt 
139147		s .randomDelay ()
140148	}
141149}
142150
143- // processBufferFile attempts to send all reports from a buffer file 
151+ // processBatch loads and sends a batch of buffer files as a single array request 
152+ // Returns error if send fails (files are kept for retry) 
153+ // Handles corrupted files by sending N/A markers 
154+ func  (s  * Sender ) processBatch (filePaths  []string ) error  {
155+ 	var  reports  []* metrics.Report 
156+ 	var  validFiles  []string 
157+ 
158+ 	// Load all reports from the batch 
159+ 	for  _ , filePath  :=  range  filePaths  {
160+ 		fileReports , err  :=  s .buffer .LoadFile (filePath )
161+ 		if  err  !=  nil  {
162+ 			// File is corrupted - send N/A marker and delete 
163+ 			logger .Warn ("Corrupted buffer file detected in batch, sending N/A metrics" ,
164+ 				logger .String ("file" , filePath ),
165+ 				logger .Err (err ))
166+ 
167+ 			// Create N/A report for this corrupted file 
168+ 			naReport  :=  s .createNAReport ()
169+ 			reports  =  append (reports , naReport )
170+ 
171+ 			// Delete corrupted file immediately 
172+ 			if  delErr  :=  s .buffer .DeleteFile (filePath ); delErr  !=  nil  {
173+ 				logger .Error ("Failed to delete corrupted buffer file" ,
174+ 					logger .String ("file" , filePath ),
175+ 					logger .Err (delErr ))
176+ 			} else  {
177+ 				logger .Info ("Deleted corrupted buffer file" , logger .String ("file" , filePath ))
178+ 			}
179+ 			continue 
180+ 		}
181+ 
182+ 		// File loaded successfully - add to batch 
183+ 		if  len (fileReports ) >  0  {
184+ 			reports  =  append (reports , fileReports ... )
185+ 			validFiles  =  append (validFiles , filePath )
186+ 		}
187+ 	}
188+ 
189+ 	// If no reports to send, we're done 
190+ 	if  len (reports ) ==  0  {
191+ 		return  nil 
192+ 	}
193+ 
194+ 	// Send batch as array 
195+ 	if  err  :=  s .sendBatch (reports ); err  !=  nil  {
196+ 		// Send failed - keep valid files for retry 
197+ 		return  fmt .Errorf ("failed to send batch of %d reports: %w" , len (reports ), err )
198+ 	}
199+ 
200+ 	// Send succeeded - delete all valid files 
201+ 	for  _ , filePath  :=  range  validFiles  {
202+ 		if  err  :=  s .buffer .DeleteFile (filePath ); err  !=  nil  {
203+ 			logger .Error ("Failed to delete buffer file after successful send" ,
204+ 				logger .String ("file" , filePath ),
205+ 				logger .Err (err ))
206+ 		}
207+ 	}
208+ 
209+ 	logger .Info ("Successfully sent batch" ,
210+ 		logger .Int ("reports" , len (reports )),
211+ 		logger .Int ("files" , len (validFiles )))
212+ 
213+ 	// Periodically clean up old buffer files 
214+ 	if  err  :=  s .buffer .Cleanup (); err  !=  nil  {
215+ 		logger .Warn ("Failed to cleanup old buffer files" , logger .Err (err ))
216+ 	}
217+ 
218+ 	return  nil 
219+ }
220+ 
221+ // processBufferFile attempts to send all reports from a buffer file (DEPRECATED - kept for compatibility) 
144222// Returns error if any report fails to send (file is kept for retry) 
145223// If file is corrupted, sends N/A metrics and deletes the corrupted file 
146224func  (s  * Sender ) processBufferFile (filePath  string ) error  {
@@ -212,6 +290,43 @@ func (s *Sender) processBufferFile(filePath string) error {
212290	return  nil 
213291}
214292
293+ // sendBatch sends an array of reports to the server 
294+ func  (s  * Sender ) sendBatch (reports  []* metrics.Report ) error  {
295+ 	// Marshal array of reports to JSON 
296+ 	data , err  :=  json .Marshal (reports )
297+ 	if  err  !=  nil  {
298+ 		return  fmt .Errorf ("failed to marshal batch: %w" , err )
299+ 	}
300+ 
301+ 	// Send via HTTP 
302+ 	if  err  :=  s .sendHTTP (data ); err  !=  nil  {
303+ 		return  fmt .Errorf ("failed to send batch: %w" , err )
304+ 	}
305+ 
306+ 	return  nil 
307+ }
308+ 
309+ // createNAReport creates a report with all metrics set to null (N/A) 
310+ func  (s  * Sender ) createNAReport () * metrics.Report  {
311+ 	hostname , err  :=  os .Hostname ()
312+ 	if  err  !=  nil  {
313+ 		hostname  =  "unknown" 
314+ 	}
315+ 
316+ 	return  & metrics.Report {
317+ 		ServerID :   s .config .Agent .ServerID ,
318+ 		Timestamp :  time .Now ().UTC ().Format (time .RFC3339 ),
319+ 		Hostname :   hostname ,
320+ 		SystemInfo : nil ,
321+ 		CPU :        nil ,
322+ 		Memory :     nil ,
323+ 		Disk :       nil ,
324+ 		Network :    nil ,
325+ 		Uptime :     nil ,
326+ 		Processes :  nil ,
327+ 	}
328+ }
329+ 
215330// sendCorruptedFileMarker sends a report with all metrics set to null (N/A) 
216331// This keeps the timeline intact when a corrupted buffer file is encountered 
217332func  (s  * Sender ) sendCorruptedFileMarker (filePath  string ) error  {
0 commit comments