Development Tools & Debugging
The Go-AI SDK provides several tools and techniques for debugging, observability, and testing your AI applications. This guide covers logging, request inspection, performance profiling, and testing utilities.
Debug Mode
Enabling Debug Logging
There is no built-in DebugMode flag. Every provider's Config accepts an
HTTPClient override, so you can log requests and responses by supplying an
http.Client with a logging http.RoundTripper (see
Intercepting HTTP Requests below for the
LoggingTransport implementation):
package main
import (
"context"
"log"
"net/http"
"os"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
// simpleLoggingTransport logs the method and URL of every request.
// See LoggingTransport under "Intercepting HTTP Requests" below for a
// version that also logs headers and bodies.
type simpleLoggingTransport struct {
Transport http.RoundTripper
}
func (t *simpleLoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
log.Printf("Request: %s %s", req.Method, req.URL)
resp, err := t.Transport.RoundTrip(req)
if err == nil {
log.Printf("Response Status: %s", resp.Status)
}
return resp, err
}
func main() {
ctx := context.Background()
// Log all HTTP requests and responses via a custom HTTPClient
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
HTTPClient: &http.Client{
Transport: &simpleLoggingTransport{Transport: http.DefaultTransport},
},
})
model, _ := provider.LanguageModel("gpt-4")
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Hello, world!",
})
if err != nil {
log.Fatal(err)
}
log.Printf("Result: %s", result.Text)
}
Provider-Specific Debug Options
The same HTTPClient override works for every provider:
// Anthropic
anthropicProvider := anthropic.New(anthropic.Config{
APIKey: os.Getenv("ANTHROPIC_API_KEY"),
HTTPClient: httpClient,
})
// Google
googleProvider := google.New(google.Config{
APIKey: os.Getenv("GOOGLE_GENERATIVE_AI_API_KEY"),
HTTPClient: httpClient,
})
// Bedrock does not expose an HTTPClient override; use Headers for
// provider-visible debug markers, or inspect traffic at the network layer.
bedrockProvider := bedrock.New(bedrock.Config{
Region: "us-east-1",
})
Request & Response Inspection
Logging Request Details
Inspect full request configuration before sending:
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func main() {
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
temperature := 0.7
maxTokens := 500
options := ai.GenerateTextOptions{
Model: model,
Prompt: "Explain quantum computing",
Temperature: &temperature,
MaxTokens: &maxTokens,
System: "You are a physics professor.",
}
// Log request configuration
fmt.Println("=== Request Configuration ===")
fmt.Printf("Model: %T\n", options.Model)
fmt.Printf("Temperature: %.2f\n", *options.Temperature)
fmt.Printf("MaxTokens: %d\n", *options.MaxTokens)
fmt.Printf("System: %s\n", options.System)
fmt.Printf("Prompt: %s\n", options.Prompt)
fmt.Println()
result, err := ai.GenerateText(ctx, options)
if err != nil {
log.Fatal(err)
}
// Log response details
fmt.Println("=== Response Details ===")
fmt.Printf("Text: %s\n", result.Text)
fmt.Printf("Finish Reason: %s\n", result.FinishReason)
fmt.Printf("Model: %s\n", result.Response.ModelID)
fmt.Println()
// Log usage statistics
fmt.Println("=== Usage Statistics ===")
fmt.Printf("Prompt Tokens: %d\n", result.Usage.GetInputTokens())
fmt.Printf("Completion Tokens: %d\n", result.Usage.GetOutputTokens())
fmt.Printf("Total Tokens: %d\n", result.Usage.GetTotalTokens())
}
Intercepting HTTP Requests
Use a custom HTTP client to intercept and log requests:
package main
import (
"bytes"
"context"
"io"
"log"
"net/http"
"os"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
// LoggingTransport wraps http.RoundTripper to log requests
type LoggingTransport struct {
Transport http.RoundTripper
}
func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Log request
log.Printf("Request: %s %s", req.Method, req.URL)
log.Printf("Headers: %v", req.Header)
// Read and log body
if req.Body != nil {
body, _ := io.ReadAll(req.Body)
log.Printf("Body: %s", string(body))
req.Body = io.NopCloser(bytes.NewBuffer(body))
}
// Execute request
resp, err := t.Transport.RoundTrip(req)
if err != nil {
return nil, err
}
// Log response
log.Printf("Response Status: %s", resp.Status)
log.Printf("Response Headers: %v", resp.Header)
return resp, nil
}
func main() {
ctx := context.Background()
// Create custom HTTP client with logging
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-4")
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Hello!",
})
if err != nil {
log.Fatal(err)
}
log.Printf("Result: %s", result.Text)
}
Token Usage Monitoring
Tracking Token Consumption
Monitor token usage to optimize costs:
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
type TokenTracker struct {
TotalPromptTokens int64
TotalCompletionTokens int64
TotalCost float64
}
func (t *TokenTracker) Track(result *ai.GenerateTextResult) {
t.TotalPromptTokens += result.Usage.GetInputTokens()
t.TotalCompletionTokens += result.Usage.GetOutputTokens()
// GPT-4 pricing (example)
promptCost := float64(result.Usage.GetInputTokens()) * 0.03 / 1000
completionCost := float64(result.Usage.GetOutputTokens()) * 0.06 / 1000
t.TotalCost += promptCost + completionCost
}
func (t *TokenTracker) Report() {
fmt.Println("=== Token Usage Report ===")
fmt.Printf("Total Prompt Tokens: %d\n", t.TotalPromptTokens)
fmt.Printf("Total Completion Tokens: %d\n", t.TotalCompletionTokens)
fmt.Printf("Total Tokens: %d\n", t.TotalPromptTokens+t.TotalCompletionTokens)
fmt.Printf("Estimated Cost: $%.4f\n", t.TotalCost)
}
func main() {
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
tracker := &TokenTracker{}
// Make multiple requests
prompts := []string{
"What is Go?",
"Explain concurrency in Go",
"What are goroutines?",
}
for _, prompt := range prompts {
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: prompt,
})
if err != nil {
log.Fatal(err)
}
tracker.Track(result)
fmt.Printf("Prompt: %s\n", prompt)
fmt.Printf("Response: %s\n", result.Text)
fmt.Printf("Tokens: %d\n\n", result.Usage.GetTotalTokens())
}
tracker.Report()
}
Setting Token Budgets
Enforce token limits to prevent runaway costs:
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
type BudgetedAI struct {
Provider *openai.Provider
TokenBudget int64
TokensUsed int64
}
func (b *BudgetedAI) GenerateText(ctx context.Context, opts ai.GenerateTextOptions) (*ai.GenerateTextResult, error) {
if b.TokensUsed >= b.TokenBudget {
return nil, errors.New("token budget exceeded")
}
result, err := ai.GenerateText(ctx, opts)
if err != nil {
return nil, err
}
b.TokensUsed += result.Usage.GetTotalTokens()
fmt.Printf("Tokens used: %d/%d (%.1f%%)\n",
b.TokensUsed, b.TokenBudget,
float64(b.TokensUsed)/float64(b.TokenBudget)*100)
return result, nil
}
func main() {
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
budgetedAI := &BudgetedAI{
Provider: provider,
TokenBudget: 1000, // Maximum 1000 tokens
}
model, _ := provider.LanguageModel("gpt-4")
result, err := budgetedAI.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Write a short story",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Text)
}
Performance Profiling
Using Go's pprof
Profile CPU and memory usage:
package main
import (
"context"
"log"
"os"
"runtime/pprof"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func main() {
// Start CPU profiling
cpuFile, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
defer cpuFile.Close()
pprof.StartCPUProfile(cpuFile)
defer pprof.StopCPUProfile()
// Your AI operations
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
for i := 0; i < 10; i++ {
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Hello!",
})
if err != nil {
log.Fatal(err)
}
log.Printf("Iteration %d: %s", i, result.Text)
}
// Write heap profile
heapFile, err := os.Create("heap.prof")
if err != nil {
log.Fatal(err)
}
defer heapFile.Close()
pprof.WriteHeapProfile(heapFile)
}
Analyze profiles:
# View CPU profile
go tool pprof cpu.prof
# View memory profile
go tool pprof heap.prof
# Generate visualization
go tool pprof -http=:8080 cpu.prof
Measuring Request Latency
Track operation duration:
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
type LatencyTracker struct {
Latencies []time.Duration
}
func (t *LatencyTracker) Track(duration time.Duration) {
t.Latencies = append(t.Latencies, duration)
}
func (t *LatencyTracker) Report() {
if len(t.Latencies) == 0 {
return
}
var total time.Duration
min := t.Latencies[0]
max := t.Latencies[0]
for _, d := range t.Latencies {
total += d
if d < min {
min = d
}
if d > max {
max = d
}
}
avg := total / time.Duration(len(t.Latencies))
fmt.Println("=== Latency Report ===")
fmt.Printf("Requests: %d\n", len(t.Latencies))
fmt.Printf("Min: %v\n", min)
fmt.Printf("Max: %v\n", max)
fmt.Printf("Avg: %v\n", avg)
}
func main() {
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
tracker := &LatencyTracker{}
for i := 0; i < 5; i++ {
start := time.Now()
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Hello!",
})
duration := time.Since(start)
tracker.Track(duration)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Request %d: %v - %s\n", i+1, duration, result.Text)
}
tracker.Report()
}
Testing Utilities
Mocking AI Responses
Create mock providers for testing:
package main
import (
"context"
"testing"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider"
"github.com/digitallysavvy/go-ai/pkg/provider/types"
)
// MockLanguageModel implements provider.LanguageModel
type MockLanguageModel struct {
Response string
Error error
}
func int64Ptr(v int64) *int64 {
return &v
}
func (m *MockLanguageModel) SpecificationVersion() string { return "v3" }
func (m *MockLanguageModel) Provider() string { return "mock" }
func (m *MockLanguageModel) ModelID() string { return "mock-model" }
func (m *MockLanguageModel) SupportsTools() bool { return false }
func (m *MockLanguageModel) SupportsStructuredOutput() bool { return false }
func (m *MockLanguageModel) SupportsImageInput() bool { return false }
func (m *MockLanguageModel) DoGenerate(ctx context.Context, opts *provider.GenerateOptions) (*types.GenerateResult, error) {
if m.Error != nil {
return nil, m.Error
}
return &types.GenerateResult{
Text: m.Response,
FinishReason: types.FinishReasonStop,
Usage: types.Usage{
InputTokens: int64Ptr(10),
OutputTokens: int64Ptr(20),
TotalTokens: int64Ptr(30),
},
}, nil
}
func (m *MockLanguageModel) DoStream(ctx context.Context, opts *provider.GenerateOptions) (provider.TextStream, error) {
return nil, nil // Implement if needed
}
// Example test
func TestGenerateText(t *testing.T) {
ctx := context.Background()
mock := &MockLanguageModel{
Response: "This is a mocked response",
}
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: mock,
Prompt: "Test prompt",
})
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Text != "This is a mocked response" {
t.Errorf("Expected 'This is a mocked response', got '%s'", result.Text)
}
if result.Usage.GetTotalTokens() != 30 {
t.Errorf("Expected 30 total tokens, got %d", result.Usage.GetTotalTokens())
}
}
Integration Testing
Test with real providers in controlled environments:
package main
import (
"context"
"os"
"testing"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func TestOpenAIIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
t.Skip("OPENAI_API_KEY not set")
}
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: apiKey,
})
model, err := provider.LanguageModel("gpt-3.5-turbo")
if err != nil {
t.Fatal(err)
}
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Say 'test'",
})
if err != nil {
t.Fatalf("GenerateText failed: %v", err)
}
if result.Text == "" {
t.Error("Expected non-empty response")
}
if result.Usage.GetTotalTokens() == 0 {
t.Error("Expected non-zero token usage")
}
}
Run integration tests:
# Run all tests including integration tests
go test ./...
# Skip integration tests (faster)
go test -short ./...
# Run with verbose output
go test -v ./...
Snapshot Testing
Test output consistency:
package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider"
"github.com/digitallysavvy/go-ai/pkg/provider/types"
)
// MockLanguageModel implements provider.LanguageModel (same definition as
// shown earlier under "Mocking AI Responses").
type MockLanguageModel struct {
Response string
Error error
}
func (m *MockLanguageModel) SpecificationVersion() string { return "v3" }
func (m *MockLanguageModel) Provider() string { return "mock" }
func (m *MockLanguageModel) ModelID() string { return "mock-model" }
func (m *MockLanguageModel) SupportsTools() bool { return false }
func (m *MockLanguageModel) SupportsStructuredOutput() bool { return false }
func (m *MockLanguageModel) SupportsImageInput() bool { return false }
func (m *MockLanguageModel) DoGenerate(ctx context.Context, opts *provider.GenerateOptions) (*types.GenerateResult, error) {
if m.Error != nil {
return nil, m.Error
}
return &types.GenerateResult{
Text: m.Response,
FinishReason: types.FinishReasonStop,
}, nil
}
func (m *MockLanguageModel) DoStream(ctx context.Context, opts *provider.GenerateOptions) (provider.TextStream, error) {
return nil, nil
}
func TestGenerateTextSnapshot(t *testing.T) {
ctx := context.Background()
mock := &MockLanguageModel{
Response: "Consistent response for snapshot",
}
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: mock,
Prompt: "Test prompt",
})
if err != nil {
t.Fatal(err)
}
// Create snapshot
snapshotPath := filepath.Join("testdata", "snapshots", "generate_text.json")
data, _ := json.MarshalIndent(result, "", " ")
// Check if snapshot exists
if _, err := os.Stat(snapshotPath); os.IsNotExist(err) {
// Create snapshot
os.MkdirAll(filepath.Dir(snapshotPath), 0755)
os.WriteFile(snapshotPath, data, 0644)
t.Log("Created new snapshot")
return
}
// Compare with snapshot
expected, _ := os.ReadFile(snapshotPath)
if string(data) != string(expected) {
t.Errorf("Snapshot mismatch.\nExpected:\n%s\n\nGot:\n%s", expected, data)
}
}
Observability
Structured Logging
Use structured logging for better observability:
package main
import (
"context"
"log/slog"
"os"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func main() {
// Set up structured logger
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
logger.Info("Starting AI request",
"model", "gpt-4",
"prompt", "Hello!",
)
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Hello!",
})
if err != nil {
logger.Error("AI request failed",
"error", err,
)
return
}
logger.Info("AI request completed",
"model", result.Response.ModelID,
"finish_reason", result.FinishReason,
"prompt_tokens", result.Usage.GetInputTokens(),
"completion_tokens", result.Usage.GetOutputTokens(),
"total_tokens", result.Usage.GetTotalTokens(),
)
}
Tracing with OpenTelemetry
Add distributed tracing:
package main
import (
"context"
"log"
"os"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func generateWithTracing(ctx context.Context, model provider.LanguageModel, prompt string) (*ai.GenerateTextResult, error) {
tracer := otel.Tracer("go-ai-app")
ctx, span := tracer.Start(ctx, "ai.GenerateText")
defer span.End()
span.SetAttributes(
attribute.String("ai.model", "gpt-4"),
attribute.String("ai.prompt", prompt),
)
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: prompt,
})
if err != nil {
span.RecordError(err)
return nil, err
}
span.SetAttributes(
attribute.Int64("ai.tokens.prompt", result.Usage.GetInputTokens()),
attribute.Int64("ai.tokens.completion", result.Usage.GetOutputTokens()),
attribute.Int64("ai.tokens.total", result.Usage.GetTotalTokens()),
attribute.String("ai.finish_reason", string(result.FinishReason)),
)
return result, nil
}
func main() {
ctx := context.Background()
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
result, err := generateWithTracing(ctx, model, "Hello!")
if err != nil {
log.Fatal(err)
}
log.Printf("Result: %s", result.Text)
}
Debugging Common Issues
Rate Limiting
Debug rate limit errors:
if err != nil {
if strings.Contains(err.Error(), "rate_limit") {
log.Printf("Rate limit hit: %v", err)
log.Printf("Retry after delay...")
time.Sleep(60 * time.Second)
// Retry logic
}
}
Context Cancellation
Debug timeout issues:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: prompt,
})
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("Request timed out after 30 seconds")
} else {
log.Printf("Request failed: %v", err)
}
}
Tool Execution Errors
Debug tool calling issues:
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Tools: tools,
Prompt: "Use the weather tool",
OnStepFinish: func(ctx context.Context, step types.StepResult, userContext interface{}) {
log.Printf("Step %d:", step.StepNumber)
for _, call := range step.ToolCalls {
log.Printf(" Tool: %s", call.ToolName)
log.Printf(" Args: %+v", call.Arguments)
}
for _, res := range step.ToolResults {
if res.Error != nil {
log.Printf(" Error: %v", res.Error)
} else {
log.Printf(" Result: %+v", res.Result)
}
}
},
})
Best Practices
1. Use Debug Mode in Development
// ✅ Good: Debug mode in development
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
DebugMode: os.Getenv("ENV") == "development",
})
2. Monitor Token Usage
// ✅ Good: Track and log token usage
if result.Usage.GetTotalTokens() > 10000 {
log.Printf("WARNING: High token usage: %d", result.Usage.GetTotalTokens())
}
3. Use Structured Logging
// ✅ Good: Structured logging
logger.Info("AI request",
"model", "gpt-4",
"tokens", result.Usage.GetTotalTokens(),
"duration", duration,
)
4. Test with Mocks
// ✅ Good: Use mocks for unit tests
mock := &MockLanguageModel{Response: "test"}
result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: mock,
})
5. Profile Production Issues
// ✅ Good: Enable profiling in production
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
Tools Summary
| Tool | Purpose | Use Case |
|---|---|---|
| Debug Mode | Log HTTP requests/responses | Development debugging |
| Token Tracking | Monitor API costs | Cost optimization |
| pprof | CPU/memory profiling | Performance tuning |
| Latency Tracking | Measure request duration | Performance monitoring |
| Mock Providers | Unit testing | Test automation |
| Structured Logging | Observability | Production monitoring |
| OpenTelemetry | Distributed tracing | Microservices debugging |