···33import (
44 "context"
55 "fmt"
66- "log"
66+ "log/slog"
77 "net/http"
88 "os"
99 "os/signal"
···1313 "atcr.io/pkg/hold"
1414 "atcr.io/pkg/hold/oci"
1515 "atcr.io/pkg/hold/pds"
1616+ "atcr.io/pkg/logging"
1617 "atcr.io/pkg/s3"
17181819 // Import storage drivers
···2829 // Load configuration from environment variables
2930 cfg, err := hold.LoadConfigFromEnv()
3031 if err != nil {
3131- log.Fatalf("Failed to load config: %v", err)
3232+ slog.Error("Failed to load config", "error", err)
3333+ os.Exit(1)
3234 }
33353636+ // Initialize structured logging
3737+ logging.InitLogger(cfg.LogLevel)
3838+3439 // Initialize embedded PDS if database path is configured
3540 // This must happen before creating HoldService since service needs PDS for authorization
3641 var holdPDS *pds.HoldPDS
···3944 if cfg.Database.Path != "" {
4045 // Generate did:web from public URL
4146 holdDID := pds.GenerateDIDFromURL(cfg.Server.PublicURL)
4242- log.Printf("Initializing embedded PDS with DID: %s", holdDID)
4747+ slog.Info("Initializing embedded PDS", "did", holdDID)
43484449 // Initialize PDS with carstore and keys
4550 ctx := context.Background()
4651 holdPDS, err = pds.NewHoldPDS(ctx, holdDID, cfg.Server.PublicURL, cfg.Database.Path, cfg.Database.KeyPath, cfg.Registration.EnableBlueskyPosts)
4752 if err != nil {
4848- log.Fatalf("Failed to initialize embedded PDS: %v", err)
5353+ slog.Error("Failed to initialize embedded PDS", "error", err)
5454+ os.Exit(1)
4955 }
50565157 // Create storage driver from config (needed for bootstrap profile avatar)
5258 driver, err := factory.Create(ctx, cfg.Storage.Type(), cfg.Storage.Parameters())
5359 if err != nil {
5454- log.Fatalf("failed to create storage driver: %v", err)
5555- return
6060+ slog.Error("Failed to create storage driver", "error", err)
6161+ os.Exit(1)
5662 }
57635864 // Bootstrap PDS with captain record, hold owner as first crew member, and profile
5965 if err := holdPDS.Bootstrap(ctx, driver, cfg.Registration.OwnerDID, cfg.Server.Public, cfg.Registration.AllowAllCrew, cfg.Registration.ProfileAvatarURL); err != nil {
6060- log.Fatalf("Failed to bootstrap PDS: %v", err)
6666+ slog.Error("Failed to bootstrap PDS", "error", err)
6767+ os.Exit(1)
6168 }
62696370 // Create event broadcaster for subscribeRepos firehose
···72797380 // Bootstrap events from existing repo records (one-time migration)
7481 if err := broadcaster.BootstrapFromRepo(holdPDS); err != nil {
7575- log.Printf("Warning: Failed to bootstrap events from repo: %v", err)
8282+ slog.Warn("Failed to bootstrap events from repo", "error", err)
7683 }
77847885 // Wire up repo event handler to broadcaster
7986 holdPDS.RepomgrRef().SetEventHandler(broadcaster.SetRepoEventHandler(), true)
80878181- log.Printf("Embedded PDS initialized successfully with firehose enabled")
8888+ slog.Info("Embedded PDS initialized successfully with firehose enabled")
8289 } else {
8383- log.Fatalf("Database path is required for embedded PDS authorization")
9090+ slog.Error("Database path is required for embedded PDS authorization")
9191+ os.Exit(1)
8492 }
85938694 // Create blob store adapter and XRPC handlers
···9098 ctx := context.Background()
9199 driver, err := factory.Create(ctx, cfg.Storage.Type(), cfg.Storage.Parameters())
92100 if err != nil {
9393- log.Fatalf("failed to create storage driver: %v", err)
9494- return
101101+ slog.Error("Failed to create storage driver", "error", err)
102102+ os.Exit(1)
95103 }
9610497105 s3Service, err := s3.NewS3Service(cfg.Storage.Parameters(), cfg.Server.DisablePresignedURLs, cfg.Storage.Type())
98106 if err != nil {
9999- log.Fatalf("Failed to create s3 service: %v", err)
107107+ slog.Error("Failed to create S3 service", "error", err)
108108+ os.Exit(1)
100109 }
101110102111 // Create PDS XRPC handler (ATProto endpoints)
···128137129138 // Register XRPC/ATProto PDS endpoints if PDS is initialized
130139 if xrpcHandler != nil {
131131- log.Printf("Registering ATProto PDS endpoints")
140140+ slog.Info("Registering ATProto PDS endpoints")
132141 xrpcHandler.RegisterHandlers(r)
133142 }
134143135144 // Register OCI multipart upload endpoints
136145 if ociHandler != nil {
137137- log.Printf("Registering OCI multipart upload endpoints")
146146+ slog.Info("Registering OCI multipart upload endpoints")
138147 ociHandler.RegisterHandlers(r)
139148 }
140149···153162 // Start server in goroutine
154163 serverErr := make(chan error, 1)
155164 go func() {
156156- log.Printf("Starting hold service on %s", cfg.Server.Addr)
165165+ slog.Info("Starting hold service", "addr", cfg.Server.Addr)
157166 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
158167 serverErr <- err
159168 }
···164173 ctx := context.Background()
165174166175 if err := holdPDS.SetStatus(ctx, "online"); err != nil {
167167- log.Printf("Warning: Failed to set status post to online: %v", err)
176176+ slog.Warn("Failed to set status post to online", "error", err)
168177 } else {
169169- log.Printf("Status post set to online")
178178+ slog.Info("Status post set to online")
170179 }
171180 }
172181173182 // Wait for signal or server error
174183 select {
175184 case err := <-serverErr:
176176- log.Fatalf("Server failed: %v", err)
185185+ slog.Error("Server failed", "error", err)
186186+ os.Exit(1)
177187 case sig := <-sigChan:
178178- log.Printf("Received signal %v, shutting down gracefully...", sig)
188188+ slog.Info("Received signal, shutting down gracefully", "signal", sig)
179189180190 // Update status post to "offline" before shutdown
181191 if holdPDS != nil {
182192 ctx := context.Background()
183193 if err := holdPDS.SetStatus(ctx, "offline"); err != nil {
184184- log.Printf("Warning: Failed to set status post to offline: %v", err)
194194+ slog.Warn("Failed to set status post to offline", "error", err)
185195 } else {
186186- log.Printf("Status post set to offline")
196196+ slog.Info("Status post set to offline")
187197 }
188198 }
189199190200 // Close broadcaster database connection
191201 if broadcaster != nil {
192202 if err := broadcaster.Close(); err != nil {
193193- log.Printf("Warning: Failed to close broadcaster database: %v", err)
203203+ slog.Warn("Failed to close broadcaster database", "error", err)
194204 } else {
195195- log.Printf("Broadcaster database closed")
205205+ slog.Info("Broadcaster database closed")
196206 }
197207 }
198208···201211 defer cancel()
202212203213 if err := server.Shutdown(shutdownCtx); err != nil {
204204- log.Printf("Server shutdown error: %v", err)
214214+ slog.Error("Server shutdown error", "error", err)
205215 } else {
206206- log.Printf("Server shutdown complete")
216216+ slog.Info("Server shutdown complete")
207217 }
208218 }
209219}
+6
pkg/appview/config.go
···240240 return defaultValue
241241}
242242243243+// GetLogLevel returns the configured log level from environment
244244+// Centralizes ATCR_LOG_LEVEL env var reading
245245+func GetLogLevel() string {
246246+ return GetEnvOrDefault("ATCR_LOG_LEVEL", "info")
247247+}
248248+243249// GetStringParam extracts a string parameter from configuration.Parameters
244250func GetStringParam(params configuration.Parameters, key, defaultValue string) string {
245251 if v, ok := params[key]; ok {
+4
pkg/hold/config.go
···1717// Config represents the hold service configuration
1818type Config struct {
1919 Version string `yaml:"version"`
2020+ LogLevel string `yaml:"log_level"`
2021 Storage StorageConfig `yaml:"storage"`
2122 Server ServerConfig `yaml:"server"`
2223 Registration RegistrationConfig `yaml:"registration"`
···8990 cfg := &Config{
9091 Version: "0.1",
9192 }
9393+9494+ // Logging configuration
9595+ cfg.LogLevel = getEnvOrDefault("ATCR_LOG_LEVEL", "info")
92969397 // Server configuration
9498 cfg.Server.Addr = getEnvOrDefault("HOLD_SERVER_ADDR", ":8080")
+58
pkg/logging/logger.go
···11+// Package logging provides centralized structured logging using slog
22+// with configurable log levels. Call InitLogger() from main() to configure.
33+package logging
44+55+import (
66+ "io"
77+ "log/slog"
88+ "os"
99+ "strings"
1010+)
1111+1212+// InitLogger initializes the global slog default logger with the specified log level.
1313+// Valid levels: debug, info, warn, error (case-insensitive)
1414+// If level is empty or invalid, defaults to INFO.
1515+// Call this from main() at startup.
1616+func InitLogger(level string) {
1717+ var logLevel slog.Level
1818+1919+ switch strings.ToLower(strings.TrimSpace(level)) {
2020+ case "debug":
2121+ logLevel = slog.LevelDebug
2222+ case "info", "":
2323+ logLevel = slog.LevelInfo
2424+ case "warn", "warning":
2525+ logLevel = slog.LevelWarn
2626+ case "error":
2727+ logLevel = slog.LevelError
2828+ default:
2929+ logLevel = slog.LevelInfo
3030+ }
3131+3232+ opts := &slog.HandlerOptions{
3333+ Level: logLevel,
3434+ }
3535+3636+ handler := slog.NewTextHandler(os.Stdout, opts)
3737+ slog.SetDefault(slog.New(handler))
3838+}
3939+4040+// SetupTestLogger configures logging for tests to reduce noise.
4141+// Sets log level to WARN and outputs to io.Discard to suppress DEBUG and INFO messages.
4242+// Returns a cleanup function that should be called when the test completes (use t.Cleanup).
4343+func SetupTestLogger() func() {
4444+ // Save original logger to restore later
4545+ originalLogger := slog.Default()
4646+4747+ // Set level to WARN and discard output to silence tests
4848+ opts := &slog.HandlerOptions{
4949+ Level: slog.LevelWarn,
5050+ }
5151+ handler := slog.NewTextHandler(io.Discard, opts)
5252+ slog.SetDefault(slog.New(handler))
5353+5454+ // Return cleanup function
5555+ return func() {
5656+ slog.SetDefault(originalLogger)
5757+ }
5858+}