http.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  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 slog.Default().Enabled(req.Context(), slog.LevelDebug) {
  60. slog.Debug(
  61. "HTTP Response",
  62. "status_code", resp.StatusCode,
  63. "status", resp.Status,
  64. "headers", formatHeaders(resp.Header),
  65. "body", bodyToString(save),
  66. "content_length", resp.ContentLength,
  67. "duration_ms", duration.Milliseconds(),
  68. "error", err,
  69. )
  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.Indent(&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. }