| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- package log
- import (
- "bytes"
- "encoding/json"
- "io"
- "log/slog"
- "net/http"
- "strings"
- "time"
- )
- // NewHTTPClient creates an HTTP client with debug logging enabled when debug mode is on.
- func NewHTTPClient() *http.Client {
- return &http.Client{
- Transport: &HTTPRoundTripLogger{
- Transport: http.DefaultTransport,
- },
- }
- }
- // HTTPRoundTripLogger is an http.RoundTripper that logs requests and responses.
- type HTTPRoundTripLogger struct {
- Transport http.RoundTripper
- }
- // RoundTrip implements http.RoundTripper interface with logging.
- func (h *HTTPRoundTripLogger) RoundTrip(req *http.Request) (*http.Response, error) {
- var err error
- var save io.ReadCloser
- save, req.Body, err = drainBody(req.Body)
- if err != nil {
- slog.Error(
- "HTTP request failed",
- "method", req.Method,
- "url", req.URL,
- "error", err,
- )
- return nil, err
- }
- if slog.Default().Enabled(req.Context(), slog.LevelDebug) {
- slog.Debug(
- "HTTP Request",
- "method", req.Method,
- "url", req.URL,
- "body", bodyToString(save),
- )
- }
- start := time.Now()
- resp, err := h.Transport.RoundTrip(req)
- duration := time.Since(start)
- if err != nil {
- slog.Error(
- "HTTP request failed",
- "method", req.Method,
- "url", req.URL,
- "duration_ms", duration.Milliseconds(),
- "error", err,
- )
- return resp, err
- }
- save, resp.Body, err = drainBody(resp.Body)
- if slog.Default().Enabled(req.Context(), slog.LevelDebug) {
- slog.Debug(
- "HTTP Response",
- "status_code", resp.StatusCode,
- "status", resp.Status,
- "headers", formatHeaders(resp.Header),
- "body", bodyToString(save),
- "content_length", resp.ContentLength,
- "duration_ms", duration.Milliseconds(),
- "error", err,
- )
- }
- return resp, err
- }
- func bodyToString(body io.ReadCloser) string {
- if body == nil {
- return ""
- }
- src, err := io.ReadAll(body)
- if err != nil {
- slog.Error("Failed to read body", "error", err)
- return ""
- }
- var b bytes.Buffer
- if json.Indent(&b, bytes.TrimSpace(src), "", " ") != nil {
- // not json probably
- return string(src)
- }
- return b.String()
- }
- // formatHeaders formats HTTP headers for logging, filtering out sensitive information.
- func formatHeaders(headers http.Header) map[string][]string {
- filtered := make(map[string][]string)
- for key, values := range headers {
- lowerKey := strings.ToLower(key)
- // Filter out sensitive headers
- if strings.Contains(lowerKey, "authorization") ||
- strings.Contains(lowerKey, "api-key") ||
- strings.Contains(lowerKey, "token") ||
- strings.Contains(lowerKey, "secret") {
- filtered[key] = []string{"[REDACTED]"}
- } else {
- filtered[key] = values
- }
- }
- return filtered
- }
- func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
- if b == nil || b == http.NoBody {
- return http.NoBody, http.NoBody, nil
- }
- var buf bytes.Buffer
- if _, err = buf.ReadFrom(b); err != nil {
- return nil, b, err
- }
- if err = b.Close(); err != nil {
- return nil, b, err
- }
- return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), nil
- }
|