|
|
|
|
|
|
|
|
|
|
|
package logging |
|
|
|
|
|
import ( |
|
|
"bytes" |
|
|
"compress/flate" |
|
|
"compress/gzip" |
|
|
"fmt" |
|
|
"io" |
|
|
"os" |
|
|
"path/filepath" |
|
|
"regexp" |
|
|
"sort" |
|
|
"strings" |
|
|
"sync/atomic" |
|
|
"time" |
|
|
|
|
|
"github.com/andybalholm/brotli" |
|
|
"github.com/klauspost/compress/zstd" |
|
|
log "github.com/sirupsen/logrus" |
|
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo" |
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" |
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" |
|
|
) |
|
|
|
|
|
var requestLogID atomic.Uint64 |
|
|
|
|
|
|
|
|
|
|
|
type RequestLogger interface { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LogRequest(url, method string, requestHeaders map[string][]string, body []byte, statusCode int, responseHeaders map[string][]string, response, apiRequest, apiResponse []byte, apiResponseErrors []*interfaces.ErrorMessage, requestID string) error |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LogStreamingRequest(url, method string, headers map[string][]string, body []byte, requestID string) (StreamingLogWriter, error) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
IsEnabled() bool |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type StreamingLogWriter interface { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
WriteChunkAsync(chunk []byte) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
WriteStatus(status int, headers map[string][]string) error |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
WriteAPIRequest(apiRequest []byte) error |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
WriteAPIResponse(apiResponse []byte) error |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Close() error |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type FileRequestLogger struct { |
|
|
|
|
|
enabled bool |
|
|
|
|
|
|
|
|
logsDir string |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func NewFileRequestLogger(enabled bool, logsDir string, configDir string) *FileRequestLogger { |
|
|
|
|
|
if !filepath.IsAbs(logsDir) { |
|
|
|
|
|
if configDir != "" { |
|
|
logsDir = filepath.Join(configDir, logsDir) |
|
|
} |
|
|
} |
|
|
return &FileRequestLogger{ |
|
|
enabled: enabled, |
|
|
logsDir: logsDir, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) IsEnabled() bool { |
|
|
return l.enabled |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) SetEnabled(enabled bool) { |
|
|
l.enabled = enabled |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) LogRequest(url, method string, requestHeaders map[string][]string, body []byte, statusCode int, responseHeaders map[string][]string, response, apiRequest, apiResponse []byte, apiResponseErrors []*interfaces.ErrorMessage, requestID string) error { |
|
|
return l.logRequest(url, method, requestHeaders, body, statusCode, responseHeaders, response, apiRequest, apiResponse, apiResponseErrors, false, requestID) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) LogRequestWithOptions(url, method string, requestHeaders map[string][]string, body []byte, statusCode int, responseHeaders map[string][]string, response, apiRequest, apiResponse []byte, apiResponseErrors []*interfaces.ErrorMessage, force bool, requestID string) error { |
|
|
return l.logRequest(url, method, requestHeaders, body, statusCode, responseHeaders, response, apiRequest, apiResponse, apiResponseErrors, force, requestID) |
|
|
} |
|
|
|
|
|
func (l *FileRequestLogger) logRequest(url, method string, requestHeaders map[string][]string, body []byte, statusCode int, responseHeaders map[string][]string, response, apiRequest, apiResponse []byte, apiResponseErrors []*interfaces.ErrorMessage, force bool, requestID string) error { |
|
|
if !l.enabled && !force { |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
if errEnsure := l.ensureLogsDir(); errEnsure != nil { |
|
|
return fmt.Errorf("failed to create logs directory: %w", errEnsure) |
|
|
} |
|
|
|
|
|
|
|
|
filename := l.generateFilename(url, requestID) |
|
|
if force && !l.enabled { |
|
|
filename = l.generateErrorFilename(url, requestID) |
|
|
} |
|
|
filePath := filepath.Join(l.logsDir, filename) |
|
|
|
|
|
requestBodyPath, errTemp := l.writeRequestBodyTempFile(body) |
|
|
if errTemp != nil { |
|
|
log.WithError(errTemp).Warn("failed to create request body temp file, falling back to direct write") |
|
|
} |
|
|
if requestBodyPath != "" { |
|
|
defer func() { |
|
|
if errRemove := os.Remove(requestBodyPath); errRemove != nil { |
|
|
log.WithError(errRemove).Warn("failed to remove request body temp file") |
|
|
} |
|
|
}() |
|
|
} |
|
|
|
|
|
responseToWrite, decompressErr := l.decompressResponse(responseHeaders, response) |
|
|
if decompressErr != nil { |
|
|
|
|
|
responseToWrite = response |
|
|
} |
|
|
|
|
|
logFile, errOpen := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) |
|
|
if errOpen != nil { |
|
|
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 errCleanup := l.cleanupOldErrorLogs(); errCleanup != nil { |
|
|
log.WithError(errCleanup).Warn("failed to clean up old error logs") |
|
|
} |
|
|
} |
|
|
|
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) LogStreamingRequest(url, method string, headers map[string][]string, body []byte, requestID string) (StreamingLogWriter, error) { |
|
|
if !l.enabled { |
|
|
return &NoOpStreamingLogWriter{}, nil |
|
|
} |
|
|
|
|
|
|
|
|
if err := l.ensureLogsDir(); err != nil { |
|
|
return nil, fmt.Errorf("failed to create logs directory: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
filename := l.generateFilename(url, requestID) |
|
|
filePath := filepath.Join(l.logsDir, filename) |
|
|
|
|
|
requestHeaders := make(map[string][]string, len(headers)) |
|
|
for key, values := range headers { |
|
|
headerValues := make([]string, len(values)) |
|
|
copy(headerValues, values) |
|
|
requestHeaders[key] = headerValues |
|
|
} |
|
|
|
|
|
requestBodyPath, errTemp := l.writeRequestBodyTempFile(body) |
|
|
if errTemp != nil { |
|
|
return nil, fmt.Errorf("failed to create request body temp file: %w", errTemp) |
|
|
} |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
writer := &FileStreamingLogWriter{ |
|
|
logFilePath: filePath, |
|
|
url: url, |
|
|
method: method, |
|
|
timestamp: time.Now(), |
|
|
requestHeaders: requestHeaders, |
|
|
requestBodyPath: requestBodyPath, |
|
|
responseBodyPath: responseBodyPath, |
|
|
responseBodyFile: responseBodyFile, |
|
|
chunkChan: make(chan []byte, 100), |
|
|
closeChan: make(chan struct{}), |
|
|
errorChan: make(chan error, 1), |
|
|
} |
|
|
|
|
|
|
|
|
go writer.asyncWriter() |
|
|
|
|
|
return writer, nil |
|
|
} |
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) generateErrorFilename(url string, requestID ...string) string { |
|
|
return fmt.Sprintf("error-%s", l.generateFilename(url, requestID...)) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) ensureLogsDir() error { |
|
|
if _, err := os.Stat(l.logsDir); os.IsNotExist(err) { |
|
|
return os.MkdirAll(l.logsDir, 0755) |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) generateFilename(url string, requestID ...string) string { |
|
|
|
|
|
path := url |
|
|
if strings.Contains(url, "?") { |
|
|
path = strings.Split(url, "?")[0] |
|
|
} |
|
|
|
|
|
|
|
|
if strings.HasPrefix(path, "/") { |
|
|
path = path[1:] |
|
|
} |
|
|
|
|
|
|
|
|
sanitized := l.sanitizeForFilename(path) |
|
|
|
|
|
|
|
|
timestamp := time.Now().Format("2006-01-02T150405") |
|
|
|
|
|
|
|
|
var idPart string |
|
|
if len(requestID) > 0 && requestID[0] != "" { |
|
|
idPart = requestID[0] |
|
|
} else { |
|
|
id := requestLogID.Add(1) |
|
|
idPart = fmt.Sprintf("%d", id) |
|
|
} |
|
|
|
|
|
return fmt.Sprintf("%s-%s-%s.log", sanitized, timestamp, idPart) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) sanitizeForFilename(path string) string { |
|
|
|
|
|
sanitized := strings.ReplaceAll(path, "/", "-") |
|
|
|
|
|
|
|
|
sanitized = strings.ReplaceAll(sanitized, ":", "-") |
|
|
|
|
|
|
|
|
reg := regexp.MustCompile(`[<>:"|?*\s]`) |
|
|
sanitized = reg.ReplaceAllString(sanitized, "-") |
|
|
|
|
|
|
|
|
reg = regexp.MustCompile(`-+`) |
|
|
sanitized = reg.ReplaceAllString(sanitized, "-") |
|
|
|
|
|
|
|
|
sanitized = strings.Trim(sanitized, "-") |
|
|
|
|
|
|
|
|
if sanitized == "" { |
|
|
sanitized = "root" |
|
|
} |
|
|
|
|
|
return sanitized |
|
|
} |
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) cleanupOldErrorLogs() error { |
|
|
entries, errRead := os.ReadDir(l.logsDir) |
|
|
if errRead != nil { |
|
|
return errRead |
|
|
} |
|
|
|
|
|
type logFile struct { |
|
|
name string |
|
|
modTime time.Time |
|
|
} |
|
|
|
|
|
var files []logFile |
|
|
for _, entry := range entries { |
|
|
if entry.IsDir() { |
|
|
continue |
|
|
} |
|
|
name := entry.Name() |
|
|
if !strings.HasPrefix(name, "error-") || !strings.HasSuffix(name, ".log") { |
|
|
continue |
|
|
} |
|
|
info, errInfo := entry.Info() |
|
|
if errInfo != nil { |
|
|
log.WithError(errInfo).Warn("failed to read error log info") |
|
|
continue |
|
|
} |
|
|
files = append(files, logFile{name: name, modTime: info.ModTime()}) |
|
|
} |
|
|
|
|
|
if len(files) <= 10 { |
|
|
return nil |
|
|
} |
|
|
|
|
|
sort.Slice(files, func(i, j int) bool { |
|
|
return files[i].modTime.After(files[j].modTime) |
|
|
}) |
|
|
|
|
|
for _, file := range files[10:] { |
|
|
if errRemove := os.Remove(filepath.Join(l.logsDir, file.name)); errRemove != nil { |
|
|
log.WithError(errRemove).Warnf("failed to remove old error log: %s", file.name) |
|
|
} |
|
|
} |
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) formatLogContent(url, method string, headers map[string][]string, body, apiRequest, apiResponse, response []byte, status int, responseHeaders map[string][]string, apiResponseErrors []*interfaces.ErrorMessage) string { |
|
|
var content strings.Builder |
|
|
|
|
|
|
|
|
content.WriteString(l.formatRequestInfo(url, method, headers, body)) |
|
|
|
|
|
if len(apiRequest) > 0 { |
|
|
if bytes.HasPrefix(apiRequest, []byte("=== API REQUEST")) { |
|
|
content.Write(apiRequest) |
|
|
if !bytes.HasSuffix(apiRequest, []byte("\n")) { |
|
|
content.WriteString("\n") |
|
|
} |
|
|
} else { |
|
|
content.WriteString("=== API REQUEST ===\n") |
|
|
content.Write(apiRequest) |
|
|
content.WriteString("\n") |
|
|
} |
|
|
content.WriteString("\n") |
|
|
} |
|
|
|
|
|
for i := 0; i < len(apiResponseErrors); i++ { |
|
|
content.WriteString("=== API ERROR RESPONSE ===\n") |
|
|
content.WriteString(fmt.Sprintf("HTTP Status: %d\n", apiResponseErrors[i].StatusCode)) |
|
|
content.WriteString(apiResponseErrors[i].Error.Error()) |
|
|
content.WriteString("\n\n") |
|
|
} |
|
|
|
|
|
if len(apiResponse) > 0 { |
|
|
if bytes.HasPrefix(apiResponse, []byte("=== API RESPONSE")) { |
|
|
content.Write(apiResponse) |
|
|
if !bytes.HasSuffix(apiResponse, []byte("\n")) { |
|
|
content.WriteString("\n") |
|
|
} |
|
|
} else { |
|
|
content.WriteString("=== API RESPONSE ===\n") |
|
|
content.Write(apiResponse) |
|
|
content.WriteString("\n") |
|
|
} |
|
|
content.WriteString("\n") |
|
|
} |
|
|
|
|
|
|
|
|
content.WriteString("=== RESPONSE ===\n") |
|
|
content.WriteString(fmt.Sprintf("Status: %d\n", status)) |
|
|
|
|
|
if responseHeaders != nil { |
|
|
for key, values := range responseHeaders { |
|
|
for _, value := range values { |
|
|
content.WriteString(fmt.Sprintf("%s: %s\n", key, value)) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
content.WriteString("\n") |
|
|
content.Write(response) |
|
|
content.WriteString("\n") |
|
|
|
|
|
return content.String() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) decompressResponse(responseHeaders map[string][]string, response []byte) ([]byte, error) { |
|
|
if responseHeaders == nil || len(response) == 0 { |
|
|
return response, nil |
|
|
} |
|
|
|
|
|
|
|
|
var contentEncoding string |
|
|
for key, values := range responseHeaders { |
|
|
if strings.ToLower(key) == "content-encoding" && len(values) > 0 { |
|
|
contentEncoding = strings.ToLower(values[0]) |
|
|
break |
|
|
} |
|
|
} |
|
|
|
|
|
switch contentEncoding { |
|
|
case "gzip": |
|
|
return l.decompressGzip(response) |
|
|
case "deflate": |
|
|
return l.decompressDeflate(response) |
|
|
case "br": |
|
|
return l.decompressBrotli(response) |
|
|
case "zstd": |
|
|
return l.decompressZstd(response) |
|
|
default: |
|
|
|
|
|
return response, nil |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) decompressGzip(data []byte) ([]byte, error) { |
|
|
reader, err := gzip.NewReader(bytes.NewReader(data)) |
|
|
if err != nil { |
|
|
return nil, fmt.Errorf("failed to create gzip reader: %w", err) |
|
|
} |
|
|
defer func() { |
|
|
if errClose := reader.Close(); errClose != nil { |
|
|
log.WithError(errClose).Warn("failed to close gzip reader in request logger") |
|
|
} |
|
|
}() |
|
|
|
|
|
decompressed, err := io.ReadAll(reader) |
|
|
if err != nil { |
|
|
return nil, fmt.Errorf("failed to decompress gzip data: %w", err) |
|
|
} |
|
|
|
|
|
return decompressed, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) decompressDeflate(data []byte) ([]byte, error) { |
|
|
reader := flate.NewReader(bytes.NewReader(data)) |
|
|
defer func() { |
|
|
if errClose := reader.Close(); errClose != nil { |
|
|
log.WithError(errClose).Warn("failed to close deflate reader in request logger") |
|
|
} |
|
|
}() |
|
|
|
|
|
decompressed, err := io.ReadAll(reader) |
|
|
if err != nil { |
|
|
return nil, fmt.Errorf("failed to decompress deflate data: %w", err) |
|
|
} |
|
|
|
|
|
return decompressed, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) decompressBrotli(data []byte) ([]byte, error) { |
|
|
reader := brotli.NewReader(bytes.NewReader(data)) |
|
|
|
|
|
decompressed, err := io.ReadAll(reader) |
|
|
if err != nil { |
|
|
return nil, fmt.Errorf("failed to decompress brotli data: %w", err) |
|
|
} |
|
|
|
|
|
return decompressed, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) decompressZstd(data []byte) ([]byte, error) { |
|
|
decoder, err := zstd.NewReader(bytes.NewReader(data)) |
|
|
if err != nil { |
|
|
return nil, fmt.Errorf("failed to create zstd reader: %w", err) |
|
|
} |
|
|
defer decoder.Close() |
|
|
|
|
|
decompressed, err := io.ReadAll(decoder) |
|
|
if err != nil { |
|
|
return nil, fmt.Errorf("failed to decompress zstd data: %w", err) |
|
|
} |
|
|
|
|
|
return decompressed, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (l *FileRequestLogger) formatRequestInfo(url, method string, headers map[string][]string, body []byte) string { |
|
|
var content strings.Builder |
|
|
|
|
|
content.WriteString("=== REQUEST INFO ===\n") |
|
|
content.WriteString(fmt.Sprintf("Version: %s\n", buildinfo.Version)) |
|
|
content.WriteString(fmt.Sprintf("URL: %s\n", url)) |
|
|
content.WriteString(fmt.Sprintf("Method: %s\n", method)) |
|
|
content.WriteString(fmt.Sprintf("Timestamp: %s\n", time.Now().Format(time.RFC3339Nano))) |
|
|
content.WriteString("\n") |
|
|
|
|
|
content.WriteString("=== HEADERS ===\n") |
|
|
for key, values := range headers { |
|
|
for _, value := range values { |
|
|
masked := util.MaskSensitiveHeaderValue(key, value) |
|
|
content.WriteString(fmt.Sprintf("%s: %s\n", key, masked)) |
|
|
} |
|
|
} |
|
|
content.WriteString("\n") |
|
|
|
|
|
content.WriteString("=== REQUEST BODY ===\n") |
|
|
content.Write(body) |
|
|
content.WriteString("\n\n") |
|
|
|
|
|
return content.String() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type FileStreamingLogWriter struct { |
|
|
|
|
|
logFilePath string |
|
|
|
|
|
|
|
|
url string |
|
|
|
|
|
|
|
|
method string |
|
|
|
|
|
|
|
|
timestamp time.Time |
|
|
|
|
|
|
|
|
requestHeaders map[string][]string |
|
|
|
|
|
|
|
|
requestBodyPath string |
|
|
|
|
|
|
|
|
responseBodyPath string |
|
|
|
|
|
|
|
|
responseBodyFile *os.File |
|
|
|
|
|
|
|
|
chunkChan chan []byte |
|
|
|
|
|
|
|
|
closeChan chan struct{} |
|
|
|
|
|
|
|
|
errorChan chan error |
|
|
|
|
|
|
|
|
responseStatus int |
|
|
|
|
|
|
|
|
statusWritten bool |
|
|
|
|
|
|
|
|
responseHeaders map[string][]string |
|
|
|
|
|
|
|
|
apiRequest []byte |
|
|
|
|
|
|
|
|
apiResponse []byte |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *FileStreamingLogWriter) WriteChunkAsync(chunk []byte) { |
|
|
if w.chunkChan == nil { |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
chunkCopy := make([]byte, len(chunk)) |
|
|
copy(chunkCopy, chunk) |
|
|
|
|
|
|
|
|
select { |
|
|
case w.chunkChan <- chunkCopy: |
|
|
default: |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *FileStreamingLogWriter) WriteStatus(status int, headers map[string][]string) error { |
|
|
if status == 0 { |
|
|
return nil |
|
|
} |
|
|
|
|
|
w.responseStatus = status |
|
|
if headers != nil { |
|
|
w.responseHeaders = make(map[string][]string, len(headers)) |
|
|
for key, values := range headers { |
|
|
headerValues := make([]string, len(values)) |
|
|
copy(headerValues, values) |
|
|
w.responseHeaders[key] = headerValues |
|
|
} |
|
|
} |
|
|
w.statusWritten = true |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *FileStreamingLogWriter) WriteAPIRequest(apiRequest []byte) error { |
|
|
if len(apiRequest) == 0 { |
|
|
return nil |
|
|
} |
|
|
w.apiRequest = bytes.Clone(apiRequest) |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *FileStreamingLogWriter) WriteAPIResponse(apiResponse []byte) error { |
|
|
if len(apiResponse) == 0 { |
|
|
return nil |
|
|
} |
|
|
w.apiResponse = bytes.Clone(apiResponse) |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *FileStreamingLogWriter) Close() error { |
|
|
if w.chunkChan != nil { |
|
|
close(w.chunkChan) |
|
|
} |
|
|
|
|
|
|
|
|
if w.closeChan != nil { |
|
|
<-w.closeChan |
|
|
w.chunkChan = nil |
|
|
} |
|
|
|
|
|
select { |
|
|
case errWrite := <-w.errorChan: |
|
|
w.cleanupTempFiles() |
|
|
return errWrite |
|
|
default: |
|
|
} |
|
|
|
|
|
if w.logFilePath == "" { |
|
|
w.cleanupTempFiles() |
|
|
return nil |
|
|
} |
|
|
|
|
|
logFile, errOpen := os.OpenFile(w.logFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) |
|
|
if errOpen != nil { |
|
|
w.cleanupTempFiles() |
|
|
return fmt.Errorf("failed to create log file: %w", errOpen) |
|
|
} |
|
|
|
|
|
writeErr := w.writeFinalLog(logFile) |
|
|
if errClose := logFile.Close(); errClose != nil { |
|
|
log.WithError(errClose).Warn("failed to close request log file") |
|
|
if writeErr == nil { |
|
|
writeErr = errClose |
|
|
} |
|
|
} |
|
|
|
|
|
w.cleanupTempFiles() |
|
|
return writeErr |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (w *FileStreamingLogWriter) asyncWriter() { |
|
|
defer close(w.closeChan) |
|
|
|
|
|
for chunk := range w.chunkChan { |
|
|
if w.responseBodyFile == nil { |
|
|
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 = "" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type NoOpStreamingLogWriter struct{} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *NoOpStreamingLogWriter) WriteChunkAsync(_ []byte) {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *NoOpStreamingLogWriter) WriteStatus(_ int, _ map[string][]string) error { |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *NoOpStreamingLogWriter) WriteAPIRequest(_ []byte) error { |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *NoOpStreamingLogWriter) WriteAPIResponse(_ []byte) error { |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *NoOpStreamingLogWriter) Close() error { return nil } |
|
|
|