Skip to main content

Debugging Guide

This guide covers debugging techniques and tools for troubleshooting Go AI applications.

Enable Debug Logging​

SDK Warning Logging​

The SDK has no global debug/log-level switch. What it does provide is a warnings logger: every call that returns provider warnings (unsupported settings, compatibility fallbacks, etc.) is routed through ai.LogWarnings, which writes to stderr by default. Install your own sink with ai.SetLogWarnings, or silence it with ai.DisableLogWarnings() (or the AI_SDK_LOG_WARNINGS=false environment variable):

package main

import (
"log"

"github.com/digitallysavvy/go-ai/pkg/ai"
)

func main() {
// Route SDK warnings into your own logger instead of stderr.
ai.SetLogWarnings(func(opts ai.LogWarningsOptions) {
for _, w := range opts.Warnings {
log.Printf("[%s/%s] %s", opts.Provider, opts.Model, ai.FormatWarning(w, opts.Provider, opts.Model))
}
})

// Your code here...
}

For full HTTP request/response visibility, wrap the HTTP client instead — see below.

Request/Response Logging​

package main

import (
"bytes"
"context"
"fmt"
"io"
"log"
"net/http"
"os"

"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)

// Custom HTTP transport with logging
type LoggingTransport struct {
Transport http.RoundTripper
}

func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Log request
log.Printf("=== REQUEST ===")
log.Printf("%s %s", req.Method, req.URL)
log.Printf("Headers: %v", req.Header)

if req.Body != nil {
bodyBytes, _ := io.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
log.Printf("Body: %s", string(bodyBytes))
}

// Execute request
resp, err := t.Transport.RoundTrip(req)
if err != nil {
log.Printf("Request error: %v", err)
return nil, err
}

// Log response
log.Printf("=== RESPONSE ===")
log.Printf("Status: %d %s", resp.StatusCode, resp.Status)
log.Printf("Headers: %v", resp.Header)

if resp.Body != nil {
bodyBytes, _ := io.ReadAll(resp.Body)
resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
log.Printf("Body: %s", string(bodyBytes))
}

return resp, nil
}

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

// Create HTTP client with logging transport
httpClient := &http.Client{
Transport: &LoggingTransport{
Transport: http.DefaultTransport,
},
}

provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
HTTPClient: httpClient,
})

model, _ := provider.LanguageModel("gpt-4o-mini")

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Hello!",
})
if err != nil {
log.Fatal(err)
}

fmt.Println(result.Text)
}

Debugging Goroutines​

Detect Goroutine Leaks​

package main

import (
"context"
"fmt"
"log"
"os"
"runtime"
"time"

"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)

func printGoroutineCount(label string) {
count := runtime.NumGoroutine()
log.Printf("[%s] Active goroutines: %d", label, count)
}

func detectLeaks() {
printGoroutineCount("Start")

ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4o-mini")

// Start streaming
stream, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a short story",
})
if err != nil {
log.Fatal(err)
}

printGoroutineCount("After stream start")

// IMPORTANT: Consume the stream completely
for chunk := range stream.Chunks() {
fmt.Print(chunk.Text)
}

printGoroutineCount("After stream complete")

// Wait for cleanup
time.Sleep(100 * time.Millisecond)

printGoroutineCount("After cleanup wait")

// Force garbage collection
runtime.GC()
time.Sleep(100 * time.Millisecond)

printGoroutineCount("After GC")
}

func main() {
detectLeaks()
}

Stack Traces for Debugging​

package main

import (
"fmt"
"os"
"os/signal"
"runtime"
"syscall"
)

func dumpStackTraces() {
buf := make([]byte, 1024*1024)
stackSize := runtime.Stack(buf, true)
fmt.Printf("=== Stack Traces ===\n%s\n", buf[:stackSize])
}

func setupDebugHandlers() {
// Dump stack traces on SIGUSR1
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGUSR1)

go func() {
for range sigCh {
dumpStackTraces()
}
}()
}

func main() {
setupDebugHandlers()

// Your application code...
select {}
}

Debugging with pprof​

CPU and Memory Profiling​

package main

import (
"context"
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"os"
"runtime"
"runtime/pprof"

"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)

func startProfiling() {
// Start pprof HTTP server
go func() {
log.Println("Starting pprof server on :6060")
log.Println(http.ListenAndServe("localhost:6060", nil))
}()

// Or profile to files
cpuFile, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(cpuFile)
defer pprof.StopCPUProfile()

memFile, _ := os.Create("mem.prof")
defer func() {
runtime.GC()
pprof.WriteHeapProfile(memFile)
memFile.Close()
}()
}

func main() {
startProfiling()

ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4o-mini")

// Run your code to profile
for i := 0; i < 10; i++ {
result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: fmt.Sprintf("Request %d", i),
})
fmt.Println(result.Text)
}

// View profiles:
// go tool pprof cpu.prof
// go tool pprof mem.prof
// Or visit http://localhost:6060/debug/pprof/
}

Debugging Streams​

Stream Debug Wrapper​

package main

import (
"context"
"fmt"
"log"
"os"
"time"

"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)

func debugStream(ctx context.Context, model provider.LanguageModel, prompt string) error {
log.Printf("Starting stream for prompt: %s", prompt)

stream, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: prompt,
})
if err != nil {
return fmt.Errorf("stream creation failed: %w", err)
}

startTime := time.Now()
chunkCount := 0
totalBytes := 0
var firstChunkTime time.Duration

log.Println("Waiting for chunks...")

for chunk := range stream.Chunks() {
if chunkCount == 0 {
firstChunkTime = time.Since(startTime)
log.Printf("Time to first chunk: %v", firstChunkTime)
}

chunkCount++

switch chunk.Type {
case provider.ChunkTypeText:
totalBytes += len(chunk.Text)
log.Printf("Chunk %d: %d bytes (type: text)", chunkCount, len(chunk.Text))
fmt.Print(chunk.Text)

case provider.ChunkTypeToolCall:
log.Printf("Chunk %d: tool call %s (id: %s)",
chunkCount, chunk.ToolCall.ToolName, chunk.ToolCall.ID)

case provider.ChunkTypeError:
log.Printf("Chunk %d: ERROR - %v", chunkCount, chunk.Err)

case provider.ChunkTypeFinish:
log.Printf("Chunk %d: FINISH - reason: %s", chunkCount, chunk.FinishReason)
}
}

if err := stream.Err(); err != nil {
log.Printf("Stream error: %v", err)
return err
}

totalTime := time.Since(startTime)
bytesPerSecond := float64(totalBytes) / totalTime.Seconds()

log.Printf("\n=== Stream Statistics ===")
log.Printf("Total chunks: %d", chunkCount)
log.Printf("Total bytes: %d", totalBytes)
log.Printf("Time to first chunk: %v", firstChunkTime)
log.Printf("Total time: %v", totalTime)
log.Printf("Throughput: %.2f bytes/sec", bytesPerSecond)

return nil
}

func main() {
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")

if err := debugStream(ctx, model, "Write a short story about a robot"); err != nil {
log.Fatalf("Debug stream failed: %v", err)
}
}

Error Tracking​

Structured Error Logging​

package main

import (
"context"
"encoding/json"
"errors"
"log"
"time"

"github.com/digitallysavvy/go-ai/pkg/ai"
providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)

type ErrorLog struct {
Timestamp time.Time `json:"timestamp"`
ErrorType string `json:"error_type"`
Message string `json:"message"`
Provider string `json:"provider,omitempty"`
StatusCode int `json:"status_code,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
Operation string `json:"operation"`
Prompt string `json:"prompt,omitempty"`
StackTrace string `json:"stack_trace,omitempty"`
Additional map[string]interface{} `json:"additional,omitempty"`
}

func logError(err error, operation string, prompt string) {
errorLog := ErrorLog{
Timestamp: time.Now(),
Message: err.Error(),
Operation: operation,
Prompt: prompt,
Additional: make(map[string]interface{}),
}

// Check for specific error types
var providerErr *providererrors.ProviderError
if errors.As(err, &providerErr) {
errorLog.ErrorType = "provider_error"
errorLog.Provider = providerErr.Provider
errorLog.StatusCode = providerErr.StatusCode
errorLog.ErrorCode = providerErr.ErrorCode
}

var rateLimitErr *providererrors.RateLimitError
if errors.As(err, &rateLimitErr) {
errorLog.ErrorType = "rate_limit_error"
errorLog.Provider = rateLimitErr.Provider
if rateLimitErr.RetryAfterSeconds != nil {
errorLog.Additional["retry_after_seconds"] = *rateLimitErr.RetryAfterSeconds
}
}

var validationErr *providererrors.ValidationError
if errors.As(err, &validationErr) {
errorLog.ErrorType = "validation_error"
errorLog.Additional["field"] = validationErr.Field
}

// Log as JSON
jsonLog, _ := json.MarshalIndent(errorLog, "", " ")
log.Printf("ERROR LOG:\n%s", string(jsonLog))
}

func main() {
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: "invalid-key", // Will cause auth error
})
model, _ := provider.LanguageModel("gpt-4")

prompt := "Hello, world!"
_, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: prompt,
})

if err != nil {
logError(err, "generate_text", prompt)
}
}

Testing and Debugging​

Mock Provider for Testing​

package main

import (
"context"
"fmt"
"testing"

"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider"
"github.com/digitallysavvy/go-ai/pkg/provider/types"
"github.com/digitallysavvy/go-ai/pkg/testutil"
)

func int64Ptr(v int64) *int64 {
return &v
}

func TestGeneration(t *testing.T) {
mock := &testutil.MockLanguageModel{
DoGenerateFunc: func(ctx context.Context, opts *provider.GenerateOptions) (*types.GenerateResult, error) {
return &types.GenerateResult{
Text: "Test response",
FinishReason: types.FinishReasonStop,
Usage: types.Usage{
InputTokens: int64Ptr(10),
OutputTokens: int64Ptr(20),
TotalTokens: int64Ptr(30),
},
}, nil
},
}

result, err := ai.GenerateText(context.Background(), ai.GenerateTextOptions{
Model: mock,
Prompt: "Test prompt",
})

if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}

if result.Text != "Test response" {
t.Fatalf("Expected 'Test response', got: %s", result.Text)
}
}

func TestGenerationError(t *testing.T) {
mock := &testutil.MockLanguageModel{
DoGenerateFunc: func(ctx context.Context, opts *provider.GenerateOptions) (*types.GenerateResult, error) {
return nil, fmt.Errorf("mock error")
},
}

_, err := ai.GenerateText(context.Background(), ai.GenerateTextOptions{
Model: mock,
Prompt: "Test prompt",
})

if err == nil {
t.Fatal("Expected error, got nil")
}
}

testutil.MockLanguageModel implements provider.LanguageModel for you — see Testing for its other hooks (DoStreamFunc, call tracking, etc.).

Best Practices​

1. Use Structured Logging​

log.Printf("[%s] %s: %v", level, operation, details)

2. Log Request Context​

log.Printf("Request: model=%s, prompt_length=%d, max_tokens=%d",
modelName, len(prompt), maxTokens)

3. Monitor Goroutines​

log.Printf("Active goroutines: %d", runtime.NumGoroutine())

4. Profile Performance​

import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()

5. Test with Mocks​

mock := &testutil.MockLanguageModel{}
result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{Model: mock})

See Also​