A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go

Standardize Appview Logging #3

closed
opened by evan.jarrett.net

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

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 flows
  • pkg/hold/registration.go - Hold registration
  • pkg/appview/middleware/registry.go - Request routing
  • pkg/appview/storage/proxy_blob_store.go - Blob operations
  • pkg/hold/authorization.go - Authorization checks
  • cmd/appview/serve.go - Server startup
  • cmd/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/client
  • manifest_store, blob_store
  • hold/registration, hold/authorization
  • middleware/registry, middleware/auth
  • jetstream/worker, jetstream/backfill

Testing#

  • Set LOG_LEVEL=DEBUG and verify debug logs appear
  • Set LOG_LEVEL=ERROR and verify only errors appear
  • Check logs are valid JSON (if using JSON handler)
  • Verify all error conditions still log appropriately
  • Ensure no fmt.Printf debugging 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#

  1. Start with one package (e.g., pkg/auth/oauth/server.go)
  2. Convert all logs in that file
  3. Test thoroughly
  4. Move to next package
  5. 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.Println for 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
sign up or login to add to the discussion
Labels
good-first-issue
assignee

None yet.

Participants 1
AT URI
at://did:plc:pddp4xt5lgnv2qsegbzzs4xg/sh.tangled.repo.issue/3m35zqw4xvz22