Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions option/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
package option

import (
"context"
"log"
"log/slog"
"net/http"
"net/http/httputil"
"time"
)

// WithDebugLog logs the HTTP request and response content.
Expand Down Expand Up @@ -36,3 +39,46 @@ func WithDebugLog(logger *log.Logger) RequestOption {
return resp, err
})
}

// WithLogger logs the HTTP request and response information using a structured
// logger.
func WithLogger(logger StructuredLogger) RequestOption {
return WithMiddleware(func(req *http.Request, nxt MiddlewareNext) (*http.Response, error) {
if logger == nil {
logger = slog.Default()
}

start := time.Now()
resp, err := nxt(req)
duration := time.Since(start)

if err != nil {
logger.Error("HTTP request failed",
slog.String("method", req.Method),
slog.String("url", req.URL.String()),
slog.Duration("duration", duration),
slog.Any("error", err),
)
return resp, err
}

logger.Info("HTTP request completed",
slog.String("method", req.Method),
slog.String("url", req.URL.String()),
slog.Int("status", resp.StatusCode),
slog.Duration("duration", duration),
)

return resp, err
})
}

// StructuredLogger is an interface for structured logging, compatible with
// [log/slog.Logger].
type StructuredLogger interface {
Info(msg string, args ...any)
Error(msg string, args ...any)
Warn(msg string, args ...any)
Debug(msg string, args ...any)
Log(ctx context.Context, level slog.Level, msg string, args ...any)
}
68 changes: 68 additions & 0 deletions option/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package option_test

import (
"bytes"
"context"
"io"
"log/slog"
"net/http"
"testing"

"github.com/logchimp/logchimp-go"
"github.com/logchimp/logchimp-go/option"
)

type mockLogger struct {
infoCalled bool
errorCalled bool
lastMsg string
}

func (m *mockLogger) Info(msg string, args ...any) {
m.infoCalled = true
m.lastMsg = msg
}

func (m *mockLogger) Error(msg string, args ...any) {
m.errorCalled = true
m.lastMsg = msg
}

func (m *mockLogger) Warn(msg string, args ...any) {}
func (m *mockLogger) Debug(msg string, args ...any) {}
func (m *mockLogger) Log(ctx context.Context, level slog.Level, msg string, args ...any) {}

type closureTransport struct {
fn func(req *http.Request) (*http.Response, error)
}

func (t *closureTransport) Do(req *http.Request) (*http.Response, error) {
return t.fn(req)
}

func TestWithLogger(t *testing.T) {
logger := &mockLogger{}
client := logchimp.NewClient(
option.WithHTTPClient(&closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString("{}")),
}, nil
},
}),
option.WithLogger(logger),
)

client.Auth.Login(context.Background(), logchimp.AuthLoginParams{
Email: "[email protected]",
Password: "password",
})

if !logger.infoCalled {
t.Error("Expected logger.Info to be called")
}
if logger.lastMsg != "HTTP request completed" {
t.Errorf("Expected message 'HTTP request completed', got %q", logger.lastMsg)
}
}