Skip to content

Commit a0bb1f3

Browse files
committed
feat(logging): add file-backed sources for request logging
- Introduced `FileBodySource` to support large request log sections stored in temp files. - Added file-backed support for WebSocket timeline and API WebSocket timeline logging. - Updated `LogRequest` and middleware to integrate optional file-backed sources. - Implemented clean-up mechanisms to manage temporary log files after processing.
1 parent 412d344 commit a0bb1f3

8 files changed

Lines changed: 892 additions & 40 deletions

File tree

internal/api/middleware/request_logging.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc {
5858
wrapper.logOnErrorOnly = true
5959
}
6060
c.Writer = wrapper
61+
attachWebsocketLogSources(c, logger, loggerEnabled)
6162

6263
// Process the request
6364
c.Next()
@@ -70,6 +71,26 @@ func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc {
7071
}
7172
}
7273

74+
type fileBodySourceFactory interface {
75+
NewFileBodySource(prefix string) (*logging.FileBodySource, error)
76+
}
77+
78+
func attachWebsocketLogSources(c *gin.Context, logger logging.RequestLogger, loggerEnabled bool) {
79+
if c == nil || !loggerEnabled || !isResponsesWebsocketUpgrade(c.Request) {
80+
return
81+
}
82+
factory, ok := logger.(fileBodySourceFactory)
83+
if !ok || factory == nil {
84+
return
85+
}
86+
if source, errSource := factory.NewFileBodySource("websocket-timeline"); errSource == nil {
87+
c.Set(logging.WebsocketTimelineSourceContextKey, source)
88+
}
89+
if source, errSource := factory.NewFileBodySource("api-websocket-timeline"); errSource == nil {
90+
c.Set(logging.APIWebsocketTimelineSourceContextKey, source)
91+
}
92+
}
93+
7394
func shouldSkipMethodForRequestLogging(req *http.Request) bool {
7495
if req == nil {
7596
return true

internal/api/middleware/request_logging_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import (
66
"net/http"
77
"net/http/httptest"
88
"net/url"
9+
"os"
910
"strings"
1011
"testing"
1112

1213
"github.com/gin-gonic/gin"
1314
"github.com/klauspost/compress/zstd"
15+
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
1416
)
1517

1618
func TestShouldSkipMethodForRequestLogging(t *testing.T) {
@@ -142,6 +144,63 @@ func TestShouldCaptureRequestBody(t *testing.T) {
142144
}
143145
}
144146

147+
func TestAttachWebsocketLogSourcesUsesLoggerLogsDir(t *testing.T) {
148+
gin.SetMode(gin.TestMode)
149+
150+
logsDir := t.TempDir()
151+
logger := logging.NewFileRequestLogger(true, logsDir, "", 0)
152+
recorder := httptest.NewRecorder()
153+
c, _ := gin.CreateTestContext(recorder)
154+
c.Request = httptest.NewRequest(http.MethodGet, "/v1/responses", nil)
155+
c.Request.Header.Set("Upgrade", "websocket")
156+
157+
attachWebsocketLogSources(c, logger, true)
158+
defer cleanupFileBodySourcesFromContext(c)
159+
160+
for _, key := range []string{
161+
logging.WebsocketTimelineSourceContextKey,
162+
logging.APIWebsocketTimelineSourceContextKey,
163+
} {
164+
value, exists := c.Get(key)
165+
if !exists {
166+
t.Fatalf("expected %s source to be attached", key)
167+
}
168+
source, ok := value.(*logging.FileBodySource)
169+
if !ok || source == nil {
170+
t.Fatalf("%s source type = %T", key, value)
171+
}
172+
file, errPart := source.CreatePart("probe")
173+
if errPart != nil {
174+
t.Fatalf("CreatePart(%s): %v", key, errPart)
175+
}
176+
path := file.Name()
177+
if errClose := file.Close(); errClose != nil {
178+
t.Fatalf("close part: %v", errClose)
179+
}
180+
if !strings.HasPrefix(path, logsDir+string(os.PathSeparator)) {
181+
t.Fatalf("%s part path %s is not under logs dir %s", key, path, logsDir)
182+
}
183+
}
184+
}
185+
186+
func cleanupFileBodySourcesFromContext(c *gin.Context) {
187+
if c == nil {
188+
return
189+
}
190+
for _, key := range []string{
191+
logging.WebsocketTimelineSourceContextKey,
192+
logging.APIWebsocketTimelineSourceContextKey,
193+
} {
194+
value, exists := c.Get(key)
195+
if !exists {
196+
continue
197+
}
198+
if source, ok := value.(*logging.FileBodySource); ok && source != nil {
199+
_ = source.Cleanup()
200+
}
201+
}
202+
}
203+
145204
func TestCaptureRequestInfoDecodesZstdRequestBodyForLog(t *testing.T) {
146205
gin.SetMode(gin.TestMode)
147206

internal/api/middleware/response_writer.go

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,10 @@ func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
280280

281281
hasAPIError := len(slicesAPIResponseError) > 0 || finalStatusCode >= http.StatusBadRequest
282282
forceLog := w.logOnErrorOnly && hasAPIError && !w.logger.IsEnabled()
283+
websocketTimelineSource := w.extractWebsocketTimelineSource(c)
284+
apiWebsocketTimelineSource := w.extractAPIWebsocketTimelineSource(c)
283285
if !w.logger.IsEnabled() && !forceLog {
286+
cleanupFileBodySources(websocketTimelineSource, apiWebsocketTimelineSource)
284287
return nil
285288
}
286289

@@ -307,6 +310,13 @@ func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
307310
_ = w.streamWriter.WriteAPIResponse(apiResponse)
308311
}
309312
apiWebsocketTimeline := w.extractAPIWebsocketTimeline(c)
313+
var errMerge error
314+
apiWebsocketTimeline, errMerge = mergeFileBodySource(apiWebsocketTimeline, apiWebsocketTimelineSource)
315+
if errMerge != nil {
316+
cleanupFileBodySources(websocketTimelineSource)
317+
return errMerge
318+
}
319+
cleanupFileBodySources(websocketTimelineSource)
310320
if len(apiWebsocketTimeline) > 0 {
311321
_ = w.streamWriter.WriteAPIWebsocketTimeline(apiWebsocketTimeline)
312322
}
@@ -318,7 +328,7 @@ func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
318328
return nil
319329
}
320330

321-
return w.logRequest(w.extractRequestBody(c), finalStatusCode, w.cloneHeaders(), w.extractResponseBody(c), w.extractWebsocketTimeline(c), w.extractAPIRequest(c), w.extractAPIResponse(c), w.extractAPIWebsocketTimeline(c), w.extractAPIResponseTimestamp(c), slicesAPIResponseError, forceLog)
331+
return w.logRequest(w.extractRequestBody(c), finalStatusCode, w.cloneHeaders(), w.extractResponseBody(c), w.extractWebsocketTimeline(c), websocketTimelineSource, w.extractAPIRequest(c), w.extractAPIResponse(c), w.extractAPIWebsocketTimeline(c), apiWebsocketTimelineSource, w.extractAPIResponseTimestamp(c), slicesAPIResponseError, forceLog)
322332
}
323333

324334
func (w *ResponseWriterWrapper) cloneHeaders() map[string][]string {
@@ -370,6 +380,10 @@ func (w *ResponseWriterWrapper) extractAPIWebsocketTimeline(c *gin.Context) []by
370380
return bytes.Clone(data)
371381
}
372382

383+
func (w *ResponseWriterWrapper) extractAPIWebsocketTimelineSource(c *gin.Context) *logging.FileBodySource {
384+
return extractFileBodySource(c, logging.APIWebsocketTimelineSourceContextKey)
385+
}
386+
373387
func (w *ResponseWriterWrapper) extractAPIResponseTimestamp(c *gin.Context) time.Time {
374388
ts, isExist := c.Get("API_RESPONSE_TIMESTAMP")
375389
if !isExist {
@@ -405,6 +419,25 @@ func (w *ResponseWriterWrapper) extractWebsocketTimeline(c *gin.Context) []byte
405419
return extractBodyOverride(c, websocketTimelineOverrideContextKey)
406420
}
407421

422+
func (w *ResponseWriterWrapper) extractWebsocketTimelineSource(c *gin.Context) *logging.FileBodySource {
423+
return extractFileBodySource(c, logging.WebsocketTimelineSourceContextKey)
424+
}
425+
426+
func extractFileBodySource(c *gin.Context, key string) *logging.FileBodySource {
427+
if c == nil {
428+
return nil
429+
}
430+
value, exists := c.Get(key)
431+
if !exists {
432+
return nil
433+
}
434+
source, ok := value.(*logging.FileBodySource)
435+
if !ok || source == nil {
436+
return nil
437+
}
438+
return source
439+
}
440+
408441
func extractBodyOverride(c *gin.Context, key string) []byte {
409442
if c == nil {
410443
return nil
@@ -426,11 +459,48 @@ func extractBodyOverride(c *gin.Context, key string) []byte {
426459
return nil
427460
}
428461

429-
func (w *ResponseWriterWrapper) logRequest(requestBody []byte, statusCode int, headers map[string][]string, body, websocketTimeline, apiRequestBody, apiResponseBody, apiWebsocketTimeline []byte, apiResponseTimestamp time.Time, apiResponseErrors []*interfaces.ErrorMessage, forceLog bool) error {
462+
func (w *ResponseWriterWrapper) logRequest(requestBody []byte, statusCode int, headers map[string][]string, body, websocketTimeline []byte, websocketTimelineSource *logging.FileBodySource, apiRequestBody, apiResponseBody, apiWebsocketTimeline []byte, apiWebsocketTimelineSource *logging.FileBodySource, apiResponseTimestamp time.Time, apiResponseErrors []*interfaces.ErrorMessage, forceLog bool) error {
430463
if w.requestInfo == nil {
464+
cleanupFileBodySources(websocketTimelineSource, apiWebsocketTimelineSource)
431465
return nil
432466
}
433467

468+
if loggerWithSources, ok := w.logger.(interface {
469+
LogRequestWithOptionsAndSources(string, string, map[string][]string, []byte, int, map[string][]string, []byte, []byte, *logging.FileBodySource, []byte, []byte, []byte, *logging.FileBodySource, []*interfaces.ErrorMessage, bool, string, time.Time, time.Time) error
470+
}); ok {
471+
return loggerWithSources.LogRequestWithOptionsAndSources(
472+
w.requestInfo.URL,
473+
w.requestInfo.Method,
474+
w.requestInfo.Headers,
475+
requestBody,
476+
statusCode,
477+
headers,
478+
body,
479+
websocketTimeline,
480+
websocketTimelineSource,
481+
apiRequestBody,
482+
apiResponseBody,
483+
apiWebsocketTimeline,
484+
apiWebsocketTimelineSource,
485+
apiResponseErrors,
486+
forceLog,
487+
w.requestInfo.RequestID,
488+
w.requestInfo.Timestamp,
489+
apiResponseTimestamp,
490+
)
491+
}
492+
493+
var errMerge error
494+
websocketTimeline, errMerge = mergeFileBodySource(websocketTimeline, websocketTimelineSource)
495+
if errMerge != nil {
496+
cleanupFileBodySources(apiWebsocketTimelineSource)
497+
return errMerge
498+
}
499+
apiWebsocketTimeline, errMerge = mergeFileBodySource(apiWebsocketTimeline, apiWebsocketTimelineSource)
500+
if errMerge != nil {
501+
return errMerge
502+
}
503+
434504
if loggerWithOptions, ok := w.logger.(interface {
435505
LogRequestWithOptions(string, string, map[string][]string, []byte, int, map[string][]string, []byte, []byte, []byte, []byte, []byte, []*interfaces.ErrorMessage, bool, string, time.Time, time.Time) error
436506
}); ok {
@@ -472,3 +542,34 @@ func (w *ResponseWriterWrapper) logRequest(requestBody []byte, statusCode int, h
472542
apiResponseTimestamp,
473543
)
474544
}
545+
546+
func mergeFileBodySource(payload []byte, source *logging.FileBodySource) ([]byte, error) {
547+
if source == nil {
548+
return payload, nil
549+
}
550+
defer cleanupFileBodySources(source)
551+
if !source.HasPayload() {
552+
return payload, nil
553+
}
554+
var buf bytes.Buffer
555+
if len(payload) > 0 {
556+
buf.Write(payload)
557+
if !bytes.HasSuffix(payload, []byte("\n")) {
558+
buf.WriteByte('\n')
559+
}
560+
buf.WriteByte('\n')
561+
}
562+
if errWrite := source.WriteTo(&buf); errWrite != nil {
563+
return nil, errWrite
564+
}
565+
return buf.Bytes(), nil
566+
}
567+
568+
func cleanupFileBodySources(sources ...*logging.FileBodySource) {
569+
for _, source := range sources {
570+
if source == nil {
571+
continue
572+
}
573+
_ = source.Cleanup()
574+
}
575+
}

0 commit comments

Comments
 (0)