http.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. package log
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "io"
  6. "log/slog"
  7. "net/http"
  8. "strings"
  9. "time"
  10. )
  11. // NewHTTPClient creates an HTTP client with debug logging enabled when debug mode is on.
  12. func NewHTTPClient() *http.Client {
  13. return &http.Client{
  14. Transport: &HTTPRoundTripLogger{
  15. Transport: http.DefaultTransport,
  16. },
  17. }
  18. }
  19. // HTTPRoundTripLogger is an http.RoundTripper that logs requests and responses.
  20. type HTTPRoundTripLogger struct {
  21. Transport http.RoundTripper
  22. }
  23. // RoundTrip implements http.RoundTripper interface with logging.
  24. func (h *HTTPRoundTripLogger) RoundTrip(req *http.Request) (*http.Response, error) {
  25. var err error
  26. var save io.ReadCloser
  27. save, req.Body, err = drainBody(req.Body)
  28. if err != nil {
  29. slog.Error(
  30. "HTTP request failed",
  31. "method", req.Method,
  32. "url", req.URL,
  33. "error", err,
  34. )
  35. return nil, err
  36. }
  37. if slog.Default().Enabled(req.Context(), slog.LevelDebug) {
  38. slog.Debug(
  39. "HTTP Request",
  40. "method", req.Method,
  41. "url", req.URL,
  42. "body", bodyToString(save),
  43. )
  44. }
  45. start := time.Now()
  46. resp, err := h.Transport.RoundTrip(req)
  47. duration := time.Since(start)
  48. if err != nil {
  49. slog.Error(
  50. "HTTP request failed",
  51. "method", req.Method,
  52. "url", req.URL,
  53. "duration_ms", duration.Milliseconds(),
  54. "error", err,
  55. )
  56. return resp, err
  57. }
  58. save, resp.Body, err = drainBody(resp.Body)
  59. if err != nil {
  60. slog.Error("Failed to drain response body", "error", err)
  61. return resp, err
  62. }
  63. if slog.Default().Enabled(req.Context(), slog.LevelDebug) {
  64. slog.Debug(
  65. "HTTP Response",
  66. "status_code", resp.StatusCode,
  67. "status", resp.Status,
  68. "headers", formatHeaders(resp.Header),
  69. "body", bodyToString(save),
  70. "content_length", resp.ContentLength,
  71. "duration_ms", duration.Milliseconds(),
  72. )
  73. }
  74. return resp, nil
  75. }
  76. func bodyToString(body io.ReadCloser) string {
  77. if body == nil {
  78. return ""
  79. }
  80. src, err := io.ReadAll(body)
  81. if err != nil {
  82. slog.Error("Failed to read body", "error", err)
  83. return ""
  84. }
  85. var b bytes.Buffer
  86. if json.Indent(&b, bytes.TrimSpace(src), "", " ") != nil {
  87. // not json probably
  88. return string(src)
  89. }
  90. return b.String()
  91. }
  92. // formatHeaders formats HTTP headers for logging, filtering out sensitive information.
  93. func formatHeaders(headers http.Header) map[string][]string {
  94. filtered := make(map[string][]string)
  95. for key, values := range headers {
  96. lowerKey := strings.ToLower(key)
  97. // Filter out sensitive headers
  98. if strings.Contains(lowerKey, "authorization") ||
  99. strings.Contains(lowerKey, "api-key") ||
  100. strings.Contains(lowerKey, "token") ||
  101. strings.Contains(lowerKey, "secret") {
  102. filtered[key] = []string{"[REDACTED]"}
  103. } else {
  104. filtered[key] = values
  105. }
  106. }
  107. return filtered
  108. }
  109. func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
  110. if b == nil || b == http.NoBody {
  111. return http.NoBody, http.NoBody, nil
  112. }
  113. var buf bytes.Buffer
  114. if _, err = buf.ReadFrom(b); err != nil {
  115. return nil, b, err
  116. }
  117. if err = b.Close(); err != nil {
  118. return nil, b, err
  119. }
  120. return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), nil
  121. }