f388af994ec86ad17c6f69864f50e57683b7f65b

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

feat: use contexts and clean logging

Diff

This diff is truncated to protect this page.

   1diff --git a/cmd/soft/hook.go b/cmd/soft/hook.go
   2index fc78221722e4c0b5801cdc24fe30e9456786d325..c1f9d4233ab56846043584f052c1195bb235cffa 100644
   3--- a/cmd/soft/hook.go
   4+++ b/cmd/soft/hook.go
   5@@ -11,6 +11,7 @@ import (
   6 	"path/filepath"
   7 	"strings"
   8 
   9+	"github.com/charmbracelet/log"
  10 	"github.com/charmbracelet/soft-serve/server/backend"
  11 	"github.com/charmbracelet/soft-serve/server/backend/sqlite"
  12 	"github.com/charmbracelet/soft-serve/server/config"
  13@@ -18,53 +19,57 @@ import (
  14 	"github.com/spf13/cobra"
  15 )
  16 
  17-var (
  18-	confixCtxKey  = "config"
  19-	backendCtxKey = "backend"
  20-)
  21-
  22 var (
  23 	configPath string
  24 
  25+	logFileCtxKey = struct{}{}
  26+
  27 	hookCmd = &cobra.Command{
  28 		Use:    "hook",
  29 		Short:  "Run git server hooks",
  30 		Long:   "Handles Soft Serve git server hooks.",
  31 		Hidden: true,
  32 		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
  33+			ctx := cmd.Context()
  34 			cfg, err := config.ParseConfig(configPath)
  35 			if err != nil {
  36 				return fmt.Errorf("could not parse config: %w", err)
  37 			}
  38 
  39-			customHooksPath := filepath.Join(filepath.Dir(configPath), "hooks")
  40-			if _, err := os.Stat(customHooksPath); err != nil && os.IsNotExist(err) {
  41-				os.MkdirAll(customHooksPath, os.ModePerm)
  42-				// Generate update hook example without executable permissions
  43-				hookPath := filepath.Join(customHooksPath, "update.sample")
  44-				if err := os.WriteFile(hookPath, []byte(updateHookExample), 0744); err != nil {
  45-					return fmt.Errorf("failed to generate update hook example: %w", err)
  46-				}
  47+			ctx = config.WithContext(ctx, cfg)
  48+
  49+			logPath := filepath.Join(cfg.DataPath, "log", "hooks.log")
  50+			f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
  51+			if err != nil {
  52+				return fmt.Errorf("opening file: %w", err)
  53 			}
  54 
  55+			ctx = context.WithValue(ctx, logFileCtxKey, f)
  56+			logger := log.FromContext(ctx)
  57+			logger.SetOutput(f)
  58+			ctx = log.WithContext(ctx, logger)
  59+			cmd.SetContext(ctx)
  60+
  61 			// Set up the backend
  62 			// TODO: support other backends
  63-			sb, err := sqlite.NewSqliteBackend(cmd.Context(), cfg)
  64+			sb, err := sqlite.NewSqliteBackend(ctx)
  65 			if err != nil {
  66 				return fmt.Errorf("failed to create sqlite backend: %w", err)
  67 			}
  68 
  69 			cfg = cfg.WithBackend(sb)
  70 
  71-			cmd.SetContext(context.WithValue(cmd.Context(), confixCtxKey, cfg))
  72-			cmd.SetContext(context.WithValue(cmd.Context(), backendCtxKey, sb))
  73-
  74 			return nil
  75 		},
  76+		PersistentPostRunE: func(cmd *cobra.Command, _ []string) error {
  77+			f := cmd.Context().Value(logFileCtxKey).(*os.File)
  78+			return f.Close()
  79+		},
  80 	}
  81 
  82 	hooksRunE = func(cmd *cobra.Command, args []string) error {
  83-		cfg := cmd.Context().Value(confixCtxKey).(*config.Config)
  84+		ctx := cmd.Context()
  85+		cfg := config.FromContext(ctx)
  86 		hks := cfg.Backend.(backend.Hooks)
  87 
  88 		// This is set in the server before invoking git-receive-pack/git-upload-pack
  89@@ -119,7 +124,7 @@ var (
  90 		// Custom hooks
  91 		if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {
  92 			// If the custom hook is executable, run it
  93-			if err := runCommand(cmd.Context(), &buf, stdout, stderr, customHookPath, args...); err != nil {
  94+			if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {
  95 				return fmt.Errorf("failed to run custom hook: %w", err)
  96 			}
  97 		}
  98diff --git a/cmd/soft/migrate_config.go b/cmd/soft/migrate_config.go
  99index a20cc9d41a51d71aaac9b4433150b52986892177..888c722aa44d82bd9e37bb9fd67510d685534ddc 100644
 100--- a/cmd/soft/migrate_config.go
 101+++ b/cmd/soft/migrate_config.go
 102@@ -2,6 +2,7 @@ package main
 103 
 104 import (
 105 	"encoding/json"
 106+	"errors"
 107 	"fmt"
 108 	"io"
 109 	"os"
 110@@ -27,15 +28,18 @@ var (
 111 		Short:  "Migrate config to new format",
 112 		Hidden: true,
 113 		RunE: func(cmd *cobra.Command, _ []string) error {
 114+			ctx := cmd.Context()
 115+
 116+			logger := log.FromContext(ctx)
 117 			// Disable logging timestamp
 118-			log.SetReportTimestamp(false)
 119+			logger.SetReportTimestamp(false)
 120 
 121 			keyPath := os.Getenv("SOFT_SERVE_KEY_PATH")
 122 			reposPath := os.Getenv("SOFT_SERVE_REPO_PATH")
 123 			bindAddr := os.Getenv("SOFT_SERVE_BIND_ADDRESS")
 124-			ctx := cmd.Context()
 125 			cfg := config.DefaultConfig()
 126-			sb, err := sqlite.NewSqliteBackend(ctx, cfg)
 127+			ctx = config.WithContext(ctx, cfg)
 128+			sb, err := sqlite.NewSqliteBackend(ctx)
 129 			if err != nil {
 130 				return fmt.Errorf("failed to create sqlite backend: %w", err)
 131 			}
 132@@ -43,13 +47,13 @@ var (
 133 			cfg = cfg.WithBackend(sb)
 134 
 135 			// Set SSH listen address
 136-			log.Info("Setting SSH listen address...")
 137+			logger.Info("Setting SSH listen address...")
 138 			if bindAddr != "" {
 139 				cfg.SSH.ListenAddr = bindAddr
 140 			}
 141 
 142 			// Copy SSH host key
 143-			log.Info("Copying SSH host key...")
 144+			logger.Info("Copying SSH host key...")
 145 			if keyPath != "" {
 146 				if err := os.MkdirAll(filepath.Join(cfg.DataPath, "ssh"), os.ModePerm); err != nil {
 147 					return fmt.Errorf("failed to create ssh directory: %w", err)
 148@@ -60,14 +64,14 @@ var (
 149 				}
 150 
 151 				if err := copyFile(keyPath+".pub", filepath.Join(cfg.DataPath, "ssh", filepath.Base(keyPath))+".pub"); err != nil {
 152-					log.Errorf("failed to copy ssh key: %s", err)
 153+					logger.Errorf("failed to copy ssh key: %s", err)
 154 				}
 155 
 156 				cfg.SSH.KeyPath = filepath.Join(cfg.DataPath, "ssh", filepath.Base(keyPath))
 157 			}
 158 
 159 			// Read config
 160-			log.Info("Reading config repository...")
 161+			logger.Info("Reading config repository...")
 162 			r, err := git.Open(filepath.Join(reposPath, "config"))
 163 			if err != nil {
 164 				return fmt.Errorf("failed to open config repo: %w", err)
 165@@ -119,7 +123,7 @@ var (
 166 			cfg.SSH.PublicURL = fmt.Sprintf("ssh://%s:%d", ocfg.Host, ocfg.Port)
 167 
 168 			// Set server settings
 169-			log.Info("Setting server settings...")
 170+			logger.Info("Setting server settings...")
 171 			if cfg.Backend.SetAllowKeyless(ocfg.AllowKeyless) != nil {
 172 				fmt.Fprintf(os.Stderr, "failed to set allow keyless\n")
 173 			}
 174@@ -132,7 +136,7 @@ var (
 175 
 176 			// Copy repos
 177 			if reposPath != "" {
 178-				log.Info("Copying repos...")
 179+				logger.Info("Copying repos...")
 180 				if err := os.MkdirAll(filepath.Join(cfg.DataPath, "repos"), os.ModePerm); err != nil {
 181 					return fmt.Errorf("failed to create repos directory: %w", err)
 182 				}
 183@@ -151,7 +155,7 @@ var (
 184 						continue
 185 					}
 186 
 187-					log.Infof("  Copying repo %s", dir.Name())
 188+					logger.Infof("  Copying repo %s", dir.Name())
 189 					src := filepath.Join(reposPath, utils.SanitizeRepo(dir.Name()))
 190 					dst := filepath.Join(cfg.DataPath, "repos", utils.SanitizeRepo(dir.Name())) + ".git"
 191 					if err := os.MkdirAll(dst, os.ModePerm); err != nil {
 192@@ -168,7 +172,7 @@ var (
 193 				}
 194 
 195 				if hasReadme {
 196-					log.Infof("  Copying readme from \"config\" to \".soft-serve\"")
 197+					logger.Infof("  Copying readme from \"config\" to \".soft-serve\"")
 198 
 199 					// Switch to main branch
 200 					bcmd := git.NewCommand("branch", "-M", "main")
 201@@ -236,7 +240,7 @@ var (
 202diff --git a/cmd/soft/root.go b/cmd/soft/root.go
 203index 44b6e8ab84f9631fa0fcfd069a90e17b60183f86..b47e9cdd04bf8393c4b537e1599d0ed2b47e3dcc 100644
 204--- a/cmd/soft/root.go
 205+++ b/cmd/soft/root.go
 206@@ -4,9 +4,11 @@ import (
 207 	"context"
 208 	"os"
 209 	"runtime/debug"
 210+	"strconv"
 211+	"strings"
 212+	"time"
 213 
 214 	"github.com/charmbracelet/log"
 215-	_ "github.com/charmbracelet/soft-serve/log"
 216 	"github.com/spf13/cobra"
 217 )
 218 
 219@@ -51,15 +53,24 @@ func init() {
 220 }
 221 
 222 func main() {
 223+	ctx := context.Background()
 224 	logger := log.NewWithOptions(os.Stderr, log.Options{
 225 		ReportTimestamp: true,
 226-		TimeFormat:      "2006-01-02",
 227+		TimeFormat:      time.DateOnly,
 228 	})
 229-	if os.Getenv("SOFT_SERVE_DEBUG") == "true" {
 230+	if debug, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_DEBUG")); debug {
 231 		logger.SetLevel(log.DebugLevel)
 232 	}
 233 
 234-	ctx := context.Background()
 235+	switch strings.ToLower(os.Getenv("SOFT_SERVE_LOG_FORMAT")) {
 236+	case "json":
 237+		logger.SetFormatter(log.JSONFormatter)
 238+	case "logfmt":
 239+		logger.SetFormatter(log.LogfmtFormatter)
 240+	case "text":
 241+		logger.SetFormatter(log.TextFormatter)
 242+	}
 243+
 244 	ctx = log.WithContext(ctx, logger)
 245 	if err := rootCmd.ExecuteContext(ctx); err != nil {
 246 		os.Exit(1)
 247diff --git a/cmd/soft/serve.go b/cmd/soft/serve.go
 248index 55e0d2aca2c24c381d795c754149ad269edd5f83..b9a9b2937a1be656acbe38b32325161cee61c572 100644
 249--- a/cmd/soft/serve.go
 250+++ b/cmd/soft/serve.go
 251@@ -5,10 +5,10 @@ import (
 252 	"fmt"
 253 	"os"
 254 	"os/signal"
 255+	"path/filepath"
 256 	"syscall"
 257 	"time"
 258 
 259-	_ "github.com/charmbracelet/soft-serve/log"
 260 	"github.com/charmbracelet/soft-serve/server"
 261 	"github.com/charmbracelet/soft-serve/server/config"
 262 	"github.com/spf13/cobra"
 263@@ -20,10 +20,31 @@ var (
 264 		Short: "Start the server",
 265 		Long:  "Start the server",
 266 		Args:  cobra.NoArgs,
 267-		RunE: func(cmd *cobra.Command, args []string) error {
 268+		RunE: func(cmd *cobra.Command, _ []string) error {
 269 			ctx := cmd.Context()
 270 			cfg := config.DefaultConfig()
 271-			s, err := server.NewServer(ctx, cfg)
 272+			ctx = config.WithContext(ctx, cfg)
 273+			cmd.SetContext(ctx)
 274+
 275+			// Create custom hooks directory if it doesn't exist
 276+			customHooksPath := filepath.Join(cfg.DataPath, "hooks")
 277+			if _, err := os.Stat(customHooksPath); err != nil && os.IsNotExist(err) {
 278+				os.MkdirAll(customHooksPath, os.ModePerm) // nolint: errcheck
 279+				// Generate update hook example without executable permissions
 280+				hookPath := filepath.Join(customHooksPath, "update.sample")
 281+				// nolint: gosec
 282+				if err := os.WriteFile(hookPath, []byte(updateHookExample), 0744); err != nil {
 283+					return fmt.Errorf("failed to generate update hook example: %w", err)
 284+				}
 285+			}
 286+
 287+			// Create log directory if it doesn't exist
 288+			logPath := filepath.Join(cfg.DataPath, "log")
 289+			if _, err := os.Stat(logPath); err != nil && os.IsNotExist(err) {
 290+				os.MkdirAll(logPath, os.ModePerm) // nolint: errcheck
 291+			}
 292+
 293+			s, err := server.NewServer(ctx)
 294 			if err != nil {
 295 				return fmt.Errorf("start server: %w", err)
 296 			}
 297diff --git a/examples/setuid/main.go b/examples/setuid/main.go
 298index e9cc1f751a431c961e36d050fa62f809479bd8c1..4b6c4770d5d6d2a759b1dd291bc67faf655faa4d 100644
 299--- a/examples/setuid/main.go
 300+++ b/examples/setuid/main.go
 301@@ -46,8 +46,9 @@ func main() {
 302 	}
 303 	ctx := context.Background()
 304 	cfg := config.DefaultConfig()
 305+	ctx = config.WithContext(ctx, cfg)
 306 	cfg.SSH.ListenAddr = fmt.Sprintf(":%d", *port)
 307-	s, err := server.NewServer(ctx, cfg)
 308+	s, err := server.NewServer(ctx)
 309 	if err != nil {
 310 		log.Fatal(err)
 311 	}
 312diff --git a/log/log.go b/log/log.go
 313deleted file mode 100644
 314index d2df5bbb46ab00b34f37bceb8c52a55f3a1e4269..0000000000000000000000000000000000000000
 315--- a/log/log.go
 316+++ /dev/null
 317@@ -1,14 +0,0 @@
 318-// Package log initializes the logger for Soft Serve modules.
 319-package log
 320-
 321-import (
 322-	"os"
 323-
 324-	"github.com/charmbracelet/log"
 325-)
 326-
 327-func init() {
 328-	if os.Getenv("SOFT_SERVE_DEBUG") == "true" {
 329-		log.SetLevel(log.DebugLevel)
 330-	}
 331-}
 332diff --git a/server/backend/sqlite/db.go b/server/backend/sqlite/db.go
 333index 839d9105db96bc4a624cca5c1dca357758fd5a8a..fac0394f4309af33f7c26cc84dcb94904013f512 100644
 334--- a/server/backend/sqlite/db.go
 335+++ b/server/backend/sqlite/db.go
 336@@ -66,7 +66,7 @@ func (d *SqliteBackend) init() error {
 337 			for _, k := range d.cfg.InitialAdminKeys {
 338 				pk, _, err := backend.ParseAuthorizedKey(k)
 339 				if err != nil {
 340-					logger.Error("error parsing initial admin key, skipping", "key", k, "err", err)
 341+					d.logger.Error("error parsing initial admin key, skipping", "key", k, "err", err)
 342 					continue
 343 				}
 344 
 345diff --git a/server/backend/sqlite/hooks.go b/server/backend/sqlite/hooks.go
 346index cf82c2c009a3b72415d0ba09b72152521eabe823..0f9d61658d0b061bc37c14e807c614444f5cc6c7 100644
 347--- a/server/backend/sqlite/hooks.go
 348+++ b/server/backend/sqlite/hooks.go
 349@@ -4,7 +4,6 @@ import (
 350 	"io"
 351 	"sync"
 352 
 353-	"github.com/charmbracelet/log"
 354 	"github.com/charmbracelet/soft-serve/server/backend"
 355 )
 356 
 357@@ -12,28 +11,28 @@ import (
 358 //
 359 // It implements Hooks.
 360 func (d *SqliteBackend) PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []backend.HookArg) {
 361-	log.WithPrefix("backend.sqlite.hooks").Debug("post-receive hook called", "repo", repo, "args", args)
 362+	d.logger.Debug("post-receive hook called", "repo", repo, "args", args)
 363 }
 364 
 365 // PreReceive is called by the git pre-receive hook.
 366 //
 367 // It implements Hooks.
 368 func (d *SqliteBackend) PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []backend.HookArg) {
 369-	log.WithPrefix("backend.sqlite.hooks").Debug("pre-receive hook called", "repo", repo, "args", args)
 370+	d.logger.Debug("pre-receive hook called", "repo", repo, "args", args)
 371 }
 372 
 373 // Update is called by the git update hook.
 374 //
 375 // It implements Hooks.
 376 func (d *SqliteBackend) Update(stdout io.Writer, stderr io.Writer, repo string, arg backend.HookArg) {
 377-	log.WithPrefix("backend.sqlite.hooks").Debug("update hook called", "repo", repo, "arg", arg)
 378+	d.logger.Debug("update hook called", "repo", repo, "arg", arg)
 379 }
 380 
 381 // PostUpdate is called by the git post-update hook.
 382 //
 383 // It implements Hooks.
 384 func (d *SqliteBackend) PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string) {
 385-	log.WithPrefix("backend.sqlite.hooks").Debug("post-update hook called", "repo", repo, "args", args)
 386+	d.logger.Debug("post-update hook called", "repo", repo, "args", args)
 387 
 388 	var wg sync.WaitGroup
 389 
 390@@ -44,18 +43,18 @@ func (d *SqliteBackend) PostUpdate(stdout io.Writer, stderr io.Writer, repo stri
 391 
 392 		rr, err := d.Repository(repo)
 393 		if err != nil {
 394-			log.WithPrefix("backend.sqlite.hooks").Error("error getting repository", "repo", repo, "err", err)
 395+			d.logger.Error("error getting repository", "repo", repo, "err", err)
 396 			return
 397 		}
 398 
 399 		r, err := rr.Open()
 400 		if err != nil {
 401-			log.WithPrefix("backend.sqlite.hooks").Error("error opening repository", "repo", repo, "err", err)
 402+			d.logger.Error("error opening repository", "repo", repo, "err", err)
 403 			return
 404 		}
 405 
 406 		if err := r.UpdateServerInfo(); err != nil {
 407-			log.WithPrefix("backend.sqlite.hooks").Error("error updating server-info", "repo", repo, "err", err)
 408+			d.logger.Error("error updating server-info", "repo", repo, "err", err)
 409 			return
 410 		}
 411 	}()
 412diff --git a/server/backend/sqlite/sqlite.go b/server/backend/sqlite/sqlite.go
 413index 0720a09710bed98a2a3062482740ae3ddaafa050..8b0a0c98a359e9282e614cabc66dceb76e9a9f6f 100644
 414--- a/server/backend/sqlite/sqlite.go
 415+++ b/server/backend/sqlite/sqlite.go
 416@@ -17,17 +17,14 @@ import (
 417 	_ "modernc.org/sqlite"
 418 )
 419 
 420-var (
 421-	logger = log.WithPrefix("backend.sqlite")
 422-)
 423-
 424 // SqliteBackend is a backend that uses a SQLite database as a Soft Serve
 425 // backend.
 426 type SqliteBackend struct {
 427-	cfg *config.Config
 428-	ctx context.Context
 429-	dp  string
 430-	db  *sqlx.DB
 431+	cfg    *config.Config
 432+	ctx    context.Context
 433+	dp     string
 434+	db     *sqlx.DB
 435+	logger *log.Logger
 436 }
 437 
 438 var _ backend.Backend = (*SqliteBackend)(nil)
 439@@ -37,7 +34,8 @@ func (d *SqliteBackend) reposPath() string {
 440 }
 441 
 442 // NewSqliteBackend creates a new SqliteBackend.
 443-func NewSqliteBackend(ctx context.Context, cfg *config.Config) (*SqliteBackend, error) {
 444+func NewSqliteBackend(ctx context.Context) (*SqliteBackend, error) {
 445+	cfg := config.FromContext(ctx)
 446 	dataPath := cfg.DataPath
 447 	if err := os.MkdirAll(dataPath, os.ModePerm); err != nil {
 448 		return nil, err
 449@@ -50,10 +48,11 @@ func NewSqliteBackend(ctx context.Context, cfg *config.Config) (*SqliteBackend,
 450 	}
 451 
 452 	d := &SqliteBackend{
 453-		cfg: cfg,
 454-		ctx: ctx,
 455-		dp:  dataPath,
 456-		db:  db,
 457+		cfg:    cfg,
 458+		ctx:    ctx,
 459+		dp:     dataPath,
 460+		db:     db,
 461+		logger: log.FromContext(ctx).WithPrefix("sqlite"),
 462 	}
 463 
 464 	if err := d.init(); err != nil {
 465@@ -137,13 +136,13 @@ func (d *SqliteBackend) CreateRepository(name string, opts backend.RepositoryOpt
 466 
 467 	rr, err := git.Init(rp, true)
 468 	if err != nil {
 469-		logger.Debug("failed to create repository", "err", err)
 470+		d.logger.Debug("failed to create repository", "err", err)
 471 		cleanup() // nolint: errcheck
 472 		return nil, err
 473 	}
 474 
 475 	if err := rr.UpdateServerInfo(); err != nil {
 476-		logger.Debug("failed to update server info", "err", err)
 477+		d.logger.Debug("failed to update server info", "err", err)
 478 		cleanup() // nolint: errcheck
 479 		return nil, err
 480 	}
 481@@ -154,7 +153,7 @@ func (d *SqliteBackend) CreateRepository(name string, opts backend.RepositoryOpt
 482 			name, opts.ProjectName, opts.Description, opts.Private, opts.Mirror, opts.Hidden)
 483 		return err
 484 	}); err != nil {
 485-		logger.Debug("failed to create repository in database", "err", err)
 486+		d.logger.Debug("failed to create repository in database", "err", err)
 487 		return nil, wrapDbErr(err)
 488 	}
 489 
 490@@ -192,7 +191,7 @@ func (d *SqliteBackend) ImportRepository(name string, remote string, opts backen
 491 	}
 492 
 493 	if err := git.Clone(remote, rp, copts); err != nil {
 494-		logger.Error("failed to clone repository", "err", err, "mirror", opts.Mirror, "remote", remote, "path", rp)
 495+		d.logger.Error("failed to clone repository", "err", err, "mirror", opts.Mirror, "remote", remote, "path", rp)
 496 		return nil, err
 497 	}
 498 
 499@@ -311,7 +310,7 @@ func (d *SqliteBackend) Repository(repo string) (backend.Repository, error) {
 500 	}
 501 
 502 	if count == 0 {
 503-		logger.Warn("repository exists but not found in database", "repo", repo)
 504+		d.logger.Warn("repository exists but not found in database", "repo", repo)
 505 		return nil, ErrRepoNotExist
 506 	}
 507 
 508diff --git a/server/backend/sqlite/user.go b/server/backend/sqlite/user.go
 509index 456581d4d74c43a507d33194089db8d0611c98dd..7306c5cfa4e7d6f57ffd58258960eb15eadb8b55 100644
 510--- a/server/backend/sqlite/user.go
 511+++ b/server/backend/sqlite/user.go
 512@@ -180,7 +180,7 @@ func (d *SqliteBackend) CreateUser(username string, opts backend.UserOptions) (b
 513 		if len(opts.PublicKeys) > 0 {
 514 			userID, err := r.LastInsertId()
 515 			if err != nil {
 516-				logger.Error("error getting last insert id")
 517+				d.logger.Error("error getting last insert id")
 518 				return err
 519 			}
 520 
 521diff --git a/server/config/config.go b/server/config/config.go
 522index 7bd696e0e0d18220f3726dea9d781635b6f24fec..2c68bda24328db19d129749abe0166b954ab469d 100644
 523--- a/server/config/config.go
 524+++ b/server/config/config.go
 525@@ -1,6 +1,7 @@
 526 package config
 527 
 528 import (
 529+	"context"
 530 	"errors"
 531 	"fmt"
 532 	"os"
 533@@ -88,6 +89,10 @@ type Config struct {
 534 	// Stats is the configuration for the stats server.
 535 	Stats StatsConfig `envPrefix:"STATS_" yaml:"stats"`
 536 
 537+	// LogFormat is the format of the logs.
 538+	// Valid values are "json", "logfmt", and "text".
 539+	LogFormat string `env:"LOG_FORMAT" yaml:"log_format"`
 540+
 541 	// InitialAdminKeys is a list of public keys that will be added to the list of admins.
 542 	InitialAdminKeys []string `env:"INITIAL_ADMIN_KEYS" envSeparator:"\n" yaml:"initial_admin_keys"`
 543 
 544@@ -101,8 +106,9 @@ type Config struct {
 545 func parseConfig(path string) (*Config, error) {
 546 	dataPath := filepath.Dir(path)
 547 	cfg := &Config{
 548-		Name:     "Soft Serve",
 549-		DataPath: dataPath,
 550+		Name:      "Soft Serve",
 551+		LogFormat: "text",
 552+		DataPath:  dataPath,
 553 		SSH: SSHConfig{
 554 			ListenAddr:    ":23231",
 555 			PublicURL:     "ssh://localhost:23231",
 556@@ -273,3 +279,21 @@ func parseAuthKeys(aks []string) []ssh.PublicKey {
 557 func (c *Config) AdminKeys() []ssh.PublicKey {
 558 	return parseAuthKeys(c.InitialAdminKeys)
 559 }
 560+
 561+var (
 562+	configCtxKey = struct{ string }{"config"}
 563+)
 564+
 565+// WithContext returns a new context with the configuration attached.
 566+func WithContext(ctx context.Context, cfg *Config) context.Context {
 567+	return context.WithValue(ctx, configCtxKey, cfg)
 568+}
 569+
 570+// FromContext returns the configuration from the context.
 571+func FromContext(ctx context.Context) *Config {
 572+	if c, ok := ctx.Value(configCtxKey).(*Config); ok {
 573+		return c
 574+	}
 575+
 576+	return DefaultConfig()
 577+}
 578diff --git a/server/config/file.go b/server/config/file.go
 579index 1869dd87fcfd72aca7c71fb50cf6a19e49231f9e..11e239fb094d30b5e06591c1f6a8603a29b331b6 100644
 580--- a/server/config/file.go
 581+++ b/server/config/file.go
 582@@ -12,6 +12,9 @@ var (
 583 # This is the name that will be displayed in the UI.
 584 name: "{{ .Name }}"
 585 
 586+# Log format to use. Valid values are "json", "logfmt", and "text".
 587+log_format: "{{ .LogFormat }}"
 588+
 589 # The SSH server configuration.
 590 ssh:
 591   # The address on which the SSH server will listen.
 592diff --git a/server/cron/cron.go b/server/cron/cron.go
 593index 27e052dee20e8aeaa4dc77d2530a4f4ab87ef02d..af4baf0ad5f83d8398e403ffe15c124833b1dbda 100644
 594--- a/server/cron/cron.go
 595+++ b/server/cron/cron.go
 596@@ -39,7 +39,7 @@ func (l cronLogger) Error(err error, msg string, keysAndValues ...interface{}) {
 597 
 598 // NewCronScheduler returns a new Cron.
 599 func NewCronScheduler(ctx context.Context) *CronScheduler {
 600-	logger := cronLogger{log.FromContext(ctx).WithPrefix("server.cron")}
 601+	logger := cronLogger{log.FromContext(ctx).WithPrefix("cron")}
 602 	return &CronScheduler{
 603 		Cron: cron.New(cron.WithLogger(logger)),
 604 	}
 605diff --git a/server/daemon/daemon.go b/server/daemon/daemon.go
 606index c3e08381502b6571fb4d3595627790680d053965..cf71ae6730ac445fa8122160c86c401edd79cba7 100644
 607--- a/server/daemon/daemon.go
 608+++ b/server/daemon/daemon.go
 609@@ -19,10 +19,6 @@ import (
 610 	"github.com/prometheus/client_golang/prometheus/promauto"
 611 )
 612 
 613-var (
 614-	logger = log.WithPrefix("server.daemon")
 615-)
 616-
 617 var (
 618 	uploadPackGitCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 619 		Namespace: "soft_serve",
 620@@ -81,6 +77,7 @@ func (m *connections) CloseAll() {
 621 
 622 // GitDaemon represents a Git daemon.
 623 type GitDaemon struct {
 624+	ctx      context.Context
 625 	listener net.Listener
 626 	addr     string
 627 	finished chan struct{}
 628@@ -88,16 +85,20 @@ type GitDaemon struct {
 629 	cfg      *config.Config
 630 	wg       sync.WaitGroup
 631 	once     sync.Once
 632+	logger   *log.Logger
 633 }
 634 
 635 // NewDaemon returns a new Git daemon.
 636-func NewGitDaemon(cfg *config.Config) (*GitDaemon, error) {
 637+func NewGitDaemon(ctx context.Context) (*GitDaemon, error) {
 638+	cfg := config.FromContext(ctx)
 639 	addr := cfg.Git.ListenAddr
 640 	d := &GitDaemon{
 641+		ctx:      ctx,
 642 		addr:     addr,
 643 		finished: make(chan struct{}, 1),
 644 		cfg:      cfg,
 645 		conns:    connections{m: make(map[net.Conn]struct{})},
 646+		logger:   log.FromContext(ctx).WithPrefix("gitdaemon"),
 647 	}
 648 	listener, err := net.Listen("tcp", d.addr)
 649 	if err != nil {
 650@@ -122,7 +123,7 @@ func (d *GitDaemon) Start() error {
 651 			case <-d.finished:
 652 				return ErrServerClosed
 653 			default:
 654-				logger.Debugf("git: error accepting connection: %v", err)
 655+				d.logger.Debugf("git: error accepting connection: %v", err)
 656 			}
 657 			if ne, ok := err.(net.Error); ok && ne.Temporary() {
 658 				if tempDelay == 0 {
 659@@ -141,8 +142,8 @@ func (d *GitDaemon) Start() error {
 660 
 661 		// Close connection if there are too many open connections.
 662 		if d.conns.Size()+1 >= d.cfg.Git.MaxConnections {
 663-			logger.Debugf("git: max connections reached, closing %s", conn.RemoteAddr())
 664-			fatal(conn, git.ErrMaxConnections)
 665+			d.logger.Debugf("git: max connections reached, closing %s", conn.RemoteAddr())
 666+			d.fatal(conn, git.ErrMaxConnections)
 667 			continue
 668 		}
 669 
 670@@ -154,10 +155,10 @@ func (d *GitDaemon) Start() error {
 671 	}
 672 }
 673 
 674-func fatal(c net.Conn, err error) {
 675+func (d *GitDaemon) fatal(c net.Conn, err error) {
 676 	git.WritePktline(c, err)
 677 	if err := c.Close(); err != nil {
 678-		logger.Debugf("git: error closing connection: %v", err)
 679+		d.logger.Debugf("git: error closing connection: %v", err)
 680 	}
 681 }
 682 
 683@@ -185,10 +186,10 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
 684 		if !s.Scan() {
 685 			if err := s.Err(); err != nil {
 686 				if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
 687-					fatal(c, git.ErrTimeout)
 688+					d.fatal(c, git.ErrTimeout)
 689 				} else {
 690-					logger.Debugf("git: error scanning pktline: %v", err)
 691-					fatal(c, git.ErrSystemMalfunction)
 692+					d.logger.Debugf("git: error scanning pktline: %v", err)
 693+					d.fatal(c, git.ErrSystemMalfunction)
 694 				}
 695 			}
 696 			return
 697@@ -199,14 +200,14 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
 698 	select {
 699 	case <-ctx.Done():
 700 		if err := ctx.Err(); err != nil {
 701-			logger.Debugf("git: connection context error: %v", err)
 702+			d.logger.Debugf("git: connection context error: %v", err)
 703 		}
 704 		return
 705 	case <-readc:
 706 		line := s.Bytes()
 707 		split := bytes.SplitN(line, []byte{' '}, 2)
 708 		if len(split) != 2 {
 709diff --git a/server/daemon/daemon_test.go b/server/daemon/daemon_test.go
 710index e728af6e3597804ec568ce22aa5385a7d370c51d..b06decbf4724ad3ab3a62e6632bddbf32a0078bd 100644
 711--- a/server/daemon/daemon_test.go
 712+++ b/server/daemon/daemon_test.go
 713@@ -32,13 +32,14 @@ func TestMain(m *testing.M) {
 714 	os.Setenv("SOFT_SERVE_GIT_MAX_TIMEOUT", "100")
 715 	os.Setenv("SOFT_SERVE_GIT_IDLE_TIMEOUT", "1")
 716 	os.Setenv("SOFT_SERVE_GIT_LISTEN_ADDR", fmt.Sprintf(":%d", test.RandomPort()))
 717+	ctx := context.TODO()
 718 	cfg := config.DefaultConfig()
 719-	d, err := NewGitDaemon(cfg)
 720+	ctx = config.WithContext(ctx, cfg)
 721+	d, err := NewGitDaemon(ctx)
 722 	if err != nil {
 723 		log.Fatal(err)
 724 	}
 725-	ctx := context.TODO()
 726-	fb, err := sqlite.NewSqliteBackend(ctx, cfg)
 727+	fb, err := sqlite.NewSqliteBackend(ctx)
 728 	if err != nil {
 729 		log.Fatal(err)
 730 	}
 731diff --git a/server/git/git.go b/server/git/git.go
 732index 9a8b5ed9d401616151e7a47ced47a5ad9ff40dbc..5a49b3dc41f7e71b488ec3f511d1d665aa2a3a29 100644
 733--- a/server/git/git.go
 734+++ b/server/git/git.go
 735@@ -12,6 +12,7 @@ import (
 736 
 737 	"github.com/charmbracelet/log"
 738 	"github.com/charmbracelet/soft-serve/git"
 739+	"github.com/charmbracelet/soft-serve/server/config"
 740 	"github.com/go-git/go-git/v5/plumbing/format/pktline"
 741 	"golang.org/x/sync/errgroup"
 742 )
 743@@ -78,12 +79,16 @@ func ReceivePack(ctx context.Context, in io.Reader, out io.Writer, er io.Writer,
 744 
 745 // RunGit runs a git command in the given repo.
 746 func RunGit(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, dir string, envs []string, args ...string) error {
 747-	logger := log.WithPrefix("server.git")
 748+	cfg := config.FromContext(ctx)
 749+	logger := log.FromContext(ctx).WithPrefix("rungit")
 750 	c := exec.CommandContext(ctx, "git", args...)
 751 	c.Dir = dir
 752 	c.Env = append(c.Env, envs...)
 753-	c.Env = append(c.Env, "SOFT_SERVE_DEBUG="+os.Getenv("SOFT_SERVE_DEBUG"))
 754 	c.Env = append(c.Env, "PATH="+os.Getenv("PATH"))
 755+	c.Env = append(c.Env, "SOFT_SERVE_DEBUG="+os.Getenv("SOFT_SERVE_DEBUG"))
 756+	if cfg != nil {
 757+		c.Env = append(c.Env, "SOFT_SERVE_LOG_FORMAT="+cfg.LogFormat)
 758+	}
 759 
 760 	stdin, err := c.StdinPipe()
 761 	if err != nil {
 762diff --git a/server/jobs.go b/server/jobs.go
 763index 5f239be583961649353568d5048042e497f8bfb6..06656d1ccec5b53b6868fb613168b170802962db 100644
 764--- a/server/jobs.go
 765+++ b/server/jobs.go
 766@@ -5,7 +5,6 @@ import (
 767 	"path/filepath"
 768 
 769 	"github.com/charmbracelet/soft-serve/git"
 770-	"github.com/charmbracelet/soft-serve/server/config"
 771 )
 772 
 773 var (
 774@@ -15,9 +14,10 @@ var (
 775 )
 776 
 777 // mirrorJob runs the (pull) mirror job task.
 778-func mirrorJob(cfg *config.Config) func() {
 779+func (s *Server) mirrorJob() func() {
 780+	cfg := s.Config
 781 	b := cfg.Backend
 782-	logger := logger.WithPrefix("server.mirrorJob")
 783+	logger := s.logger
 784 	return func() {
 785 		repos, err := b.Repositories()
 786 		if err != nil {
 787diff --git a/server/server.go b/server/server.go
 788index 182b20f6662cb49c3a8655f84b5b06d806f047cd..012ea05192cf94ad8bc5f120eaf3c041a40ca62d 100644
 789--- a/server/server.go
 790+++ b/server/server.go
 791@@ -20,10 +20,6 @@ import (
 792 	"golang.org/x/sync/errgroup"
 793 )
 794 
 795-var (
 796-	logger = log.WithPrefix("server")
 797-)
 798-
 799 // Server is the Soft Serve server.
 800 type Server struct {
 801 	SSHServer   *sshsrv.SSHServer
 802@@ -33,7 +29,9 @@ type Server struct {
 803 	Cron        *cron.CronScheduler
 804 	Config      *config.Config
 805 	Backend     backend.Backend
 806-	ctx         context.Context
 807+
 808+	logger *log.Logger
 809+	ctx    context.Context
 810 }
 811 
 812 // NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
 813@@ -41,10 +39,12 @@ type Server struct {
 814 // key can be provided with authKey. If authKey is provided, access will be
 815 // restricted to that key. If authKey is not provided, the server will be
 816 // publicly writable until configured otherwise by cloning the `config` repo.
 817-func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) {
 818+func NewServer(ctx context.Context) (*Server, error) {
 819+	cfg := config.FromContext(ctx)
 820+
 821 	var err error
 822 	if cfg.Backend == nil {
 823-		sb, err := sqlite.NewSqliteBackend(ctx, cfg)
 824+		sb, err := sqlite.NewSqliteBackend(ctx)
 825 		if err != nil {
 826 			return nil, fmt.Errorf("create backend: %w", err)
 827 		}
 828@@ -56,28 +56,29 @@ func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) {
 829 		Cron:    cron.NewCronScheduler(ctx),
 830 		Config:  cfg,
 831 		Backend: cfg.Backend,
 832+		logger:  log.FromContext(ctx).WithPrefix("server"),
 833 		ctx:     ctx,
 834 	}
 835 
 836 	// Add cron jobs.
 837-	srv.Cron.AddFunc(jobSpecs["mirror"], mirrorJob(cfg))
 838+	srv.Cron.AddFunc(jobSpecs["mirror"], srv.mirrorJob())
 839 
 840-	srv.SSHServer, err = sshsrv.NewSSHServer(cfg)
 841+	srv.SSHServer, err = sshsrv.NewSSHServer(ctx)
 842 	if err != nil {
 843 		return nil, fmt.Errorf("create ssh server: %w", err)
 844 	}
 845 
 846-	srv.GitDaemon, err = daemon.NewGitDaemon(cfg)
 847+	srv.GitDaemon, err = daemon.NewGitDaemon(ctx)
 848 	if err != nil {
 849 		return nil, fmt.Errorf("create git daemon: %w", err)
 850 	}
 851 
 852-	srv.HTTPServer, err = web.NewHTTPServer(cfg)
 853+	srv.HTTPServer, err = web.NewHTTPServer(ctx)
 854 	if err != nil {
 855 		return nil, fmt.Errorf("create http server: %w", err)
 856 	}
 857 
 858-	srv.StatsServer, err = stats.NewStatsServer(cfg)
 859+	srv.StatsServer, err = stats.NewStatsServer(ctx)
 860 	if err != nil {
 861 		return nil, fmt.Errorf("create stats server: %w", err)
 862 	}
 863@@ -101,38 +102,37 @@ func start(ctx context.Context, fn func() error) error {
 864 
 865 // Start starts the SSH server.
 866 func (s *Server) Start() error {
 867-	logger := log.FromContext(s.ctx).WithPrefix("server")
 868 	errg, ctx := errgroup.WithContext(s.ctx)
 869 	errg.Go(func() error {
 870-		logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
 871+		s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
 872 		if err := start(ctx, s.GitDaemon.Start); !errors.Is(err, daemon.ErrServerClosed) {
 873 			return err
 874 		}
 875 		return nil
 876 	})
 877 	errg.Go(func() error {
 878-		logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
 879+		s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
 880 		if err := start(ctx, s.HTTPServer.ListenAndServe); !errors.Is(err, http.ErrServerClosed) {
 881 			return err
 882 		}
 883 		return nil
 884 	})
 885 	errg.Go(func() error {
 886-		logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
 887+		s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
 888 		if err := start(ctx, s.SSHServer.ListenAndServe); !errors.Is(err, ssh.ErrServerClosed) {
 889 			return err
 890 		}
 891diff --git a/server/server_test.go b/server/server_test.go
 892index a3986b2d3b985ad49ce9c91bbf555e5d00f89bed..a856f1f4c33aeec35607d0b19420378eb0432776 100644
 893--- a/server/server_test.go
 894+++ b/server/server_test.go
 895@@ -25,10 +25,11 @@ func setupServer(tb testing.TB) (*Server, *config.Config, string) {
 896 	tb.Setenv("SOFT_SERVE_INITIAL_ADMIN_KEY", authorizedKey(pub))
 897 	tb.Setenv("SOFT_SERVE_SSH_LISTEN_ADDR", sshPort)
 898 	tb.Setenv("SOFT_SERVE_GIT_LISTEN_ADDR", fmt.Sprintf(":%d", test.RandomPort()))
 899+	ctx := context.TODO()
 900 	cfg := config.DefaultConfig()
 901+	ctx = config.WithContext(ctx, cfg)
 902 	tb.Log("configuring server")
 903-	ctx := context.TODO()
 904-	s, err := NewServer(ctx, cfg)
 905+	s, err := NewServer(ctx)
 906 	if err != nil {
 907 		tb.Fatal(err)
 908 	}
 909diff --git a/server/ssh/session_test.go b/server/ssh/session_test.go
 910index a39d500d25823a1b07e496ea75f111076f209a17..995ab20fd122f060479562dba0139f83fe871902 100644
 911--- a/server/ssh/session_test.go
 912+++ b/server/ssh/session_test.go
 913@@ -59,7 +59,8 @@ func setup(tb testing.TB) (*gossh.Session, func() error) {
 914 	})
 915 	ctx := context.TODO()
 916 	cfg := config.DefaultConfig()
 917-	fb, err := sqlite.NewSqliteBackend(ctx, cfg)
 918+	ctx = config.WithContext(ctx, cfg)
 919+	fb, err := sqlite.NewSqliteBackend(ctx)
 920 	if err != nil {
 921 		log.Fatal(err)
 922 	}
 923diff --git a/server/ssh/ssh.go b/server/ssh/ssh.go
 924index 419ab241d2e335f8b6c4ca8ae6330177c77dce4f..9c55aea185f4556de3ef88cd70f9d73cb4fd96b7 100644
 925--- a/server/ssh/ssh.go
 926+++ b/server/ssh/ssh.go
 927@@ -26,10 +26,6 @@ import (
 928 	gossh "golang.org/x/crypto/ssh"
 929 )
 930 
 931-var (
 932-	logger = log.WithPrefix("server.ssh")
 933-)
 934-
 935 var (
 936 	publicKeyCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 937 		Namespace: "soft_serve",
 938@@ -76,15 +72,22 @@ var (
 939 
 940 // SSHServer is a SSH server that implements the git protocol.
 941 type SSHServer struct {
 942-	srv *ssh.Server
 943-	cfg *config.Config
 944+	srv    *ssh.Server
 945+	cfg    *config.Config
 946+	ctx    context.Context
 947+	logger *log.Logger
 948 }
 949 
 950 // NewSSHServer returns a new SSHServer.
 951-func NewSSHServer(cfg *config.Config) (*SSHServer, error) {
 952+func NewSSHServer(ctx context.Context) (*SSHServer, error) {
 953+	cfg := config.FromContext(ctx)
 954 	var err error
 955-	s := &SSHServer{cfg: cfg}
 956-	logger := logger.StandardLog(log.StandardLogOptions{ForceLevel: log.DebugLevel})
 957+	s := &SSHServer{
 958+		cfg:    cfg,
 959+		ctx:    ctx,
 960+		logger: log.FromContext(ctx).WithPrefix("ssh"),
 961+	}
 962+	logger := s.logger.StandardLog(log.StandardLogOptions{ForceLevel: log.DebugLevel})
 963 	mw := []wish.Middleware{
 964 		rm.MiddlewareWithLogger(
 965 			logger,
 966@@ -151,7 +154,7 @@ func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) (allowed
 967 	}(&allowed)
 968 
 969 	ac := s.cfg.Backend.AccessLevelByPublicKey("", pk)
 970-	logger.Debugf("access level for %q: %s", ak, ac)
 971+	s.logger.Debugf("access level for %q: %s", ak, ac)
 972 	allowed = ac >= backend.ReadOnlyAccess
 973 	return
 974 }
 975@@ -168,7 +171,7 @@ func (s *SSHServer) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.Keyboard
 976 // checked for access on a per repo basis for a ssh.Session public key.
 977 // Hooks.Push and Hooks.Fetch will be called on successful completion of
 978 // their commands.
 979-func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 980+func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 981 	return func(sh ssh.Handler) ssh.Handler {
 982 		return func(s ssh.Session) {
 983 			func() {
 984@@ -196,7 +199,7 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 985 						"SOFT_SERVE_PUBLIC_KEY=" + ak,
 986 					}
 987 
 988-					logger.Debug("git middleware", "cmd", gc, "access", access.String())
 989+					ss.logger.Debug("git middleware", "cmd", gc, "access", access.String())
 990 					repoDir := filepath.Join(reposDir, repo)
 991 					switch gc {
 992 					case git.ReceivePackBin:
 993diff --git a/server/stats/stats.go b/server/stats/stats.go
 994index d0029d65aed0b1d0d68aa7e4d625a10838b4d512..b515f1b52a816fd111b40c7842573cc592c0e8fa 100644
 995--- a/server/stats/stats.go
 996+++ b/server/stats/stats.go
 997@@ -11,15 +11,18 @@ import (
 998 
 999 // StatsServer is a server for collecting and reporting statistics.
1000 type StatsServer struct {
1001+	ctx    context.Context
1002 	cfg    *config.Config
1003 	server *http.Server
1004 }
1005 
1006 // NewStatsServer returns a new StatsServer.
1007-func NewStatsServer(cfg *config.Config) (*StatsServer, error) {
1008+func NewStatsServer(ctx context.Context) (*StatsServer, error) {
1009+	cfg := config.FromContext(ctx)
1010 	mux := http.NewServeMux()
1011 	mux.Handle("/metrics", promhttp.Handler())
1012 	return &StatsServer{
1013+		ctx: ctx,
1014 		cfg: cfg,
1015 		server: &http.Server{
1016 			Addr:              cfg.Stats.ListenAddr,
1017diff --git a/server/web/http.go b/server/web/http.go
1018index 694f58807f0750ea75ff84ed2637c0f977ef00ae..c3e33e0f9960eacccccf6500cc5751e64cd9f0a5 100644
1019--- a/server/web/http.go
1020+++ b/server/web/http.go
1021@@ -24,10 +24,6 @@ import (
1022 	"goji.io/pattern"
1023 )
1024 
1025-var (
1026-	logger = log.WithPrefix("server.web")
1027-)
1028-
1029 var (
1030 	gitHttpCounter = promauto.NewCounterVec(prometheus.CounterOpts{
1031 		Namespace: "soft_serve",
1032@@ -64,18 +60,17 @@ func (r *logWriter) WriteHeader(code int) {
1033 	r.ResponseWriter.WriteHeader(code)
1034 }
1035 
1036-func loggingMiddleware(next http.Handler) http.Handler {
1037-	logger := logger.WithPrefix("server.http")
1038+func (s *HTTPServer) loggingMiddleware(next http.Handler) http.Handler {
1039 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1040 		start := time.Now()
1041 		writer := &logWriter{code: http.StatusOK, ResponseWriter: w}
1042-		logger.Debug("request",
1043+		s.logger.Debug("request",
1044 			"method", r.Method,
1045 			"uri", r.RequestURI,
1046 			"addr", r.RemoteAddr)
1047 		next.ServeHTTP(writer, r)
1048 		elapsed := time.Since(start)
1049-		logger.Debug("response",
1050+		s.logger.Debug("response",
1051 			"status", fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code)),
1052 			"bytes", humanize.Bytes(uint64(writer.bytes)),
1053 			"time", elapsed)
1054@@ -84,15 +79,20 @@ func loggingMiddleware(next http.Handler) http.Handler {
1055 
1056 // HTTPServer is an http server.
1057 type HTTPServer struct {
1058+	ctx        context.Context
1059 	cfg        *config.Config
1060 	server     *http.Server
1061 	dirHandler http.Handler
1062+	logger     *log.Logger
1063 }
1064 
1065-func NewHTTPServer(cfg *config.Config) (*HTTPServer, error) {
1066+func NewHTTPServer(ctx context.Context) (*HTTPServer, error) {
1067+	cfg := config.FromContext(ctx)
1068 	mux := goji.NewMux()
1069 	s := &HTTPServer{
1070+		ctx:        ctx,
1071 		cfg:        cfg,
1072+		logger:     log.FromContext(ctx).WithPrefix("http"),
1073 		dirHandler: http.FileServer(http.Dir(filepath.Join(cfg.DataPath, "repos"))),
1074 		server: &http.Server{
1075 			Addr:              cfg.HTTP.ListenAddr,
1076@@ -104,7 +104,7 @@ func NewHTTPServer(cfg *config.Config) (*HTTPServer, error) {
1077 		},
1078 	}
1079 
1080-	mux.Use(loggingMiddleware)
1081+	mux.Use(s.loggingMiddleware)
1082 	for _, m := range []Matcher{
1083 		getInfoRefs,
1084 		getHead,
1085@@ -306,7 +306,7 @@ func (s *HTTPServer) handleGit(w http.ResponseWriter, r *http.Request) {
1086 	repo := pat.Param(r, "repo")
1087 	repo = utils.SanitizeRepo(repo) + ".git"
1088 	if _, err := s.cfg.Backend.Repository(repo); err != nil {
1089-		logger.Debug("repository not found", "repo", repo, "err", err)
1090+		s.logger.Debug("repository not found", "repo", repo, "err", err)
1091 		http.NotFound(w, r)
1092 		return
1093 	}
1094diff --git a/ui/common/common.go b/ui/common/common.go
1095index 78b4a992bb1ee46cb9f2edb62392e74633d8ebd3..ebcd6621289d812e28aac42c2230617372a56cad 100644
1096--- a/ui/common/common.go
1097+++ b/ui/common/common.go
1098@@ -3,6 +3,7 @@ package common
1099 import (
1100 	"context"
1101 
1102+	"github.com/charmbracelet/log"
1103 	"github.com/charmbracelet/soft-serve/git"
1104 	"github.com/charmbracelet/soft-serve/server/config"
1105 	"github.com/charmbracelet/soft-serve/ui/keymap"
1106@@ -30,6 +31,7 @@ type Common struct {
1107 	KeyMap        *keymap.KeyMap
1108 	Zone          *zone.Manager
1109 	Output        *termenv.Output
1110+	Logger        *log.Logger
1111 }
1112 
1113 // NewCommon returns a new Common struct.
1114@@ -45,6 +47,7 @@ func NewCommon(ctx context.Context, out *termenv.Output, width, height int) Comm
1115 		Styles: styles.DefaultStyles(),
1116 		KeyMap: keymap.DefaultKeyMap(),
1117 		Zone:   zone.New(),
1118+		Logger: log.FromContext(ctx).WithPrefix("ui"),
1119 	}
1120 }
1121 
1122diff --git a/ui/pages/repo/log.go b/ui/pages/repo/log.go
1123index 93c1aa5dd58e673f8863f567d8cb5bfcf112512a..e59e886f7c397e071a259cea362672aafb46b94b 100644
1124--- a/ui/pages/repo/log.go
1125+++ b/ui/pages/repo/log.go
1126@@ -393,7 +393,7 @@ func (l *Log) countCommitsCmd() tea.Msg {
1127 	}
1128 	count, err := r.CountCommits(l.ref)
1129 	if err != nil {
1130-		logger.Debugf("ui: error counting commits: %v", err)
1131+		l.common.Logger.Debugf("ui: error counting commits: %v", err)
1132 		return common.ErrorMsg(err)
1133 	}
1134 	return LogCountMsg(count)
1135@@ -423,7 +423,7 @@ func (l *Log) updateCommitsCmd() tea.Msg {
1136 	// CommitsByPage pages start at 1
1137 	cc, err := r.CommitsByPage(l.ref, page+1, limit)
1138 	if err != nil {
1139-		logger.Debugf("ui: error loading commits: %v", err)
1140+		l.common.Logger.Debugf("ui: error loading commits: %v", err)
1141 		return common.ErrorMsg(err)
1142 	}
1143 	for i, c := range cc {
1144@@ -445,12 +445,12 @@ func (l *Log) selectCommitCmd(commit *git.Commit) tea.Cmd {
1145 func (l *Log) loadDiffCmd() tea.Msg {
1146 	r, err := l.repo.Open()
1147 	if err != nil {
1148-		logger.Debugf("ui: error loading diff repository: %v", err)
1149+		l.common.Logger.Debugf("ui: error loading diff repository: %v", err)
1150 		return common.ErrorMsg(err)
1151 	}
1152 	diff, err := r.Diff(l.selectedCommit)
1153 	if err != nil {
1154-		logger.Debugf("ui: error loading diff: %v", err)
1155+		l.common.Logger.Debugf("ui: error loading diff: %v", err)
1156 		return common.ErrorMsg(err)
1157 	}
1158 	return LogDiffMsg(diff)
1159diff --git a/ui/pages/repo/refs.go b/ui/pages/repo/refs.go
1160index 90b05c22e6145218d6f1cd98e6b15b8832a3315d..c8a1bc5d726a7122fda694e729bd41947161b341 100644
1161--- a/ui/pages/repo/refs.go
1162+++ b/ui/pages/repo/refs.go
1163@@ -181,7 +181,7 @@ func (r *Refs) updateItemsCmd() tea.Msg {
1164 	}
1165 	refs, err := rr.References()
1166 	if err != nil {
1167-		logger.Debugf("ui: error getting references: %v", err)
1168+		r.common.Logger.Debugf("ui: error getting references: %v", err)
1169 		return common.ErrorMsg(err)
1170 	}
1171 	for _, ref := range refs {
1172@@ -228,10 +228,8 @@ func UpdateRefCmd(repo backend.Repository) tea.Cmd {
1173 		}
1174 		ref, err := r.HEAD()
1175 		if err != nil {
1176-			logger.Debugf("ui: error getting HEAD reference: %v", err)
1177 			return common.ErrorMsg(err)
1178 		}
1179-		logger.Debugf("HEAD: %s", ref.Name())
1180 		return RefMsg(ref)
1181 	}
1182 }
1183diff --git a/ui/pages/repo/repo.go b/ui/pages/repo/repo.go
1184index 5e280b2b24242390147845bf6cbf054f4469d9fa..87f715e610d9ec46a5b833918c76646c659da926 100644
1185--- a/ui/pages/repo/repo.go
1186+++ b/ui/pages/repo/repo.go
1187@@ -8,7 +8,6 @@ import (
1188 	"github.com/charmbracelet/bubbles/spinner"
1189 	tea "github.com/charmbracelet/bubbletea"
1190 	"github.com/charmbracelet/lipgloss"
1191-	"github.com/charmbracelet/log"
1192 	"github.com/charmbracelet/soft-serve/git"
1193 	"github.com/charmbracelet/soft-serve/server/backend"
1194 	"github.com/charmbracelet/soft-serve/ui/common"
1195@@ -17,10 +16,6 @@ import (
1196 	"github.com/charmbracelet/soft-serve/ui/components/tabs"
1197 )
1198 
1199-var (
1200-	logger = log.WithPrefix("ui.repo")
1201-)
1202-
1203 type state int
1204 
1205 const (
1206@@ -92,6 +87,7 @@ func New(c common.Common) *Repo {
1207 	for i, t := range []tab{readmeTab, filesTab, commitsTab, branchesTab, tagsTab} {
1208 		ts[i] = t.String()
1209 	}
1210+	c.Logger = c.Logger.WithPrefix("ui.repo")
1211 	tb := tabs.New(c, ts)
1212 	readme := NewReadme(c)
1213 	log := NewLog(c)
1214diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go
1215index 2c7957c6aacaae4eb380dbc3447cf95de6b21580..5860d9d8be78a6437d0accb61878e1a9204a7b61 100644
1216--- a/ui/pages/selection/selection.go
1217+++ b/ui/pages/selection/selection.go
1218@@ -8,7 +8,6 @@ import (
1219 	"github.com/charmbracelet/bubbles/list"
1220 	tea "github.com/charmbracelet/bubbletea"
1221 	"github.com/charmbracelet/lipgloss"
1222-	"github.com/charmbracelet/log"
1223 	"github.com/charmbracelet/soft-serve/server/backend"
1224 	"github.com/charmbracelet/soft-serve/ui/common"
1225 	"github.com/charmbracelet/soft-serve/ui/components/code"
1226@@ -20,10 +19,6 @@ const (
1227 	defaultNoContent = "No readme found.\n\nCreate a `.soft-serve` repository and add a `README.md` file to display readme."
1228 )
1229 
1230-var (
1231-	logger = log.WithPrefix("ui.selection")
1232-)
1233-
1234 type pane int
1235 
1236 const (
1237@@ -219,7 +214,7 @@ func (s *Selection) Init() tea.Cmd {
1238 		if al >= backend.ReadOnlyAccess {
1239 			item, err := NewItem(r, cfg)
1240 			if err != nil {
1241-				logger.Debugf("ui: failed to create item for %s: %v", r.Name(), err)
1242+				s.common.Logger.Debugf("ui: failed to create item for %s: %v", r.Name(), err)
1243 				continue
1244 			}
1245 			sortedItems = append(sortedItems, item)
1246diff --git a/ui/ui.go b/ui/ui.go
1247index 78b92b8434249be1791defa69f5c0f85a097c764..9a2553809a8529510d9d8eef0b884615fabe49d2 100644
1248--- a/ui/ui.go
1249+++ b/ui/ui.go
1250@@ -7,7 +7,6 @@ import (
1251 	"github.com/charmbracelet/bubbles/list"
1252 	tea "github.com/charmbracelet/bubbletea"
1253 	"github.com/charmbracelet/lipgloss"
1254-	"github.com/charmbracelet/log"
1255 	"github.com/charmbracelet/soft-serve/server/backend"
1256 	"github.com/charmbracelet/soft-serve/ui/common"
1257 	"github.com/charmbracelet/soft-serve/ui/components/footer"
1258@@ -17,10 +16,6 @@ import (
1259 	"github.com/charmbracelet/soft-serve/ui/pages/selection"
1260 )
1261 
1262-var (
1263-	logger = log.WithPrefix("ui")
1264-)
1265-
1266 type page int
1267 
1268 const (
1269@@ -165,7 +160,7 @@ func (ui *UI) IsFiltering() bool {
1270 
1271 // Update implements tea.Model.
1272 func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
1273-	logger.Debugf("msg received: %T", msg)
1274+	ui.common.Logger.Debugf("msg received: %T", msg)
1275 	cmds := make([]tea.Cmd, 0)
1276 	switch msg := msg.(type) {
1277 	case tea.WindowSizeMsg:
1278@@ -295,7 +290,7 @@ func (ui *UI) openRepo(rn string) (backend.Repository, error) {
1279 	}
1280 	repos, err := cfg.Backend.Repositories()
1281 	if err != nil {
1282-		logger.Debugf("ui: failed to list repos: %v", err)
1283+		ui.common.Logger.Debugf("ui: failed to list repos: %v", err)
1284 		return nil, err
1285 	}
1286 	for _, r := range repos {