Standardize Backend Logging with slog#
Goal#
Replace inconsistent fmt.Printf and log.Printf calls with structured logging using Go's built-in log/slog package.
Current State#
The codebase uses multiple logging approaches inconsistently:
fmt.Printf patterns:
fmt.Printf("WARNING: Failed to increment pull count: %v\n", err)
fmt.Printf("DEBUG [oauth/server]: Starting OAuth flow for handle=%s\n", handle)
fmt.Printf("ERROR [oauth/server]: Failed to start auth flow: %v\n", err)
log.Printf patterns:
log.Printf("ERROR: Failed to check hold public flag: %v", err)
log.Printf("✓ Hold service already registered in PDS")
log.Printf("Checking registration status for DID: %s", did)
Problems:
- No structured logging (hard to parse/query)
- Inconsistent log levels (WARNING vs Warning vs ERROR)
- Mixed prefixes (some have brackets, some don't)
- No context propagation
- Can't configure log levels dynamically
Recommended Approach#
Use Go's built-in log/slog package for structured, leveled logging.
Benefits:
- Structured key-value logging
- Standard log levels (Debug, Info, Warn, Error)
- Context-aware logging
- Easy to switch output format (JSON, text)
- Built-in, no external dependencies
Tasks#
1. Create Logger Initialization (pkg/logging/logger.go)#
Create a new package with centralized logger setup:
package logging
import (
"log/slog"
"os"
)
var Logger *slog.Logger
func init() {
// Default to JSON handler for production
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: getLogLevel(),
})
Logger = slog.New(handler)
}
func getLogLevel() slog.Level {
level := os.Getenv("LOG_LEVEL")
switch level {
case "DEBUG":
return slog.LevelDebug
case "INFO":
return slog.LevelInfo
case "WARN":
return slog.LevelWarn
case "ERROR":
return slog.LevelError
default:
return slog.LevelInfo
}
}
2. Replace fmt.Printf/log.Printf Calls#
Pattern transformations:
// Before
fmt.Printf("WARNING: Failed to increment pull count for %s/%s: %v\n", did, repo, err)
// After
logging.Logger.Warn("failed to increment pull count",
"did", did,
"repository", repo,
"error", err)
// Before
log.Printf("DEBUG [oauth/server]: Starting OAuth flow for handle=%s\n", handle)
// After
logging.Logger.Debug("starting oauth flow",
"component", "oauth/server",
"handle", handle)
// Before
fmt.Printf("ERROR [oauth/server]: Failed to start auth flow: %v\n", err)
// After
logging.Logger.Error("failed to start auth flow",
"component", "oauth/server",
"error", err)
3. Update Key Files#
Priority files (highest logging volume):
pkg/auth/oauth/server.go- OAuth flowspkg/hold/registration.go- Hold registrationpkg/appview/middleware/registry.go- Request routingpkg/appview/storage/proxy_blob_store.go- Blob operationspkg/hold/authorization.go- Authorization checkscmd/appview/serve.go- Server startupcmd/hold/main.go- Hold service startup
4. Keep Emojis in Terminal Output#
Terminal-facing output can keep emojis (registration success, etc.):
// This is fine - user-facing CLI output
fmt.Println("✓ Hold service registered successfully!")
// But log it structured too
logging.Logger.Info("hold service registered", "url", publicURL)
5. Add Component/Module Tags#
Use consistent component tags for filtering:
logging.Logger.Info("manifest stored",
"component", "manifest_store",
"did", did,
"repository", repo)
Common components:
oauth/server,oauth/clientmanifest_store,blob_storehold/registration,hold/authorizationmiddleware/registry,middleware/authjetstream/worker,jetstream/backfill
Testing#
- Set
LOG_LEVEL=DEBUGand verify debug logs appear - Set
LOG_LEVEL=ERRORand verify only errors appear - Check logs are valid JSON (if using JSON handler)
- Verify all error conditions still log appropriately
- Ensure no
fmt.Printfdebugging statements remain
Configuration#
Add to .env.appview.example and .env.hold.example:
# Logging configuration
LOG_LEVEL=INFO # DEBUG, INFO, WARN, ERROR
LOG_FORMAT=json # json or text
Migration Strategy#
- Start with one package (e.g.,
pkg/auth/oauth/server.go) - Convert all logs in that file
- Test thoroughly
- Move to next package
- Create PR when a logical chunk is complete (don't need to do everything at once)
Files to Create#
pkg/logging/logger.go- Logger initialization
Files to Modify#
All .go files with fmt.Printf or log.Printf (30 files, but prioritize high-traffic paths first)
Notes#
- Keep terminal user output separate from logs (use
fmt.Printlnfor CLI messages) - Structured logs make debugging production issues much easier
- JSON logs integrate well with log aggregation tools (CloudWatch, Datadog, etc.)
- Consider adding request IDs for tracing in future work