mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 04:10:51 +08:00
feat(logging): revamp request logger to support streaming and temporary file spooling
This update enhances the `FileRequestLogger` by introducing support for spooling large request and response bodies to temporary files, reducing memory consumption. It adds atomic requestLogID generation for sequential log naming and new methods for non-streaming/streaming log assembly. Also includes better error handling during logging and temp file cleanups.
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/andybalholm/brotli"
|
"github.com/andybalholm/brotli"
|
||||||
@@ -25,6 +26,8 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var requestLogID atomic.Uint64
|
||||||
|
|
||||||
// RequestLogger defines the interface for logging HTTP requests and responses.
|
// RequestLogger defines the interface for logging HTTP requests and responses.
|
||||||
// It provides methods for logging both regular and streaming HTTP request/response cycles.
|
// It provides methods for logging both regular and streaming HTTP request/response cycles.
|
||||||
type RequestLogger interface {
|
type RequestLogger interface {
|
||||||
@@ -204,19 +207,52 @@ func (l *FileRequestLogger) logRequest(url, method string, requestHeaders map[st
|
|||||||
}
|
}
|
||||||
filePath := filepath.Join(l.logsDir, filename)
|
filePath := filepath.Join(l.logsDir, filename)
|
||||||
|
|
||||||
// Decompress response if needed
|
requestBodyPath, errTemp := l.writeRequestBodyTempFile(body)
|
||||||
decompressedResponse, err := l.decompressResponse(responseHeaders, response)
|
if errTemp != nil {
|
||||||
if err != nil {
|
log.WithError(errTemp).Warn("failed to create request body temp file, falling back to direct write")
|
||||||
// If decompression fails, log the error but continue with original response
|
}
|
||||||
decompressedResponse = append(response, []byte(fmt.Sprintf("\n[DECOMPRESSION ERROR: %v]", err))...)
|
if requestBodyPath != "" {
|
||||||
|
defer func() {
|
||||||
|
if errRemove := os.Remove(requestBodyPath); errRemove != nil {
|
||||||
|
log.WithError(errRemove).Warn("failed to remove request body temp file")
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create log content
|
responseToWrite, decompressErr := l.decompressResponse(responseHeaders, response)
|
||||||
content := l.formatLogContent(url, method, requestHeaders, body, apiRequest, apiResponse, decompressedResponse, statusCode, responseHeaders, apiResponseErrors)
|
if decompressErr != nil {
|
||||||
|
// If decompression fails, continue with original response and annotate the log output.
|
||||||
|
responseToWrite = response
|
||||||
|
}
|
||||||
|
|
||||||
// Write to file
|
logFile, errOpen := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
if err = os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
if errOpen != nil {
|
||||||
return fmt.Errorf("failed to write log file: %w", err)
|
return fmt.Errorf("failed to create log file: %w", errOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeErr := l.writeNonStreamingLog(
|
||||||
|
logFile,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
requestHeaders,
|
||||||
|
body,
|
||||||
|
requestBodyPath,
|
||||||
|
apiRequest,
|
||||||
|
apiResponse,
|
||||||
|
apiResponseErrors,
|
||||||
|
statusCode,
|
||||||
|
responseHeaders,
|
||||||
|
responseToWrite,
|
||||||
|
decompressErr,
|
||||||
|
)
|
||||||
|
if errClose := logFile.Close(); errClose != nil {
|
||||||
|
log.WithError(errClose).Warn("failed to close request log file")
|
||||||
|
if writeErr == nil {
|
||||||
|
return errClose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if writeErr != nil {
|
||||||
|
return fmt.Errorf("failed to write log file: %w", writeErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if force && !l.enabled {
|
if force && !l.enabled {
|
||||||
@@ -253,26 +289,38 @@ func (l *FileRequestLogger) LogStreamingRequest(url, method string, headers map[
|
|||||||
filename := l.generateFilename(url)
|
filename := l.generateFilename(url)
|
||||||
filePath := filepath.Join(l.logsDir, filename)
|
filePath := filepath.Join(l.logsDir, filename)
|
||||||
|
|
||||||
// Create and open file
|
requestHeaders := make(map[string][]string, len(headers))
|
||||||
file, err := os.Create(filePath)
|
for key, values := range headers {
|
||||||
if err != nil {
|
headerValues := make([]string, len(values))
|
||||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
copy(headerValues, values)
|
||||||
|
requestHeaders[key] = headerValues
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write initial request information
|
requestBodyPath, errTemp := l.writeRequestBodyTempFile(body)
|
||||||
requestInfo := l.formatRequestInfo(url, method, headers, body)
|
if errTemp != nil {
|
||||||
if _, err = file.WriteString(requestInfo); err != nil {
|
return nil, fmt.Errorf("failed to create request body temp file: %w", errTemp)
|
||||||
_ = file.Close()
|
|
||||||
return nil, fmt.Errorf("failed to write request info: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
responseBodyFile, errCreate := os.CreateTemp(l.logsDir, "response-body-*.tmp")
|
||||||
|
if errCreate != nil {
|
||||||
|
_ = os.Remove(requestBodyPath)
|
||||||
|
return nil, fmt.Errorf("failed to create response body temp file: %w", errCreate)
|
||||||
|
}
|
||||||
|
responseBodyPath := responseBodyFile.Name()
|
||||||
|
|
||||||
// Create streaming writer
|
// Create streaming writer
|
||||||
writer := &FileStreamingLogWriter{
|
writer := &FileStreamingLogWriter{
|
||||||
file: file,
|
logFilePath: filePath,
|
||||||
chunkChan: make(chan []byte, 100), // Buffered channel for async writes
|
url: url,
|
||||||
closeChan: make(chan struct{}),
|
method: method,
|
||||||
errorChan: make(chan error, 1),
|
timestamp: time.Now(),
|
||||||
bufferedChunks: &bytes.Buffer{},
|
requestHeaders: requestHeaders,
|
||||||
|
requestBodyPath: requestBodyPath,
|
||||||
|
responseBodyPath: responseBodyPath,
|
||||||
|
responseBodyFile: responseBodyFile,
|
||||||
|
chunkChan: make(chan []byte, 100), // Buffered channel for async writes
|
||||||
|
closeChan: make(chan struct{}),
|
||||||
|
errorChan: make(chan error, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start async writer goroutine
|
// Start async writer goroutine
|
||||||
@@ -323,7 +371,9 @@ func (l *FileRequestLogger) generateFilename(url string) string {
|
|||||||
timestamp := time.Now().Format("2006-01-02T150405-.000000000")
|
timestamp := time.Now().Format("2006-01-02T150405-.000000000")
|
||||||
timestamp = strings.Replace(timestamp, ".", "", -1)
|
timestamp = strings.Replace(timestamp, ".", "", -1)
|
||||||
|
|
||||||
return fmt.Sprintf("%s-%s.log", sanitized, timestamp)
|
id := requestLogID.Add(1)
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s-%s-%d.log", sanitized, timestamp, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeForFilename replaces characters that are not safe for filenames.
|
// sanitizeForFilename replaces characters that are not safe for filenames.
|
||||||
@@ -405,6 +455,220 @@ func (l *FileRequestLogger) cleanupOldErrorLogs() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *FileRequestLogger) writeRequestBodyTempFile(body []byte) (string, error) {
|
||||||
|
tmpFile, errCreate := os.CreateTemp(l.logsDir, "request-body-*.tmp")
|
||||||
|
if errCreate != nil {
|
||||||
|
return "", errCreate
|
||||||
|
}
|
||||||
|
tmpPath := tmpFile.Name()
|
||||||
|
|
||||||
|
if _, errCopy := io.Copy(tmpFile, bytes.NewReader(body)); errCopy != nil {
|
||||||
|
_ = tmpFile.Close()
|
||||||
|
_ = os.Remove(tmpPath)
|
||||||
|
return "", errCopy
|
||||||
|
}
|
||||||
|
if errClose := tmpFile.Close(); errClose != nil {
|
||||||
|
_ = os.Remove(tmpPath)
|
||||||
|
return "", errClose
|
||||||
|
}
|
||||||
|
return tmpPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *FileRequestLogger) writeNonStreamingLog(
|
||||||
|
w io.Writer,
|
||||||
|
url, method string,
|
||||||
|
requestHeaders map[string][]string,
|
||||||
|
requestBody []byte,
|
||||||
|
requestBodyPath string,
|
||||||
|
apiRequest []byte,
|
||||||
|
apiResponse []byte,
|
||||||
|
apiResponseErrors []*interfaces.ErrorMessage,
|
||||||
|
statusCode int,
|
||||||
|
responseHeaders map[string][]string,
|
||||||
|
response []byte,
|
||||||
|
decompressErr error,
|
||||||
|
) error {
|
||||||
|
if errWrite := writeRequestInfoWithBody(w, url, method, requestHeaders, requestBody, requestBodyPath, time.Now()); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if errWrite := writeAPISection(w, "=== API REQUEST ===\n", "=== API REQUEST", apiRequest); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if errWrite := writeAPIErrorResponses(w, apiResponseErrors); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if errWrite := writeAPISection(w, "=== API RESPONSE ===\n", "=== API RESPONSE", apiResponse); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
return writeResponseSection(w, statusCode, true, responseHeaders, bytes.NewReader(response), decompressErr, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRequestInfoWithBody(
|
||||||
|
w io.Writer,
|
||||||
|
url, method string,
|
||||||
|
headers map[string][]string,
|
||||||
|
body []byte,
|
||||||
|
bodyPath string,
|
||||||
|
timestamp time.Time,
|
||||||
|
) error {
|
||||||
|
if _, errWrite := io.WriteString(w, "=== REQUEST INFO ===\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("Version: %s\n", buildinfo.Version)); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("URL: %s\n", url)); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("Method: %s\n", method)); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("Timestamp: %s\n", timestamp.Format(time.RFC3339Nano))); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, errWrite := io.WriteString(w, "=== HEADERS ===\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
for key, values := range headers {
|
||||||
|
for _, value := range values {
|
||||||
|
masked := util.MaskSensitiveHeaderValue(key, value)
|
||||||
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("%s: %s\n", key, masked)); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, errWrite := io.WriteString(w, "=== REQUEST BODY ===\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
if bodyPath != "" {
|
||||||
|
bodyFile, errOpen := os.Open(bodyPath)
|
||||||
|
if errOpen != nil {
|
||||||
|
return errOpen
|
||||||
|
}
|
||||||
|
if _, errCopy := io.Copy(w, bodyFile); errCopy != nil {
|
||||||
|
_ = bodyFile.Close()
|
||||||
|
return errCopy
|
||||||
|
}
|
||||||
|
if errClose := bodyFile.Close(); errClose != nil {
|
||||||
|
log.WithError(errClose).Warn("failed to close request body temp file")
|
||||||
|
}
|
||||||
|
} else if _, errWrite := w.Write(body); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, errWrite := io.WriteString(w, "\n\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAPISection(w io.Writer, sectionHeader string, sectionPrefix string, payload []byte) error {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.HasPrefix(payload, []byte(sectionPrefix)) {
|
||||||
|
if _, errWrite := w.Write(payload); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if !bytes.HasSuffix(payload, []byte("\n")) {
|
||||||
|
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, errWrite := io.WriteString(w, sectionHeader); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if _, errWrite := w.Write(payload); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAPIErrorResponses(w io.Writer, apiResponseErrors []*interfaces.ErrorMessage) error {
|
||||||
|
for i := 0; i < len(apiResponseErrors); i++ {
|
||||||
|
if apiResponseErrors[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, "=== API ERROR RESPONSE ===\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("HTTP Status: %d\n", apiResponseErrors[i].StatusCode)); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if apiResponseErrors[i].Error != nil {
|
||||||
|
if _, errWrite := io.WriteString(w, apiResponseErrors[i].Error.Error()); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, errWrite := io.WriteString(w, "\n\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeResponseSection(w io.Writer, statusCode int, statusWritten bool, responseHeaders map[string][]string, responseReader io.Reader, decompressErr error, trailingNewline bool) error {
|
||||||
|
if _, errWrite := io.WriteString(w, "=== RESPONSE ===\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if statusWritten {
|
||||||
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("Status: %d\n", statusCode)); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if responseHeaders != nil {
|
||||||
|
for key, values := range responseHeaders {
|
||||||
|
for _, value := range values {
|
||||||
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("%s: %s\n", key, value)); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
if responseReader != nil {
|
||||||
|
if _, errCopy := io.Copy(w, responseReader); errCopy != nil {
|
||||||
|
return errCopy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if decompressErr != nil {
|
||||||
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("\n[DECOMPRESSION ERROR: %v]", decompressErr)); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if trailingNewline {
|
||||||
|
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// formatLogContent creates the complete log content for non-streaming requests.
|
// formatLogContent creates the complete log content for non-streaming requests.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
@@ -648,13 +912,34 @@ func (l *FileRequestLogger) formatRequestInfo(url, method string, headers map[st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FileStreamingLogWriter implements StreamingLogWriter for file-based streaming logs.
|
// FileStreamingLogWriter implements StreamingLogWriter for file-based streaming logs.
|
||||||
// It handles asynchronous writing of streaming response chunks to a file.
|
// It spools streaming response chunks to a temporary file to avoid retaining large responses in memory.
|
||||||
// All data is buffered and written in the correct order when Close is called.
|
// The final log file is assembled when Close is called.
|
||||||
type FileStreamingLogWriter struct {
|
type FileStreamingLogWriter struct {
|
||||||
// file is the file where log data is written.
|
// logFilePath is the final log file path.
|
||||||
file *os.File
|
logFilePath string
|
||||||
|
|
||||||
// chunkChan is a channel for receiving response chunks to buffer.
|
// url is the request URL (masked upstream in middleware).
|
||||||
|
url string
|
||||||
|
|
||||||
|
// method is the HTTP method.
|
||||||
|
method string
|
||||||
|
|
||||||
|
// timestamp is captured when the streaming log is initialized.
|
||||||
|
timestamp time.Time
|
||||||
|
|
||||||
|
// requestHeaders stores the request headers.
|
||||||
|
requestHeaders map[string][]string
|
||||||
|
|
||||||
|
// requestBodyPath is a temporary file path holding the request body.
|
||||||
|
requestBodyPath string
|
||||||
|
|
||||||
|
// responseBodyPath is a temporary file path holding the streaming response body.
|
||||||
|
responseBodyPath string
|
||||||
|
|
||||||
|
// responseBodyFile is the temp file where chunks are appended by the async writer.
|
||||||
|
responseBodyFile *os.File
|
||||||
|
|
||||||
|
// chunkChan is a channel for receiving response chunks to spool.
|
||||||
chunkChan chan []byte
|
chunkChan chan []byte
|
||||||
|
|
||||||
// closeChan is a channel for signaling when the writer is closed.
|
// closeChan is a channel for signaling when the writer is closed.
|
||||||
@@ -663,9 +948,6 @@ type FileStreamingLogWriter struct {
|
|||||||
// errorChan is a channel for reporting errors during writing.
|
// errorChan is a channel for reporting errors during writing.
|
||||||
errorChan chan error
|
errorChan chan error
|
||||||
|
|
||||||
// bufferedChunks stores the response chunks in order.
|
|
||||||
bufferedChunks *bytes.Buffer
|
|
||||||
|
|
||||||
// responseStatus stores the HTTP status code.
|
// responseStatus stores the HTTP status code.
|
||||||
responseStatus int
|
responseStatus int
|
||||||
|
|
||||||
@@ -770,85 +1052,115 @@ func (w *FileStreamingLogWriter) Close() error {
|
|||||||
close(w.chunkChan)
|
close(w.chunkChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for async writer to finish buffering chunks
|
// Wait for async writer to finish spooling chunks
|
||||||
if w.closeChan != nil {
|
if w.closeChan != nil {
|
||||||
<-w.closeChan
|
<-w.closeChan
|
||||||
w.chunkChan = nil
|
w.chunkChan = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if w.file == nil {
|
select {
|
||||||
|
case errWrite := <-w.errorChan:
|
||||||
|
w.cleanupTempFiles()
|
||||||
|
return errWrite
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.logFilePath == "" {
|
||||||
|
w.cleanupTempFiles()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write all content in the correct order
|
logFile, errOpen := os.OpenFile(w.logFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
var content strings.Builder
|
if errOpen != nil {
|
||||||
|
w.cleanupTempFiles()
|
||||||
// 1. Write API REQUEST section
|
return fmt.Errorf("failed to create log file: %w", errOpen)
|
||||||
if len(w.apiRequest) > 0 {
|
|
||||||
if bytes.HasPrefix(w.apiRequest, []byte("=== API REQUEST")) {
|
|
||||||
content.Write(w.apiRequest)
|
|
||||||
if !bytes.HasSuffix(w.apiRequest, []byte("\n")) {
|
|
||||||
content.WriteString("\n")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content.WriteString("=== API REQUEST ===\n")
|
|
||||||
content.Write(w.apiRequest)
|
|
||||||
content.WriteString("\n")
|
|
||||||
}
|
|
||||||
content.WriteString("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Write API RESPONSE section
|
writeErr := w.writeFinalLog(logFile)
|
||||||
if len(w.apiResponse) > 0 {
|
if errClose := logFile.Close(); errClose != nil {
|
||||||
if bytes.HasPrefix(w.apiResponse, []byte("=== API RESPONSE")) {
|
log.WithError(errClose).Warn("failed to close request log file")
|
||||||
content.Write(w.apiResponse)
|
if writeErr == nil {
|
||||||
if !bytes.HasSuffix(w.apiResponse, []byte("\n")) {
|
writeErr = errClose
|
||||||
content.WriteString("\n")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content.WriteString("=== API RESPONSE ===\n")
|
|
||||||
content.Write(w.apiResponse)
|
|
||||||
content.WriteString("\n")
|
|
||||||
}
|
|
||||||
content.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Write RESPONSE section (status, headers, buffered chunks)
|
|
||||||
content.WriteString("=== RESPONSE ===\n")
|
|
||||||
if w.statusWritten {
|
|
||||||
content.WriteString(fmt.Sprintf("Status: %d\n", w.responseStatus))
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, values := range w.responseHeaders {
|
|
||||||
for _, value := range values {
|
|
||||||
content.WriteString(fmt.Sprintf("%s: %s\n", key, value))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content.WriteString("\n")
|
|
||||||
|
|
||||||
// Write buffered response body chunks
|
w.cleanupTempFiles()
|
||||||
if w.bufferedChunks != nil && w.bufferedChunks.Len() > 0 {
|
return writeErr
|
||||||
content.Write(w.bufferedChunks.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the complete content to file
|
|
||||||
if _, err := w.file.WriteString(content.String()); err != nil {
|
|
||||||
_ = w.file.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return w.file.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// asyncWriter runs in a goroutine to buffer chunks from the channel.
|
// asyncWriter runs in a goroutine to buffer chunks from the channel.
|
||||||
// It continuously reads chunks from the channel and buffers them for later writing.
|
// It continuously reads chunks from the channel and appends them to a temp file for later assembly.
|
||||||
func (w *FileStreamingLogWriter) asyncWriter() {
|
func (w *FileStreamingLogWriter) asyncWriter() {
|
||||||
defer close(w.closeChan)
|
defer close(w.closeChan)
|
||||||
|
|
||||||
for chunk := range w.chunkChan {
|
for chunk := range w.chunkChan {
|
||||||
if w.bufferedChunks != nil {
|
if w.responseBodyFile == nil {
|
||||||
w.bufferedChunks.Write(chunk)
|
continue
|
||||||
}
|
}
|
||||||
|
if _, errWrite := w.responseBodyFile.Write(chunk); errWrite != nil {
|
||||||
|
select {
|
||||||
|
case w.errorChan <- errWrite:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if errClose := w.responseBodyFile.Close(); errClose != nil {
|
||||||
|
select {
|
||||||
|
case w.errorChan <- errClose:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.responseBodyFile = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.responseBodyFile == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errClose := w.responseBodyFile.Close(); errClose != nil {
|
||||||
|
select {
|
||||||
|
case w.errorChan <- errClose:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.responseBodyFile = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FileStreamingLogWriter) writeFinalLog(logFile *os.File) error {
|
||||||
|
if errWrite := writeRequestInfoWithBody(logFile, w.url, w.method, w.requestHeaders, nil, w.requestBodyPath, w.timestamp); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if errWrite := writeAPISection(logFile, "=== API REQUEST ===\n", "=== API REQUEST", w.apiRequest); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
if errWrite := writeAPISection(logFile, "=== API RESPONSE ===\n", "=== API RESPONSE", w.apiResponse); errWrite != nil {
|
||||||
|
return errWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBodyFile, errOpen := os.Open(w.responseBodyPath)
|
||||||
|
if errOpen != nil {
|
||||||
|
return errOpen
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if errClose := responseBodyFile.Close(); errClose != nil {
|
||||||
|
log.WithError(errClose).Warn("failed to close response body temp file")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return writeResponseSection(logFile, w.responseStatus, w.statusWritten, w.responseHeaders, responseBodyFile, nil, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FileStreamingLogWriter) cleanupTempFiles() {
|
||||||
|
if w.requestBodyPath != "" {
|
||||||
|
if errRemove := os.Remove(w.requestBodyPath); errRemove != nil {
|
||||||
|
log.WithError(errRemove).Warn("failed to remove request body temp file")
|
||||||
|
}
|
||||||
|
w.requestBodyPath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.responseBodyPath != "" {
|
||||||
|
if errRemove := os.Remove(w.responseBodyPath); errRemove != nil {
|
||||||
|
log.WithError(errRemove).Warn("failed to remove response body temp file")
|
||||||
|
}
|
||||||
|
w.responseBodyPath = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user