Skip to content

LoggingTransport

LoggingTransport is an http.RoundTripper that captures HTTP traffic from Go's http.Client.

Overview

LoggingTransport wraps any http.RoundTripper (typically http.DefaultTransport) and records all HTTP request/response pairs to an IRWriter.

transport := ir.NewLoggingTransport(http.DefaultTransport, writer)
client := &http.Client{Transport: transport}

// All requests through this client are automatically logged
resp, err := client.Get("https://api.example.com/users")

Basic Usage

package main

import (
    "context"
    "net/http"

    "github.com/grokify/traffic2openapi/pkg/ir"
)

func main() {
    ctx := context.Background()

    // Create a provider and writer
    provider := ir.GzipNDJSON()
    writer, _ := provider.NewWriter(ctx, "traffic.ndjson.gz")
    defer writer.Close()

    // Wrap the default transport
    transport := ir.NewLoggingTransport(http.DefaultTransport, writer)

    // Create client with logging transport
    client := &http.Client{Transport: transport}

    // Make requests - they're automatically captured
    resp, _ := client.Get("https://api.example.com/users")
    defer resp.Body.Close()

    // POST with body
    client.Post("https://api.example.com/users",
        "application/json",
        strings.NewReader(`{"name": "Alice"}`))
}

Configuration Options

Header Filtering

Exclude sensitive headers from captured records:

transport := ir.NewLoggingTransport(http.DefaultTransport, writer,
    ir.WithFilterHeaders("Authorization", "Cookie", "X-API-Key"),
)

Default filtered headers:

  • Authorization
  • Cookie
  • Set-Cookie
  • X-API-Key
  • X-Auth-Token

Path Filtering

Skip logging for specific paths:

transport := ir.NewLoggingTransport(http.DefaultTransport, writer,
    ir.WithSkipPaths("/health", "/metrics", "/ping"),
)

Method Filtering

Only log specific HTTP methods:

transport := ir.NewLoggingTransport(http.DefaultTransport, writer,
    ir.WithAllowMethods("GET", "POST", "PUT", "DELETE"),
)

Status Code Filtering

Skip logging for specific status codes:

transport := ir.NewLoggingTransport(http.DefaultTransport, writer,
    ir.WithSkipStatusCodes(404, 500, 502, 503),
)

Request ID Headers

Extract request IDs from headers:

transport := ir.NewLoggingTransport(http.DefaultTransport, writer,
    ir.WithRequestIDHeaders("X-Request-ID", "X-Correlation-ID"),
)

If no header is found, a UUID is generated.

Error Handler

Custom error handling:

transport := ir.NewLoggingTransport(http.DefaultTransport, writer,
    ir.WithErrorHandler(func(err error) {
        log.Printf("logging error: %v", err)
    }),
)

Full Example with Options

transport := ir.NewLoggingTransport(http.DefaultTransport, writer,
    // Security
    ir.WithFilterHeaders("Authorization", "Cookie", "X-API-Key"),

    // Skip non-API paths
    ir.WithSkipPaths("/health", "/metrics", "/_next"),

    // Only log mutations
    ir.WithAllowMethods("POST", "PUT", "PATCH", "DELETE"),

    // Skip error responses
    ir.WithSkipStatusCodes(500, 502, 503),

    // Correlation
    ir.WithRequestIDHeaders("X-Request-ID", "X-Trace-ID"),

    // Error handling
    ir.WithErrorHandler(func(err error) {
        slog.Error("traffic logging failed", "error", err)
    }),
)

client := &http.Client{Transport: transport}

Captured Data

Each request/response pair is captured as an IRRecord:

Field Captured
Request method Yes
Request path Yes
Request query params Yes
Request headers Yes (filtered)
Request body Yes (JSON parsed)
Response status Yes
Response headers Yes
Response body Yes (JSON parsed)
Duration Yes
Timestamp Yes
Request ID Yes (from header or generated)

Integration Patterns

With Channel Provider for Real-time Processing

provider := ir.Channel(ir.WithChannelProviderBufferSize(1000))

// Writer for transport
writer, _ := provider.NewWriter(ctx, "")

// Reader for processing
reader, _ := provider.NewReader(ctx, "")

// Process records in background
go func() {
    for {
        record, err := reader.Read()
        if err == io.EOF {
            break
        }
        // Real-time processing: metrics, alerts, etc.
        processRecord(record)
    }
}()

// HTTP client with logging
transport := ir.NewLoggingTransport(http.DefaultTransport, writer)
client := &http.Client{Transport: transport}

With MultiWriter for Multiple Destinations

// Write to file and channel simultaneously
fileWriter, _ := fileProvider.NewWriter(ctx, "traffic.ndjson.gz")
channelWriter, _ := channelProvider.NewWriter(ctx, "")

multiWriter := ir.NewMultiWriter(fileWriter, channelWriter)

transport := ir.NewLoggingTransport(http.DefaultTransport, multiWriter)
client := &http.Client{Transport: transport}

With Async Writer for Non-blocking Logging

// Async writer doesn't block HTTP requests
asyncWriter := ir.NewAsyncNDJSONWriter(baseWriter, 1000)
defer asyncWriter.Close()

transport := ir.NewLoggingTransport(http.DefaultTransport, asyncWriter)
client := &http.Client{Transport: transport}

Testing

LoggingTransport works well with httptest:

func TestAPI(t *testing.T) {
    // Setup test server
    server := httptest.NewServer(http.HandlerFunc(handler))
    defer server.Close()

    // Capture traffic
    var records []*ir.IRRecord
    sliceWriter := ir.NewSliceWriter(&records)

    transport := ir.NewLoggingTransport(http.DefaultTransport, sliceWriter)
    client := &http.Client{Transport: transport}

    // Make requests
    client.Get(server.URL + "/users")

    // Verify captured traffic
    if len(records) != 1 {
        t.Errorf("expected 1 record, got %d", len(records))
    }
}

Thread Safety

LoggingTransport is safe for concurrent use. Multiple goroutines can share the same client:

client := &http.Client{Transport: transport}

var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        client.Get("https://api.example.com/users")
    }()
}
wg.Wait()

The underlying IRWriter must also be thread-safe. All built-in writers (NDJSONWriter, GzipNDJSONWriter, ChannelWriter) are thread-safe.