http.go 2.9 KB

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