380 lines
11 KiB
Go
380 lines
11 KiB
Go
package logging
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// FileStreamingLogWriter implements StreamingLogWriter for file-based streaming logs.
|
|
// It spools streaming response chunks to a temporary file to avoid retaining large responses in memory.
|
|
// The final log file is assembled when Close is called.
|
|
type FileStreamingLogWriter struct {
|
|
// logFilePath is the final log file path.
|
|
logFilePath string
|
|
|
|
// 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
|
|
|
|
// closeChan is a channel for signaling when the writer is closed.
|
|
closeChan chan struct{}
|
|
|
|
// errorChan is a channel for reporting errors during writing.
|
|
errorChan chan error
|
|
|
|
// responseStatus stores the HTTP status code.
|
|
responseStatus int
|
|
|
|
// statusWritten indicates whether a non-zero status was recorded.
|
|
statusWritten bool
|
|
|
|
// responseHeaders stores the response headers.
|
|
responseHeaders map[string][]string
|
|
|
|
// apiRequest stores the upstream API request data.
|
|
apiRequest []byte
|
|
|
|
// apiRequestSource stores file-backed upstream API request data.
|
|
apiRequestSource *FileBodySource
|
|
|
|
// apiResponse stores the upstream API response data.
|
|
apiResponse []byte
|
|
|
|
// apiResponseSource stores file-backed upstream API response data.
|
|
apiResponseSource *FileBodySource
|
|
|
|
// apiWebsocketTimeline stores the upstream websocket event timeline.
|
|
apiWebsocketTimeline []byte
|
|
|
|
// apiResponseTimestamp captures when the API response was received.
|
|
apiResponseTimestamp time.Time
|
|
}
|
|
|
|
// WriteChunkAsync writes a response chunk asynchronously (non-blocking).
|
|
//
|
|
// Parameters:
|
|
// - chunk: The response chunk to write
|
|
func (w *FileStreamingLogWriter) WriteChunkAsync(chunk []byte) {
|
|
if w.chunkChan == nil {
|
|
return
|
|
}
|
|
|
|
// Make a copy of the chunk to avoid data races
|
|
chunkCopy := make([]byte, len(chunk))
|
|
copy(chunkCopy, chunk)
|
|
|
|
// Non-blocking send
|
|
select {
|
|
case w.chunkChan <- chunkCopy:
|
|
default:
|
|
// Channel is full, skip this chunk to avoid blocking
|
|
}
|
|
}
|
|
|
|
// WriteStatus buffers the response status and headers for later writing.
|
|
//
|
|
// Parameters:
|
|
// - status: The response status code
|
|
// - headers: The response headers
|
|
//
|
|
// Returns:
|
|
// - error: Always returns nil (buffering cannot fail)
|
|
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
|
|
}
|
|
|
|
// WriteAPIRequest buffers the upstream API request details for later writing.
|
|
//
|
|
// Parameters:
|
|
// - apiRequest: The API request data (typically includes URL, headers, body sent upstream)
|
|
//
|
|
// Returns:
|
|
// - error: Always returns nil (buffering cannot fail)
|
|
func (w *FileStreamingLogWriter) WriteAPIRequest(apiRequest []byte) error {
|
|
if len(apiRequest) == 0 {
|
|
return nil
|
|
}
|
|
w.apiRequest = bytes.Clone(apiRequest)
|
|
return nil
|
|
}
|
|
|
|
// WriteAPIRequestSource buffers a file-backed upstream API request for final writing.
|
|
func (w *FileStreamingLogWriter) WriteAPIRequestSource(apiRequestSource *FileBodySource) error {
|
|
if apiRequestSource == nil || !apiRequestSource.HasPayload() {
|
|
return nil
|
|
}
|
|
w.apiRequestSource = apiRequestSource
|
|
return nil
|
|
}
|
|
|
|
// WriteAPIResponse buffers the upstream API response details for later writing.
|
|
//
|
|
// Parameters:
|
|
// - apiResponse: The API response data
|
|
//
|
|
// Returns:
|
|
// - error: Always returns nil (buffering cannot fail)
|
|
func (w *FileStreamingLogWriter) WriteAPIResponse(apiResponse []byte) error {
|
|
if len(apiResponse) == 0 {
|
|
return nil
|
|
}
|
|
w.apiResponse = bytes.Clone(apiResponse)
|
|
return nil
|
|
}
|
|
|
|
// WriteAPIResponseSource buffers a file-backed upstream API response for final writing.
|
|
func (w *FileStreamingLogWriter) WriteAPIResponseSource(apiResponseSource *FileBodySource) error {
|
|
if apiResponseSource == nil || !apiResponseSource.HasPayload() {
|
|
return nil
|
|
}
|
|
w.apiResponseSource = apiResponseSource
|
|
return nil
|
|
}
|
|
|
|
// WriteAPIWebsocketTimeline buffers the upstream websocket timeline for later writing.
|
|
//
|
|
// Parameters:
|
|
// - apiWebsocketTimeline: The upstream websocket event timeline
|
|
//
|
|
// Returns:
|
|
// - error: Always returns nil (buffering cannot fail)
|
|
func (w *FileStreamingLogWriter) WriteAPIWebsocketTimeline(apiWebsocketTimeline []byte) error {
|
|
if len(apiWebsocketTimeline) == 0 {
|
|
return nil
|
|
}
|
|
w.apiWebsocketTimeline = bytes.Clone(apiWebsocketTimeline)
|
|
return nil
|
|
}
|
|
|
|
func (w *FileStreamingLogWriter) SetFirstChunkTimestamp(timestamp time.Time) {
|
|
if !timestamp.IsZero() {
|
|
w.apiResponseTimestamp = timestamp
|
|
}
|
|
}
|
|
|
|
// Close finalizes the log file and cleans up resources.
|
|
// It writes all buffered data to the file in the correct order:
|
|
// API WEBSOCKET TIMELINE -> API REQUEST -> API RESPONSE -> RESPONSE (status, headers, body chunks)
|
|
//
|
|
// Returns:
|
|
// - error: An error if closing fails, nil otherwise
|
|
func (w *FileStreamingLogWriter) Close() error {
|
|
if w.chunkChan != nil {
|
|
close(w.chunkChan)
|
|
}
|
|
|
|
// Wait for async writer to finish spooling chunks
|
|
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
|
|
}
|
|
|
|
// asyncWriter runs in a goroutine to buffer chunks from the channel.
|
|
// It continuously reads chunks from the channel and appends them to a temp file for later assembly.
|
|
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, "http", inferUpstreamTransport(w.apiRequest, w.apiRequestSource, w.apiResponse, w.apiResponseSource, w.apiWebsocketTimeline, nil, nil), true); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeAPISection(logFile, "=== API WEBSOCKET TIMELINE ===\n", "=== API WEBSOCKET TIMELINE", w.apiWebsocketTimeline, time.Time{}); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writePreformattedAPISectionWithSource(logFile, "=== API REQUEST ===\n", "=== API REQUEST", w.apiRequest, w.apiRequestSource, time.Time{}); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writePreformattedAPISectionWithSource(logFile, "=== API RESPONSE ===\n", "=== API RESPONSE", w.apiResponse, w.apiResponseSource, w.apiResponseTimestamp); 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 = ""
|
|
}
|
|
}
|
|
|
|
// NoOpStreamingLogWriter is a no-operation implementation for when logging is disabled.
|
|
// It implements the StreamingLogWriter interface but performs no actual logging operations.
|
|
type NoOpStreamingLogWriter struct{}
|
|
|
|
// WriteChunkAsync is a no-op implementation that does nothing.
|
|
//
|
|
// Parameters:
|
|
// - chunk: The response chunk (ignored)
|
|
func (w *NoOpStreamingLogWriter) WriteChunkAsync(_ []byte) {}
|
|
|
|
// WriteStatus is a no-op implementation that does nothing and always returns nil.
|
|
//
|
|
// Parameters:
|
|
// - status: The response status code (ignored)
|
|
// - headers: The response headers (ignored)
|
|
//
|
|
// Returns:
|
|
// - error: Always returns nil
|
|
func (w *NoOpStreamingLogWriter) WriteStatus(_ int, _ map[string][]string) error {
|
|
return nil
|
|
}
|
|
|
|
// WriteAPIRequest is a no-op implementation that does nothing and always returns nil.
|
|
//
|
|
// Parameters:
|
|
// - apiRequest: The API request data (ignored)
|
|
//
|
|
// Returns:
|
|
// - error: Always returns nil
|
|
func (w *NoOpStreamingLogWriter) WriteAPIRequest(_ []byte) error {
|
|
return nil
|
|
}
|
|
|
|
// WriteAPIResponse is a no-op implementation that does nothing and always returns nil.
|
|
//
|
|
// Parameters:
|
|
// - apiResponse: The API response data (ignored)
|
|
//
|
|
// Returns:
|
|
// - error: Always returns nil
|
|
func (w *NoOpStreamingLogWriter) WriteAPIResponse(_ []byte) error {
|
|
return nil
|
|
}
|
|
|
|
// WriteAPIWebsocketTimeline is a no-op implementation that does nothing and always returns nil.
|
|
//
|
|
// Parameters:
|
|
// - apiWebsocketTimeline: The upstream websocket event timeline (ignored)
|
|
//
|
|
// Returns:
|
|
// - error: Always returns nil
|
|
func (w *NoOpStreamingLogWriter) WriteAPIWebsocketTimeline(_ []byte) error {
|
|
return nil
|
|
}
|
|
|
|
func (w *NoOpStreamingLogWriter) SetFirstChunkTimestamp(_ time.Time) {}
|
|
|
|
// Close is a no-op implementation that does nothing and always returns nil.
|
|
//
|
|
// Returns:
|
|
// - error: Always returns nil
|
|
func (w *NoOpStreamingLogWriter) Close() error { return nil }
|