720 lines
22 KiB
Go
720 lines
22 KiB
Go
package logging
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"compress/flate"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/andybalholm/brotli"
|
|
"github.com/klauspost/compress/zstd"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
func (l *FileRequestLogger) writeNonStreamingLog(
|
|
w io.Writer,
|
|
url, method string,
|
|
requestHeaders map[string][]string,
|
|
requestBody []byte,
|
|
requestBodyPath string,
|
|
websocketTimeline []byte,
|
|
websocketTimelineSource *FileBodySource,
|
|
apiRequest []byte,
|
|
apiRequestSource *FileBodySource,
|
|
apiResponse []byte,
|
|
apiResponseSource *FileBodySource,
|
|
apiWebsocketTimeline []byte,
|
|
apiWebsocketTimelineSource *FileBodySource,
|
|
apiResponseErrors []*interfaces.ErrorMessage,
|
|
statusCode int,
|
|
responseHeaders map[string][]string,
|
|
response []byte,
|
|
decompressErr error,
|
|
requestTimestamp time.Time,
|
|
apiResponseTimestamp time.Time,
|
|
) error {
|
|
if requestTimestamp.IsZero() {
|
|
requestTimestamp = time.Now()
|
|
}
|
|
isWebsocketTranscript := hasSectionPayload(websocketTimeline) || hasFileBodySourcePayload(websocketTimelineSource)
|
|
downstreamTransport := inferDownstreamTransport(requestHeaders, websocketTimeline, websocketTimelineSource)
|
|
upstreamTransport := inferUpstreamTransport(apiRequest, apiRequestSource, apiResponse, apiResponseSource, apiWebsocketTimeline, apiWebsocketTimelineSource, apiResponseErrors)
|
|
if errWrite := writeRequestInfoWithBody(w, url, method, requestHeaders, requestBody, requestBodyPath, requestTimestamp, downstreamTransport, upstreamTransport, !isWebsocketTranscript); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeAPISectionWithSource(w, "=== WEBSOCKET TIMELINE ===\n", "=== WEBSOCKET TIMELINE", websocketTimeline, websocketTimelineSource, time.Time{}); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeAPISectionWithSource(w, "=== API WEBSOCKET TIMELINE ===\n", "=== API WEBSOCKET TIMELINE", apiWebsocketTimeline, apiWebsocketTimelineSource, time.Time{}); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writePreformattedAPISectionWithSource(w, "=== API REQUEST ===\n", "=== API REQUEST", apiRequest, apiRequestSource, time.Time{}); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeAPIErrorResponses(w, apiResponseErrors); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writePreformattedAPISectionWithSource(w, "=== API RESPONSE ===\n", "=== API RESPONSE", apiResponse, apiResponseSource, apiResponseTimestamp); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if isWebsocketTranscript {
|
|
// Intentionally omit the generic downstream HTTP response section for websocket
|
|
// transcripts. The durable session exchange is captured in WEBSOCKET TIMELINE,
|
|
// and appending a one-off upgrade response snapshot would dilute that transcript.
|
|
return nil
|
|
}
|
|
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,
|
|
downstreamTransport string,
|
|
upstreamTransport string,
|
|
includeBody bool,
|
|
) 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 strings.TrimSpace(downstreamTransport) != "" {
|
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("Downstream Transport: %s\n", downstreamTransport)); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
if strings.TrimSpace(upstreamTransport) != "" {
|
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("Upstream Transport: %s\n", upstreamTransport)); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("Timestamp: %s\n", timestamp.Format(time.RFC3339Nano))); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeSectionSpacing(w, 1); 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 := writeSectionSpacing(w, 1); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
|
|
if !includeBody {
|
|
return nil
|
|
}
|
|
|
|
if _, errWrite := io.WriteString(w, "=== REQUEST BODY ===\n"); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
|
|
bodyTrailingNewlines := 1
|
|
if bodyPath != "" {
|
|
bodyFile, errOpen := os.Open(bodyPath)
|
|
if errOpen != nil {
|
|
return errOpen
|
|
}
|
|
tracker := &trailingNewlineTrackingWriter{writer: w}
|
|
written, errCopy := io.Copy(tracker, bodyFile)
|
|
if errCopy != nil {
|
|
_ = bodyFile.Close()
|
|
return errCopy
|
|
}
|
|
if written > 0 {
|
|
bodyTrailingNewlines = tracker.trailingNewlines
|
|
}
|
|
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
|
|
} else if len(body) > 0 {
|
|
bodyTrailingNewlines = countTrailingNewlinesBytes(body)
|
|
}
|
|
if errWrite := writeSectionSpacing(w, bodyTrailingNewlines); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func countTrailingNewlinesBytes(payload []byte) int {
|
|
count := 0
|
|
for i := len(payload) - 1; i >= 0; i-- {
|
|
if payload[i] != '\n' {
|
|
break
|
|
}
|
|
count++
|
|
}
|
|
return count
|
|
}
|
|
|
|
func writeSectionSpacing(w io.Writer, trailingNewlines int) error {
|
|
missingNewlines := 3 - trailingNewlines
|
|
if missingNewlines <= 0 {
|
|
return nil
|
|
}
|
|
_, errWrite := io.WriteString(w, strings.Repeat("\n", missingNewlines))
|
|
return errWrite
|
|
}
|
|
|
|
type trailingNewlineTrackingWriter struct {
|
|
writer io.Writer
|
|
trailingNewlines int
|
|
}
|
|
|
|
func (t *trailingNewlineTrackingWriter) Write(payload []byte) (int, error) {
|
|
written, errWrite := t.writer.Write(payload)
|
|
if written > 0 {
|
|
writtenPayload := payload[:written]
|
|
trailingNewlines := countTrailingNewlinesBytes(writtenPayload)
|
|
if trailingNewlines == len(writtenPayload) {
|
|
t.trailingNewlines += trailingNewlines
|
|
} else {
|
|
t.trailingNewlines = trailingNewlines
|
|
}
|
|
}
|
|
return written, errWrite
|
|
}
|
|
|
|
func hasSectionPayload(payload []byte) bool {
|
|
return len(bytes.TrimSpace(payload)) > 0
|
|
}
|
|
|
|
func hasFileBodySourcePayload(source *FileBodySource) bool {
|
|
return source != nil && source.HasPayload()
|
|
}
|
|
|
|
func inferDownstreamTransport(headers map[string][]string, websocketTimeline []byte, websocketTimelineSource *FileBodySource) string {
|
|
if hasSectionPayload(websocketTimeline) || hasFileBodySourcePayload(websocketTimelineSource) {
|
|
return "websocket"
|
|
}
|
|
for key, values := range headers {
|
|
if strings.EqualFold(strings.TrimSpace(key), "Upgrade") {
|
|
for _, value := range values {
|
|
if strings.EqualFold(strings.TrimSpace(value), "websocket") {
|
|
return "websocket"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return "http"
|
|
}
|
|
|
|
func inferUpstreamTransport(apiRequest []byte, apiRequestSource *FileBodySource, apiResponse []byte, apiResponseSource *FileBodySource, apiWebsocketTimeline []byte, apiWebsocketTimelineSource *FileBodySource, _ []*interfaces.ErrorMessage) string {
|
|
hasHTTP := hasSectionPayload(apiRequest) || hasFileBodySourcePayload(apiRequestSource) || hasSectionPayload(apiResponse) || hasFileBodySourcePayload(apiResponseSource)
|
|
hasWS := hasSectionPayload(apiWebsocketTimeline) || hasFileBodySourcePayload(apiWebsocketTimelineSource)
|
|
switch {
|
|
case hasHTTP && hasWS:
|
|
return "websocket+http"
|
|
case hasWS:
|
|
return "websocket"
|
|
case hasHTTP:
|
|
return "http"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func writeLogPart(w io.Writer, payload []byte, prependNewline bool) error {
|
|
if w == nil {
|
|
return nil
|
|
}
|
|
if prependNewline {
|
|
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeAPISection(w io.Writer, sectionHeader string, sectionPrefix string, payload []byte, timestamp time.Time) error {
|
|
if len(payload) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if bytes.HasPrefix(payload, []byte(sectionPrefix)) {
|
|
if _, errWrite := w.Write(payload); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
} else {
|
|
if _, errWrite := io.WriteString(w, sectionHeader); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if !timestamp.IsZero() {
|
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("Timestamp: %s\n", timestamp.Format(time.RFC3339Nano))); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
if _, errWrite := w.Write(payload); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
|
|
if errWrite := writeSectionSpacing(w, countTrailingNewlinesBytes(payload)); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeAPISectionWithSource(w io.Writer, sectionHeader string, sectionPrefix string, payload []byte, source *FileBodySource, timestamp time.Time) error {
|
|
if !hasFileBodySourcePayload(source) {
|
|
return writeAPISection(w, sectionHeader, sectionPrefix, payload, timestamp)
|
|
}
|
|
if len(payload) > 0 {
|
|
if errWrite := writeAPISection(w, sectionHeader, sectionPrefix, payload, timestamp); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
if _, errWrite := io.WriteString(w, sectionHeader); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if !timestamp.IsZero() {
|
|
if _, errWrite := io.WriteString(w, fmt.Sprintf("Timestamp: %s\n", timestamp.Format(time.RFC3339Nano))); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
tracker := &trailingNewlineTrackingWriter{writer: w}
|
|
if errWrite := source.WriteTo(tracker); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeSectionSpacing(w, tracker.trailingNewlines); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writePreformattedAPISectionWithSource(w io.Writer, sectionHeader string, sectionPrefix string, payload []byte, source *FileBodySource, timestamp time.Time) error {
|
|
if !hasFileBodySourcePayload(source) {
|
|
return writeAPISection(w, sectionHeader, sectionPrefix, payload, timestamp)
|
|
}
|
|
if len(payload) > 0 {
|
|
if errWrite := writeAPISection(w, sectionHeader, sectionPrefix, payload, timestamp); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
tracker := &trailingNewlineTrackingWriter{writer: w}
|
|
if errWrite := source.WriteTo(tracker); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errWrite := writeSectionSpacing(w, tracker.trailingNewlines); 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
|
|
}
|
|
trailingNewlines := 1
|
|
if apiResponseErrors[i].Error != nil {
|
|
errText := apiResponseErrors[i].Error.Error()
|
|
if _, errWrite := io.WriteString(w, errText); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
if errText != "" {
|
|
trailingNewlines = countTrailingNewlinesBytes([]byte(errText))
|
|
}
|
|
}
|
|
if errWrite := writeSectionSpacing(w, trailingNewlines); 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var bufferedReader *bufio.Reader
|
|
if responseReader != nil {
|
|
bufferedReader = bufio.NewReader(responseReader)
|
|
}
|
|
if !responseBodyStartsWithLeadingNewline(bufferedReader) {
|
|
if _, errWrite := io.WriteString(w, "\n"); errWrite != nil {
|
|
return errWrite
|
|
}
|
|
}
|
|
|
|
if bufferedReader != nil {
|
|
if _, errCopy := io.Copy(w, bufferedReader); 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 responseBodyStartsWithLeadingNewline(reader *bufio.Reader) bool {
|
|
if reader == nil {
|
|
return false
|
|
}
|
|
if peeked, _ := reader.Peek(2); len(peeked) >= 2 && peeked[0] == '\r' && peeked[1] == '\n' {
|
|
return true
|
|
}
|
|
if peeked, _ := reader.Peek(1); len(peeked) >= 1 && peeked[0] == '\n' {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// formatLogContent creates the complete log content for non-streaming requests.
|
|
//
|
|
// Parameters:
|
|
// - url: The request URL
|
|
// - method: The HTTP method
|
|
// - headers: The request headers
|
|
// - body: The request body
|
|
// - websocketTimeline: The downstream websocket event timeline
|
|
// - apiRequest: The API request data
|
|
// - apiResponse: The API response data
|
|
// - response: The raw response data
|
|
// - status: The response status code
|
|
// - responseHeaders: The response headers
|
|
//
|
|
// Returns:
|
|
// - string: The formatted log content
|
|
func (l *FileRequestLogger) formatLogContent(url, method string, headers map[string][]string, body, websocketTimeline, apiRequest, apiResponse, apiWebsocketTimeline, response []byte, status int, responseHeaders map[string][]string, apiResponseErrors []*interfaces.ErrorMessage) string {
|
|
var content strings.Builder
|
|
isWebsocketTranscript := hasSectionPayload(websocketTimeline)
|
|
downstreamTransport := inferDownstreamTransport(headers, websocketTimeline, nil)
|
|
upstreamTransport := inferUpstreamTransport(apiRequest, nil, apiResponse, nil, apiWebsocketTimeline, nil, apiResponseErrors)
|
|
|
|
// Request info
|
|
content.WriteString(l.formatRequestInfo(url, method, headers, body, downstreamTransport, upstreamTransport, !isWebsocketTranscript))
|
|
|
|
if len(websocketTimeline) > 0 {
|
|
if bytes.HasPrefix(websocketTimeline, []byte("=== WEBSOCKET TIMELINE")) {
|
|
content.Write(websocketTimeline)
|
|
if !bytes.HasSuffix(websocketTimeline, []byte("\n")) {
|
|
content.WriteString("\n")
|
|
}
|
|
} else {
|
|
content.WriteString("=== WEBSOCKET TIMELINE ===\n")
|
|
content.Write(websocketTimeline)
|
|
content.WriteString("\n")
|
|
}
|
|
content.WriteString("\n")
|
|
}
|
|
|
|
if len(apiWebsocketTimeline) > 0 {
|
|
if bytes.HasPrefix(apiWebsocketTimeline, []byte("=== API WEBSOCKET TIMELINE")) {
|
|
content.Write(apiWebsocketTimeline)
|
|
if !bytes.HasSuffix(apiWebsocketTimeline, []byte("\n")) {
|
|
content.WriteString("\n")
|
|
}
|
|
} else {
|
|
content.WriteString("=== API WEBSOCKET TIMELINE ===\n")
|
|
content.Write(apiWebsocketTimeline)
|
|
content.WriteString("\n")
|
|
}
|
|
content.WriteString("\n")
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
if isWebsocketTranscript {
|
|
// Mirror writeNonStreamingLog: websocket transcripts end with the dedicated
|
|
// timeline sections instead of a generic downstream HTTP response block.
|
|
return content.String()
|
|
}
|
|
|
|
// Response section
|
|
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()
|
|
}
|
|
|
|
// decompressResponse decompresses response data based on Content-Encoding header.
|
|
//
|
|
// Parameters:
|
|
// - responseHeaders: The response headers
|
|
// - response: The response data to decompress
|
|
//
|
|
// Returns:
|
|
// - []byte: The decompressed response data
|
|
// - error: An error if decompression fails, nil otherwise
|
|
func (l *FileRequestLogger) decompressResponse(responseHeaders map[string][]string, response []byte) ([]byte, error) {
|
|
if responseHeaders == nil || len(response) == 0 {
|
|
return response, nil
|
|
}
|
|
|
|
// Check Content-Encoding header
|
|
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:
|
|
// No compression or unsupported compression
|
|
return response, nil
|
|
}
|
|
}
|
|
|
|
// decompressGzip decompresses gzip-encoded data.
|
|
//
|
|
// Parameters:
|
|
// - data: The gzip-encoded data to decompress
|
|
//
|
|
// Returns:
|
|
// - []byte: The decompressed data
|
|
// - error: An error if decompression fails, nil otherwise
|
|
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
|
|
}
|
|
|
|
// decompressDeflate decompresses deflate-encoded data.
|
|
//
|
|
// Parameters:
|
|
// - data: The deflate-encoded data to decompress
|
|
//
|
|
// Returns:
|
|
// - []byte: The decompressed data
|
|
// - error: An error if decompression fails, nil otherwise
|
|
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
|
|
}
|
|
|
|
// decompressBrotli decompresses brotli-encoded data.
|
|
//
|
|
// Parameters:
|
|
// - data: The brotli-encoded data to decompress
|
|
//
|
|
// Returns:
|
|
// - []byte: The decompressed data
|
|
// - error: An error if decompression fails, nil otherwise
|
|
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
|
|
}
|
|
|
|
// decompressZstd decompresses zstd-encoded data.
|
|
//
|
|
// Parameters:
|
|
// - data: The zstd-encoded data to decompress
|
|
//
|
|
// Returns:
|
|
// - []byte: The decompressed data
|
|
// - error: An error if decompression fails, nil otherwise
|
|
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
|
|
}
|
|
|
|
// formatRequestInfo creates the request information section of the log.
|
|
//
|
|
// Parameters:
|
|
// - url: The request URL
|
|
// - method: The HTTP method
|
|
// - headers: The request headers
|
|
// - body: The request body
|
|
//
|
|
// Returns:
|
|
// - string: The formatted request information
|
|
func (l *FileRequestLogger) formatRequestInfo(url, method string, headers map[string][]string, body []byte, downstreamTransport string, upstreamTransport string, includeBody bool) 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))
|
|
if strings.TrimSpace(downstreamTransport) != "" {
|
|
content.WriteString(fmt.Sprintf("Downstream Transport: %s\n", downstreamTransport))
|
|
}
|
|
if strings.TrimSpace(upstreamTransport) != "" {
|
|
content.WriteString(fmt.Sprintf("Upstream Transport: %s\n", upstreamTransport))
|
|
}
|
|
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")
|
|
|
|
if !includeBody {
|
|
return content.String()
|
|
}
|
|
|
|
content.WriteString("=== REQUEST BODY ===\n")
|
|
content.Write(body)
|
|
content.WriteString("\n\n")
|
|
|
|
return content.String()
|
|
}
|