http.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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. slog.Debug(
  38. "HTTP Request",
  39. "method", req.Method,
  40. "url", req.URL,
  41. "body", bodyToString(save),
  42. )
  43. start := time.Now()
  44. resp, err := h.Transport.RoundTrip(req)
  45. duration := time.Since(start)
  46. if err != nil {
  47. slog.Error(
  48. "HTTP request failed",
  49. "method", req.Method,
  50. "url", req.URL,
  51. "duration_ms", duration.Milliseconds(),
  52. "error", err,
  53. )
  54. return resp, err
  55. }
  56. save, resp.Body, err = drainBody(resp.Body)
  57. slog.Debug(
  58. "HTTP Response",
  59. "status_code", resp.StatusCode,
  60. "status", resp.Status,
  61. "headers", formatHeaders(resp.Header),
  62. "body", bodyToString(save),
  63. "content_length", resp.ContentLength,
  64. "duration_ms", duration.Milliseconds(),
  65. "error", err,
  66. )
  67. return resp, err
  68. }
  69. func bodyToString(body io.ReadCloser) string {
  70. if body == nil {
  71. return ""
  72. }
  73. src, err := io.ReadAll(body)
  74. if err != nil {
  75. slog.Error("Failed to read body", "error", err)
  76. return ""
  77. }
  78. var b bytes.Buffer
  79. if json.Compact(&b, bytes.TrimSpace(src)) != nil {
  80. // not json probably
  81. return string(src)
  82. }
  83. return b.String()
  84. }
  85. // formatHeaders formats HTTP headers for logging, filtering out sensitive information.
  86. func formatHeaders(headers http.Header) map[string][]string {
  87. filtered := make(map[string][]string)
  88. for key, values := range headers {
  89. lowerKey := strings.ToLower(key)
  90. // Filter out sensitive headers
  91. if strings.Contains(lowerKey, "authorization") ||
  92. strings.Contains(lowerKey, "api-key") ||
  93. strings.Contains(lowerKey, "token") ||
  94. strings.Contains(lowerKey, "secret") {
  95. filtered[key] = []string{"[REDACTED]"}
  96. } else {
  97. filtered[key] = values
  98. }
  99. }
  100. return filtered
  101. }
  102. func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
  103. if b == nil || b == http.NoBody {
  104. return http.NoBody, http.NoBody, nil
  105. }
  106. var buf bytes.Buffer
  107. if _, err = buf.ReadFrom(b); err != nil {
  108. return nil, b, err
  109. }
  110. if err = b.Close(); err != nil {
  111. return nil, b, err
  112. }
  113. return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), nil
  114. }