mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +08:00
**feat(logging, middleware): add error-based logging support and error log management**
- Introduced `logOnErrorOnly` mode to enable logging only for error responses when request logging is disabled. - Added endpoints to list and download error logs (`/request-error-logs`). - Implemented error log file cleanup to retain only the newest 10 logs. - Refactored `ResponseWriterWrapper` to support forced logging for error responses. - Enhanced middleware to capture data for upstream error persistence. - Improved log file naming and error log filename generation.
This commit is contained in:
@@ -139,6 +139,126 @@ func (h *Handler) DeleteLogs(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRequestErrorLogs lists error request log files when RequestLog is disabled.
|
||||||
|
// It returns an empty list when RequestLog is enabled.
|
||||||
|
func (h *Handler) GetRequestErrorLogs(c *gin.Context) {
|
||||||
|
if h == nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "handler unavailable"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.cfg == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "configuration unavailable"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.cfg.RequestLog {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"files": []any{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := h.logDirectory()
|
||||||
|
if strings.TrimSpace(dir) == "" {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "log directory not configured"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"files": []any{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to list request error logs: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorLog struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Modified int64 `json:"modified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
files := make([]errorLog, 0, len(entries))
|
||||||
|
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 {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to read log info for %s: %v", name, errInfo)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
files = append(files, errorLog{
|
||||||
|
Name: name,
|
||||||
|
Size: info.Size(),
|
||||||
|
Modified: info.ModTime().Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(files, func(i, j int) bool { return files[i].Modified > files[j].Modified })
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"files": files})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadRequestErrorLog downloads a specific error request log file by name.
|
||||||
|
func (h *Handler) DownloadRequestErrorLog(c *gin.Context) {
|
||||||
|
if h == nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "handler unavailable"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.cfg == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "configuration unavailable"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := h.logDirectory()
|
||||||
|
if strings.TrimSpace(dir) == "" {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "log directory not configured"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(c.Param("name"))
|
||||||
|
if name == "" || strings.Contains(name, "/") || strings.Contains(name, "\\") {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid log file name"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(name, "error-") || !strings.HasSuffix(name, ".log") {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "log file not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dirAbs, errAbs := filepath.Abs(dir)
|
||||||
|
if errAbs != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to resolve log directory: %v", errAbs)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fullPath := filepath.Clean(filepath.Join(dirAbs, name))
|
||||||
|
prefix := dirAbs + string(os.PathSeparator)
|
||||||
|
if !strings.HasPrefix(fullPath, prefix) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid log file path"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, errStat := os.Stat(fullPath)
|
||||||
|
if errStat != nil {
|
||||||
|
if os.IsNotExist(errStat) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "log file not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to read log file: %v", errStat)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid log file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.FileAttachment(fullPath, name)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) logDirectory() string {
|
func (h *Handler) logDirectory() string {
|
||||||
if h == nil {
|
if h == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -215,7 +335,9 @@ func (acc *logAccumulator) consumeFile(path string) error {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
buf := make([]byte, 0, logScannerInitialBuffer)
|
buf := make([]byte, 0, logScannerInitialBuffer)
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
|
|
||||||
// RequestLoggingMiddleware creates a Gin middleware that logs HTTP requests and responses.
|
// RequestLoggingMiddleware creates a Gin middleware that logs HTTP requests and responses.
|
||||||
// It captures detailed information about the request and response, including headers and body,
|
// It captures detailed information about the request and response, including headers and body,
|
||||||
// and uses the provided RequestLogger to record this data. If logging is disabled in the
|
// and uses the provided RequestLogger to record this data. When logging is disabled in the
|
||||||
// logger, the middleware has minimal overhead.
|
// logger, it still captures data so that upstream errors can be persisted.
|
||||||
func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc {
|
func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
@@ -30,12 +30,6 @@ func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Early return if logging is disabled (zero overhead)
|
|
||||||
if !logger.IsEnabled() {
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture request information
|
// Capture request information
|
||||||
requestInfo, err := captureRequestInfo(c)
|
requestInfo, err := captureRequestInfo(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,6 +41,9 @@ func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc {
|
|||||||
|
|
||||||
// Create response writer wrapper
|
// Create response writer wrapper
|
||||||
wrapper := NewResponseWriterWrapper(c.Writer, logger, requestInfo)
|
wrapper := NewResponseWriterWrapper(c.Writer, logger, requestInfo)
|
||||||
|
if !logger.IsEnabled() {
|
||||||
|
wrapper.logOnErrorOnly = true
|
||||||
|
}
|
||||||
c.Writer = wrapper
|
c.Writer = wrapper
|
||||||
|
|
||||||
// Process the request
|
// Process the request
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -24,15 +25,16 @@ type RequestInfo struct {
|
|||||||
// It is designed to handle both standard and streaming responses, ensuring that logging operations do not block the client response.
|
// It is designed to handle both standard and streaming responses, ensuring that logging operations do not block the client response.
|
||||||
type ResponseWriterWrapper struct {
|
type ResponseWriterWrapper struct {
|
||||||
gin.ResponseWriter
|
gin.ResponseWriter
|
||||||
body *bytes.Buffer // body is a buffer to store the response body for non-streaming responses.
|
body *bytes.Buffer // body is a buffer to store the response body for non-streaming responses.
|
||||||
isStreaming bool // isStreaming indicates whether the response is a streaming type (e.g., text/event-stream).
|
isStreaming bool // isStreaming indicates whether the response is a streaming type (e.g., text/event-stream).
|
||||||
streamWriter logging.StreamingLogWriter // streamWriter is a writer for handling streaming log entries.
|
streamWriter logging.StreamingLogWriter // streamWriter is a writer for handling streaming log entries.
|
||||||
chunkChannel chan []byte // chunkChannel is a channel for asynchronously passing response chunks to the logger.
|
chunkChannel chan []byte // chunkChannel is a channel for asynchronously passing response chunks to the logger.
|
||||||
streamDone chan struct{} // streamDone signals when the streaming goroutine completes.
|
streamDone chan struct{} // streamDone signals when the streaming goroutine completes.
|
||||||
logger logging.RequestLogger // logger is the instance of the request logger service.
|
logger logging.RequestLogger // logger is the instance of the request logger service.
|
||||||
requestInfo *RequestInfo // requestInfo holds the details of the original request.
|
requestInfo *RequestInfo // requestInfo holds the details of the original request.
|
||||||
statusCode int // statusCode stores the HTTP status code of the response.
|
statusCode int // statusCode stores the HTTP status code of the response.
|
||||||
headers map[string][]string // headers stores the response headers.
|
headers map[string][]string // headers stores the response headers.
|
||||||
|
logOnErrorOnly bool // logOnErrorOnly enables logging only when an error response is detected.
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewResponseWriterWrapper creates and initializes a new ResponseWriterWrapper.
|
// NewResponseWriterWrapper creates and initializes a new ResponseWriterWrapper.
|
||||||
@@ -192,12 +194,34 @@ func (w *ResponseWriterWrapper) processStreamingChunks(done chan struct{}) {
|
|||||||
// For non-streaming responses, it logs the complete request and response details,
|
// For non-streaming responses, it logs the complete request and response details,
|
||||||
// including any API-specific request/response data stored in the Gin context.
|
// including any API-specific request/response data stored in the Gin context.
|
||||||
func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
|
func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
|
||||||
if !w.logger.IsEnabled() {
|
if w.logger == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
finalStatusCode := w.statusCode
|
||||||
|
if finalStatusCode == 0 {
|
||||||
|
if statusWriter, ok := w.ResponseWriter.(interface{ Status() int }); ok {
|
||||||
|
finalStatusCode = statusWriter.Status()
|
||||||
|
} else {
|
||||||
|
finalStatusCode = 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var slicesAPIResponseError []*interfaces.ErrorMessage
|
||||||
|
apiResponseError, isExist := c.Get("API_RESPONSE_ERROR")
|
||||||
|
if isExist {
|
||||||
|
if apiErrors, ok := apiResponseError.([]*interfaces.ErrorMessage); ok {
|
||||||
|
slicesAPIResponseError = apiErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAPIError := len(slicesAPIResponseError) > 0 || finalStatusCode >= http.StatusBadRequest
|
||||||
|
forceLog := w.logOnErrorOnly && hasAPIError && !w.logger.IsEnabled()
|
||||||
|
if !w.logger.IsEnabled() && !forceLog {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if w.isStreaming {
|
if w.isStreaming {
|
||||||
// Close streaming channel and writer
|
|
||||||
if w.chunkChannel != nil {
|
if w.chunkChannel != nil {
|
||||||
close(w.chunkChannel)
|
close(w.chunkChannel)
|
||||||
w.chunkChannel = nil
|
w.chunkChannel = nil
|
||||||
@@ -209,80 +233,98 @@ func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if w.streamWriter != nil {
|
if w.streamWriter != nil {
|
||||||
err := w.streamWriter.Close()
|
if err := w.streamWriter.Close(); err != nil {
|
||||||
|
w.streamWriter = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
w.streamWriter = nil
|
w.streamWriter = nil
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
} else {
|
if forceLog {
|
||||||
// Capture final status code and headers if not already captured
|
return w.logRequest(finalStatusCode, w.cloneHeaders(), w.body.Bytes(), w.extractAPIRequest(c), w.extractAPIResponse(c), slicesAPIResponseError, forceLog)
|
||||||
finalStatusCode := w.statusCode
|
|
||||||
if finalStatusCode == 0 {
|
|
||||||
// Get status from underlying ResponseWriter if available
|
|
||||||
if statusWriter, ok := w.ResponseWriter.(interface{ Status() int }); ok {
|
|
||||||
finalStatusCode = statusWriter.Status()
|
|
||||||
} else {
|
|
||||||
finalStatusCode = 200 // Default
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure we have the latest headers before finalizing
|
return w.logRequest(finalStatusCode, w.cloneHeaders(), w.body.Bytes(), w.extractAPIRequest(c), w.extractAPIResponse(c), slicesAPIResponseError, forceLog)
|
||||||
w.ensureHeadersCaptured()
|
}
|
||||||
|
|
||||||
// Use the captured headers as the final headers
|
func (w *ResponseWriterWrapper) cloneHeaders() map[string][]string {
|
||||||
finalHeaders := make(map[string][]string)
|
w.ensureHeadersCaptured()
|
||||||
for key, values := range w.headers {
|
|
||||||
// Make a copy of the values slice to avoid reference issues
|
|
||||||
headerValues := make([]string, len(values))
|
|
||||||
copy(headerValues, values)
|
|
||||||
finalHeaders[key] = headerValues
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiRequestBody []byte
|
finalHeaders := make(map[string][]string, len(w.headers))
|
||||||
apiRequest, isExist := c.Get("API_REQUEST")
|
for key, values := range w.headers {
|
||||||
if isExist {
|
headerValues := make([]string, len(values))
|
||||||
var ok bool
|
copy(headerValues, values)
|
||||||
apiRequestBody, ok = apiRequest.([]byte)
|
finalHeaders[key] = headerValues
|
||||||
if !ok {
|
}
|
||||||
apiRequestBody = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiResponseBody []byte
|
return finalHeaders
|
||||||
apiResponse, isExist := c.Get("API_RESPONSE")
|
}
|
||||||
if isExist {
|
|
||||||
var ok bool
|
|
||||||
apiResponseBody, ok = apiResponse.([]byte)
|
|
||||||
if !ok {
|
|
||||||
apiResponseBody = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var slicesAPIResponseError []*interfaces.ErrorMessage
|
func (w *ResponseWriterWrapper) extractAPIRequest(c *gin.Context) []byte {
|
||||||
apiResponseError, isExist := c.Get("API_RESPONSE_ERROR")
|
apiRequest, isExist := c.Get("API_REQUEST")
|
||||||
if isExist {
|
if !isExist {
|
||||||
var ok bool
|
return nil
|
||||||
slicesAPIResponseError, ok = apiResponseError.([]*interfaces.ErrorMessage)
|
}
|
||||||
if !ok {
|
data, ok := apiRequest.([]byte)
|
||||||
slicesAPIResponseError = nil
|
if !ok || len(data) == 0 {
|
||||||
}
|
return nil
|
||||||
}
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
// Log complete non-streaming response
|
func (w *ResponseWriterWrapper) extractAPIResponse(c *gin.Context) []byte {
|
||||||
return w.logger.LogRequest(
|
apiResponse, isExist := c.Get("API_RESPONSE")
|
||||||
|
if !isExist {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, ok := apiResponse.([]byte)
|
||||||
|
if !ok || len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWriterWrapper) logRequest(statusCode int, headers map[string][]string, body []byte, apiRequestBody, apiResponseBody []byte, apiResponseErrors []*interfaces.ErrorMessage, forceLog bool) error {
|
||||||
|
if w.requestInfo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestBody []byte
|
||||||
|
if len(w.requestInfo.Body) > 0 {
|
||||||
|
requestBody = w.requestInfo.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
if loggerWithOptions, ok := w.logger.(interface {
|
||||||
|
LogRequestWithOptions(string, string, map[string][]string, []byte, int, map[string][]string, []byte, []byte, []byte, []*interfaces.ErrorMessage, bool) error
|
||||||
|
}); ok {
|
||||||
|
return loggerWithOptions.LogRequestWithOptions(
|
||||||
w.requestInfo.URL,
|
w.requestInfo.URL,
|
||||||
w.requestInfo.Method,
|
w.requestInfo.Method,
|
||||||
w.requestInfo.Headers,
|
w.requestInfo.Headers,
|
||||||
w.requestInfo.Body,
|
requestBody,
|
||||||
finalStatusCode,
|
statusCode,
|
||||||
finalHeaders,
|
headers,
|
||||||
w.body.Bytes(),
|
body,
|
||||||
apiRequestBody,
|
apiRequestBody,
|
||||||
apiResponseBody,
|
apiResponseBody,
|
||||||
slicesAPIResponseError,
|
apiResponseErrors,
|
||||||
|
forceLog,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return w.logger.LogRequest(
|
||||||
|
w.requestInfo.URL,
|
||||||
|
w.requestInfo.Method,
|
||||||
|
w.requestInfo.Headers,
|
||||||
|
requestBody,
|
||||||
|
statusCode,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
apiRequestBody,
|
||||||
|
apiResponseBody,
|
||||||
|
apiResponseErrors,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status returns the HTTP response status code captured by the wrapper.
|
// Status returns the HTTP response status code captured by the wrapper.
|
||||||
|
|||||||
@@ -509,6 +509,8 @@ func (s *Server) registerManagementRoutes() {
|
|||||||
|
|
||||||
mgmt.GET("/logs", s.mgmt.GetLogs)
|
mgmt.GET("/logs", s.mgmt.GetLogs)
|
||||||
mgmt.DELETE("/logs", s.mgmt.DeleteLogs)
|
mgmt.DELETE("/logs", s.mgmt.DeleteLogs)
|
||||||
|
mgmt.GET("/request-error-logs", s.mgmt.GetRequestErrorLogs)
|
||||||
|
mgmt.GET("/request-error-logs/:name", s.mgmt.DownloadRequestErrorLog)
|
||||||
mgmt.GET("/request-log", s.mgmt.GetRequestLog)
|
mgmt.GET("/request-log", s.mgmt.GetRequestLog)
|
||||||
mgmt.PUT("/request-log", s.mgmt.PutRequestLog)
|
mgmt.PUT("/request-log", s.mgmt.PutRequestLog)
|
||||||
mgmt.PATCH("/request-log", s.mgmt.PutRequestLog)
|
mgmt.PATCH("/request-log", s.mgmt.PutRequestLog)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -156,17 +157,30 @@ func (l *FileRequestLogger) SetEnabled(enabled bool) {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - error: An error if logging fails, nil otherwise
|
// - error: An error if logging fails, nil otherwise
|
||||||
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) error {
|
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) error {
|
||||||
if !l.enabled {
|
return l.logRequest(url, method, requestHeaders, body, statusCode, responseHeaders, response, apiRequest, apiResponse, apiResponseErrors, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogRequestWithOptions logs a request with optional forced logging behavior.
|
||||||
|
// The force flag allows writing error logs even when regular request logging is disabled.
|
||||||
|
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) error {
|
||||||
|
return l.logRequest(url, method, requestHeaders, body, statusCode, responseHeaders, response, apiRequest, apiResponse, apiResponseErrors, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) error {
|
||||||
|
if !l.enabled && !force {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure logs directory exists
|
// Ensure logs directory exists
|
||||||
if err := l.ensureLogsDir(); err != nil {
|
if errEnsure := l.ensureLogsDir(); errEnsure != nil {
|
||||||
return fmt.Errorf("failed to create logs directory: %w", err)
|
return fmt.Errorf("failed to create logs directory: %w", errEnsure)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate filename
|
// Generate filename
|
||||||
filename := l.generateFilename(url)
|
filename := l.generateFilename(url)
|
||||||
|
if force && !l.enabled {
|
||||||
|
filename = l.generateErrorFilename(url)
|
||||||
|
}
|
||||||
filePath := filepath.Join(l.logsDir, filename)
|
filePath := filepath.Join(l.logsDir, filename)
|
||||||
|
|
||||||
// Decompress response if needed
|
// Decompress response if needed
|
||||||
@@ -184,6 +198,12 @@ func (l *FileRequestLogger) LogRequest(url, method string, requestHeaders map[st
|
|||||||
return fmt.Errorf("failed to write log file: %w", err)
|
return fmt.Errorf("failed to write log file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if force && !l.enabled {
|
||||||
|
if errCleanup := l.cleanupOldErrorLogs(); errCleanup != nil {
|
||||||
|
log.WithError(errCleanup).Warn("failed to clean up old error logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +259,11 @@ func (l *FileRequestLogger) LogStreamingRequest(url, method string, headers map[
|
|||||||
return writer, nil
|
return writer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateErrorFilename creates a filename with an error prefix to differentiate forced error logs.
|
||||||
|
func (l *FileRequestLogger) generateErrorFilename(url string) string {
|
||||||
|
return fmt.Sprintf("error-%s", l.generateFilename(url))
|
||||||
|
}
|
||||||
|
|
||||||
// ensureLogsDir creates the logs directory if it doesn't exist.
|
// ensureLogsDir creates the logs directory if it doesn't exist.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
@@ -312,6 +337,52 @@ func (l *FileRequestLogger) sanitizeForFilename(path string) string {
|
|||||||
return sanitized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanupOldErrorLogs keeps only the newest 10 forced error log files.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// formatLogContent creates the complete log content for non-streaming requests.
|
// formatLogContent creates the complete log content for non-streaming requests.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
|||||||
Reference in New Issue
Block a user