feat: use contexts and clean logging

Ayman Bagabas created

Change summary

cmd/soft/hook.go                | 43 +++++++++++++++-----------
cmd/soft/migrate_config.go      | 56 +++++++++++++++++++---------------
cmd/soft/root.go                | 19 +++++++++--
cmd/soft/serve.go               | 27 +++++++++++++++-
examples/setuid/main.go         |  3 +
log/log.go                      | 14 --------
server/backend/sqlite/db.go     |  2 
server/backend/sqlite/hooks.go  | 15 ++++-----
server/backend/sqlite/sqlite.go | 35 ++++++++++-----------
server/backend/sqlite/user.go   |  2 
server/config/config.go         | 28 ++++++++++++++++-
server/config/file.go           |  3 +
server/cron/cron.go             |  2 
server/daemon/daemon.go         | 47 +++++++++++++++--------------
server/daemon/daemon_test.go    |  7 ++-
server/git/git.go               |  9 ++++-
server/jobs.go                  |  6 +-
server/server.go                | 36 +++++++++++-----------
server/server_test.go           |  5 +-
server/ssh/session_test.go      |  3 +
server/ssh/ssh.go               | 27 +++++++++-------
server/stats/stats.go           |  5 ++
server/web/http.go              | 22 ++++++------
ui/common/common.go             |  3 +
ui/pages/repo/log.go            |  8 ++--
ui/pages/repo/refs.go           |  4 -
ui/pages/repo/repo.go           |  6 ---
ui/pages/selection/selection.go |  7 ---
ui/ui.go                        |  9 +----
29 files changed, 255 insertions(+), 198 deletions(-)

Detailed changes

cmd/soft/hook.go 🔗

@@ -11,6 +11,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/server/backend"
 	"github.com/charmbracelet/soft-serve/server/backend/sqlite"
 	"github.com/charmbracelet/soft-serve/server/config"
@@ -18,53 +19,57 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var (
-	confixCtxKey  = "config"
-	backendCtxKey = "backend"
-)
-
 var (
 	configPath string
 
+	logFileCtxKey = struct{}{}
+
 	hookCmd = &cobra.Command{
 		Use:    "hook",
 		Short:  "Run git server hooks",
 		Long:   "Handles Soft Serve git server hooks.",
 		Hidden: true,
 		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
+			ctx := cmd.Context()
 			cfg, err := config.ParseConfig(configPath)
 			if err != nil {
 				return fmt.Errorf("could not parse config: %w", err)
 			}
 
-			customHooksPath := filepath.Join(filepath.Dir(configPath), "hooks")
-			if _, err := os.Stat(customHooksPath); err != nil && os.IsNotExist(err) {
-				os.MkdirAll(customHooksPath, os.ModePerm)
-				// Generate update hook example without executable permissions
-				hookPath := filepath.Join(customHooksPath, "update.sample")
-				if err := os.WriteFile(hookPath, []byte(updateHookExample), 0744); err != nil {
-					return fmt.Errorf("failed to generate update hook example: %w", err)
-				}
+			ctx = config.WithContext(ctx, cfg)
+
+			logPath := filepath.Join(cfg.DataPath, "log", "hooks.log")
+			f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+			if err != nil {
+				return fmt.Errorf("opening file: %w", err)
 			}
 
+			ctx = context.WithValue(ctx, logFileCtxKey, f)
+			logger := log.FromContext(ctx)
+			logger.SetOutput(f)
+			ctx = log.WithContext(ctx, logger)
+			cmd.SetContext(ctx)
+
 			// Set up the backend
 			// TODO: support other backends
-			sb, err := sqlite.NewSqliteBackend(cmd.Context(), cfg)
+			sb, err := sqlite.NewSqliteBackend(ctx)
 			if err != nil {
 				return fmt.Errorf("failed to create sqlite backend: %w", err)
 			}
 
 			cfg = cfg.WithBackend(sb)
 
-			cmd.SetContext(context.WithValue(cmd.Context(), confixCtxKey, cfg))
-			cmd.SetContext(context.WithValue(cmd.Context(), backendCtxKey, sb))
-
 			return nil
 		},
+		PersistentPostRunE: func(cmd *cobra.Command, _ []string) error {
+			f := cmd.Context().Value(logFileCtxKey).(*os.File)
+			return f.Close()
+		},
 	}
 
 	hooksRunE = func(cmd *cobra.Command, args []string) error {
-		cfg := cmd.Context().Value(confixCtxKey).(*config.Config)
+		ctx := cmd.Context()
+		cfg := config.FromContext(ctx)
 		hks := cfg.Backend.(backend.Hooks)
 
 		// This is set in the server before invoking git-receive-pack/git-upload-pack
@@ -119,7 +124,7 @@ var (
 		// Custom hooks
 		if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {
 			// If the custom hook is executable, run it
-			if err := runCommand(cmd.Context(), &buf, stdout, stderr, customHookPath, args...); err != nil {
+			if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {
 				return fmt.Errorf("failed to run custom hook: %w", err)
 			}
 		}

cmd/soft/migrate_config.go 🔗

@@ -2,6 +2,7 @@ package main
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -27,15 +28,18 @@ var (
 		Short:  "Migrate config to new format",
 		Hidden: true,
 		RunE: func(cmd *cobra.Command, _ []string) error {
+			ctx := cmd.Context()
+
+			logger := log.FromContext(ctx)
 			// Disable logging timestamp
-			log.SetReportTimestamp(false)
+			logger.SetReportTimestamp(false)
 
 			keyPath := os.Getenv("SOFT_SERVE_KEY_PATH")
 			reposPath := os.Getenv("SOFT_SERVE_REPO_PATH")
 			bindAddr := os.Getenv("SOFT_SERVE_BIND_ADDRESS")
-			ctx := cmd.Context()
 			cfg := config.DefaultConfig()
-			sb, err := sqlite.NewSqliteBackend(ctx, cfg)
+			ctx = config.WithContext(ctx, cfg)
+			sb, err := sqlite.NewSqliteBackend(ctx)
 			if err != nil {
 				return fmt.Errorf("failed to create sqlite backend: %w", err)
 			}
@@ -43,13 +47,13 @@ var (
 			cfg = cfg.WithBackend(sb)
 
 			// Set SSH listen address
-			log.Info("Setting SSH listen address...")
+			logger.Info("Setting SSH listen address...")
 			if bindAddr != "" {
 				cfg.SSH.ListenAddr = bindAddr
 			}
 
 			// Copy SSH host key
-			log.Info("Copying SSH host key...")
+			logger.Info("Copying SSH host key...")
 			if keyPath != "" {
 				if err := os.MkdirAll(filepath.Join(cfg.DataPath, "ssh"), os.ModePerm); err != nil {
 					return fmt.Errorf("failed to create ssh directory: %w", err)
@@ -60,14 +64,14 @@ var (
 				}
 
 				if err := copyFile(keyPath+".pub", filepath.Join(cfg.DataPath, "ssh", filepath.Base(keyPath))+".pub"); err != nil {
-					log.Errorf("failed to copy ssh key: %s", err)
+					logger.Errorf("failed to copy ssh key: %s", err)
 				}
 
 				cfg.SSH.KeyPath = filepath.Join(cfg.DataPath, "ssh", filepath.Base(keyPath))
 			}
 
 			// Read config
-			log.Info("Reading config repository...")
+			logger.Info("Reading config repository...")
 			r, err := git.Open(filepath.Join(reposPath, "config"))
 			if err != nil {
 				return fmt.Errorf("failed to open config repo: %w", err)
@@ -119,7 +123,7 @@ var (
 			cfg.SSH.PublicURL = fmt.Sprintf("ssh://%s:%d", ocfg.Host, ocfg.Port)
 
 			// Set server settings
-			log.Info("Setting server settings...")
+			logger.Info("Setting server settings...")
 			if cfg.Backend.SetAllowKeyless(ocfg.AllowKeyless) != nil {
 				fmt.Fprintf(os.Stderr, "failed to set allow keyless\n")
 			}
@@ -132,7 +136,7 @@ var (
 
 			// Copy repos
 			if reposPath != "" {
-				log.Info("Copying repos...")
+				logger.Info("Copying repos...")
 				if err := os.MkdirAll(filepath.Join(cfg.DataPath, "repos"), os.ModePerm); err != nil {
 					return fmt.Errorf("failed to create repos directory: %w", err)
 				}
@@ -151,7 +155,7 @@ var (
 						continue
 					}
 
-					log.Infof("  Copying repo %s", dir.Name())
+					logger.Infof("  Copying repo %s", dir.Name())
 					src := filepath.Join(reposPath, utils.SanitizeRepo(dir.Name()))
 					dst := filepath.Join(cfg.DataPath, "repos", utils.SanitizeRepo(dir.Name())) + ".git"
 					if err := os.MkdirAll(dst, os.ModePerm); err != nil {
@@ -168,7 +172,7 @@ var (
 				}
 
 				if hasReadme {
-					log.Infof("  Copying readme from \"config\" to \".soft-serve\"")
+					logger.Infof("  Copying readme from \"config\" to \".soft-serve\"")
 
 					// Switch to main branch
 					bcmd := git.NewCommand("branch", "-M", "main")
@@ -236,7 +240,7 @@ var (
 			}
 
 			// Set repos metadata & collabs
-			log.Info("Setting repos metadata & collabs...")
+			logger.Info("Setting repos metadata & collabs...")
 			for _, r := range ocfg.Repos {
 				repo, name := r.Repo, r.Name
 				// Special case for config repo
@@ -246,26 +250,26 @@ var (
 				}
 
 				if err := sb.SetProjectName(repo, name); err != nil {
-					log.Errorf("failed to set repo name to %s: %s", repo, err)
+					logger.Errorf("failed to set repo name to %s: %s", repo, err)
 				}
 
 				if err := sb.SetDescription(repo, r.Note); err != nil {
-					log.Errorf("failed to set repo description to %s: %s", repo, err)
+					logger.Errorf("failed to set repo description to %s: %s", repo, err)
 				}
 
 				if err := sb.SetPrivate(repo, r.Private); err != nil {
-					log.Errorf("failed to set repo private to %s: %s", repo, err)
+					logger.Errorf("failed to set repo private to %s: %s", repo, err)
 				}
 
 				for _, collab := range r.Collabs {
 					if err := sb.AddCollaborator(repo, collab); err != nil {
-						log.Errorf("failed to add repo collab to %s: %s", repo, err)
+						logger.Errorf("failed to add repo collab to %s: %s", repo, err)
 					}
 				}
 			}
 
 			// Create users & collabs
-			log.Info("Creating users & collabs...")
+			logger.Info("Creating users & collabs...")
 			for _, user := range ocfg.Users {
 				keys := make(map[string]ssh.PublicKey)
 				for _, key := range user.PublicKeys {
@@ -284,23 +288,23 @@ var (
 
 				username := strings.ToLower(user.Name)
 				username = strings.ReplaceAll(username, " ", "-")
-				log.Infof("Creating user %q", username)
+				logger.Infof("Creating user %q", username)
 				if _, err := sb.CreateUser(username, backend.UserOptions{
 					Admin:      user.Admin,
 					PublicKeys: pubkeys,
 				}); err != nil {
-					log.Errorf("failed to create user: %s", err)
+					logger.Errorf("failed to create user: %s", err)
 				}
 
 				for _, repo := range user.CollabRepos {
 					if err := sb.AddCollaborator(repo, username); err != nil {
-						log.Errorf("failed to add user collab to %s: %s\n", repo, err)
+						logger.Errorf("failed to add user collab to %s: %s\n", repo, err)
 					}
 				}
 			}
 
-			log.Info("Writing config...")
-			defer log.Info("Done!")
+			logger.Info("Writing config...")
+			defer logger.Info("Done!")
 			return config.WriteConfig(filepath.Join(cfg.DataPath, "config.yaml"), cfg)
 		},
 	}
@@ -371,21 +375,23 @@ func copyDir(src string, dst string) error {
 	if fds, err = os.ReadDir(src); err != nil {
 		return err
 	}
+
 	for _, fd := range fds {
 		srcfp := filepath.Join(src, fd.Name())
 		dstfp := filepath.Join(dst, fd.Name())
 
 		if fd.IsDir() {
 			if err = copyDir(srcfp, dstfp); err != nil {
-				log.Error("failed to copy directory", "err", err)
+				err = errors.Join(err, err)
 			}
 		} else {
 			if err = copyFile(srcfp, dstfp); err != nil {
-				log.Error("failed to copy file", "err", err)
+				err = errors.Join(err, err)
 			}
 		}
 	}
-	return nil
+
+	return err
 }
 
 // Config is the configuration for the server.

cmd/soft/root.go 🔗

@@ -4,9 +4,11 @@ import (
 	"context"
 	"os"
 	"runtime/debug"
+	"strconv"
+	"strings"
+	"time"
 
 	"github.com/charmbracelet/log"
-	_ "github.com/charmbracelet/soft-serve/log"
 	"github.com/spf13/cobra"
 )
 
@@ -51,15 +53,24 @@ func init() {
 }
 
 func main() {
+	ctx := context.Background()
 	logger := log.NewWithOptions(os.Stderr, log.Options{
 		ReportTimestamp: true,
-		TimeFormat:      "2006-01-02",
+		TimeFormat:      time.DateOnly,
 	})
-	if os.Getenv("SOFT_SERVE_DEBUG") == "true" {
+	if debug, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_DEBUG")); debug {
 		logger.SetLevel(log.DebugLevel)
 	}
 
-	ctx := context.Background()
+	switch strings.ToLower(os.Getenv("SOFT_SERVE_LOG_FORMAT")) {
+	case "json":
+		logger.SetFormatter(log.JSONFormatter)
+	case "logfmt":
+		logger.SetFormatter(log.LogfmtFormatter)
+	case "text":
+		logger.SetFormatter(log.TextFormatter)
+	}
+
 	ctx = log.WithContext(ctx, logger)
 	if err := rootCmd.ExecuteContext(ctx); err != nil {
 		os.Exit(1)

cmd/soft/serve.go 🔗

@@ -5,10 +5,10 @@ import (
 	"fmt"
 	"os"
 	"os/signal"
+	"path/filepath"
 	"syscall"
 	"time"
 
-	_ "github.com/charmbracelet/soft-serve/log"
 	"github.com/charmbracelet/soft-serve/server"
 	"github.com/charmbracelet/soft-serve/server/config"
 	"github.com/spf13/cobra"
@@ -20,10 +20,31 @@ var (
 		Short: "Start the server",
 		Long:  "Start the server",
 		Args:  cobra.NoArgs,
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: func(cmd *cobra.Command, _ []string) error {
 			ctx := cmd.Context()
 			cfg := config.DefaultConfig()
-			s, err := server.NewServer(ctx, cfg)
+			ctx = config.WithContext(ctx, cfg)
+			cmd.SetContext(ctx)
+
+			// Create custom hooks directory if it doesn't exist
+			customHooksPath := filepath.Join(cfg.DataPath, "hooks")
+			if _, err := os.Stat(customHooksPath); err != nil && os.IsNotExist(err) {
+				os.MkdirAll(customHooksPath, os.ModePerm) // nolint: errcheck
+				// Generate update hook example without executable permissions
+				hookPath := filepath.Join(customHooksPath, "update.sample")
+				// nolint: gosec
+				if err := os.WriteFile(hookPath, []byte(updateHookExample), 0744); err != nil {
+					return fmt.Errorf("failed to generate update hook example: %w", err)
+				}
+			}
+
+			// Create log directory if it doesn't exist
+			logPath := filepath.Join(cfg.DataPath, "log")
+			if _, err := os.Stat(logPath); err != nil && os.IsNotExist(err) {
+				os.MkdirAll(logPath, os.ModePerm) // nolint: errcheck
+			}
+
+			s, err := server.NewServer(ctx)
 			if err != nil {
 				return fmt.Errorf("start server: %w", err)
 			}

examples/setuid/main.go 🔗

@@ -46,8 +46,9 @@ func main() {
 	}
 	ctx := context.Background()
 	cfg := config.DefaultConfig()
+	ctx = config.WithContext(ctx, cfg)
 	cfg.SSH.ListenAddr = fmt.Sprintf(":%d", *port)
-	s, err := server.NewServer(ctx, cfg)
+	s, err := server.NewServer(ctx)
 	if err != nil {
 		log.Fatal(err)
 	}

log/log.go 🔗

@@ -1,14 +0,0 @@
-// Package log initializes the logger for Soft Serve modules.
-package log
-
-import (
-	"os"
-
-	"github.com/charmbracelet/log"
-)
-
-func init() {
-	if os.Getenv("SOFT_SERVE_DEBUG") == "true" {
-		log.SetLevel(log.DebugLevel)
-	}
-}

server/backend/sqlite/db.go 🔗

@@ -66,7 +66,7 @@ func (d *SqliteBackend) init() error {
 			for _, k := range d.cfg.InitialAdminKeys {
 				pk, _, err := backend.ParseAuthorizedKey(k)
 				if err != nil {
-					logger.Error("error parsing initial admin key, skipping", "key", k, "err", err)
+					d.logger.Error("error parsing initial admin key, skipping", "key", k, "err", err)
 					continue
 				}
 

server/backend/sqlite/hooks.go 🔗

@@ -4,7 +4,6 @@ import (
 	"io"
 	"sync"
 
-	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/server/backend"
 )
 
@@ -12,28 +11,28 @@ import (
 //
 // It implements Hooks.
 func (d *SqliteBackend) PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []backend.HookArg) {
-	log.WithPrefix("backend.sqlite.hooks").Debug("post-receive hook called", "repo", repo, "args", args)
+	d.logger.Debug("post-receive hook called", "repo", repo, "args", args)
 }
 
 // PreReceive is called by the git pre-receive hook.
 //
 // It implements Hooks.
 func (d *SqliteBackend) PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []backend.HookArg) {
-	log.WithPrefix("backend.sqlite.hooks").Debug("pre-receive hook called", "repo", repo, "args", args)
+	d.logger.Debug("pre-receive hook called", "repo", repo, "args", args)
 }
 
 // Update is called by the git update hook.
 //
 // It implements Hooks.
 func (d *SqliteBackend) Update(stdout io.Writer, stderr io.Writer, repo string, arg backend.HookArg) {
-	log.WithPrefix("backend.sqlite.hooks").Debug("update hook called", "repo", repo, "arg", arg)
+	d.logger.Debug("update hook called", "repo", repo, "arg", arg)
 }
 
 // PostUpdate is called by the git post-update hook.
 //
 // It implements Hooks.
 func (d *SqliteBackend) PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string) {
-	log.WithPrefix("backend.sqlite.hooks").Debug("post-update hook called", "repo", repo, "args", args)
+	d.logger.Debug("post-update hook called", "repo", repo, "args", args)
 
 	var wg sync.WaitGroup
 
@@ -44,18 +43,18 @@ func (d *SqliteBackend) PostUpdate(stdout io.Writer, stderr io.Writer, repo stri
 
 		rr, err := d.Repository(repo)
 		if err != nil {
-			log.WithPrefix("backend.sqlite.hooks").Error("error getting repository", "repo", repo, "err", err)
+			d.logger.Error("error getting repository", "repo", repo, "err", err)
 			return
 		}
 
 		r, err := rr.Open()
 		if err != nil {
-			log.WithPrefix("backend.sqlite.hooks").Error("error opening repository", "repo", repo, "err", err)
+			d.logger.Error("error opening repository", "repo", repo, "err", err)
 			return
 		}
 
 		if err := r.UpdateServerInfo(); err != nil {
-			log.WithPrefix("backend.sqlite.hooks").Error("error updating server-info", "repo", repo, "err", err)
+			d.logger.Error("error updating server-info", "repo", repo, "err", err)
 			return
 		}
 	}()

server/backend/sqlite/sqlite.go 🔗

@@ -17,17 +17,14 @@ import (
 	_ "modernc.org/sqlite"
 )
 
-var (
-	logger = log.WithPrefix("backend.sqlite")
-)
-
 // SqliteBackend is a backend that uses a SQLite database as a Soft Serve
 // backend.
 type SqliteBackend struct {
-	cfg *config.Config
-	ctx context.Context
-	dp  string
-	db  *sqlx.DB
+	cfg    *config.Config
+	ctx    context.Context
+	dp     string
+	db     *sqlx.DB
+	logger *log.Logger
 }
 
 var _ backend.Backend = (*SqliteBackend)(nil)
@@ -37,7 +34,8 @@ func (d *SqliteBackend) reposPath() string {
 }
 
 // NewSqliteBackend creates a new SqliteBackend.
-func NewSqliteBackend(ctx context.Context, cfg *config.Config) (*SqliteBackend, error) {
+func NewSqliteBackend(ctx context.Context) (*SqliteBackend, error) {
+	cfg := config.FromContext(ctx)
 	dataPath := cfg.DataPath
 	if err := os.MkdirAll(dataPath, os.ModePerm); err != nil {
 		return nil, err
@@ -50,10 +48,11 @@ func NewSqliteBackend(ctx context.Context, cfg *config.Config) (*SqliteBackend,
 	}
 
 	d := &SqliteBackend{
-		cfg: cfg,
-		ctx: ctx,
-		dp:  dataPath,
-		db:  db,
+		cfg:    cfg,
+		ctx:    ctx,
+		dp:     dataPath,
+		db:     db,
+		logger: log.FromContext(ctx).WithPrefix("sqlite"),
 	}
 
 	if err := d.init(); err != nil {
@@ -137,13 +136,13 @@ func (d *SqliteBackend) CreateRepository(name string, opts backend.RepositoryOpt
 
 	rr, err := git.Init(rp, true)
 	if err != nil {
-		logger.Debug("failed to create repository", "err", err)
+		d.logger.Debug("failed to create repository", "err", err)
 		cleanup() // nolint: errcheck
 		return nil, err
 	}
 
 	if err := rr.UpdateServerInfo(); err != nil {
-		logger.Debug("failed to update server info", "err", err)
+		d.logger.Debug("failed to update server info", "err", err)
 		cleanup() // nolint: errcheck
 		return nil, err
 	}
@@ -154,7 +153,7 @@ func (d *SqliteBackend) CreateRepository(name string, opts backend.RepositoryOpt
 			name, opts.ProjectName, opts.Description, opts.Private, opts.Mirror, opts.Hidden)
 		return err
 	}); err != nil {
-		logger.Debug("failed to create repository in database", "err", err)
+		d.logger.Debug("failed to create repository in database", "err", err)
 		return nil, wrapDbErr(err)
 	}
 
@@ -192,7 +191,7 @@ func (d *SqliteBackend) ImportRepository(name string, remote string, opts backen
 	}
 
 	if err := git.Clone(remote, rp, copts); err != nil {
-		logger.Error("failed to clone repository", "err", err, "mirror", opts.Mirror, "remote", remote, "path", rp)
+		d.logger.Error("failed to clone repository", "err", err, "mirror", opts.Mirror, "remote", remote, "path", rp)
 		return nil, err
 	}
 
@@ -311,7 +310,7 @@ func (d *SqliteBackend) Repository(repo string) (backend.Repository, error) {
 	}
 
 	if count == 0 {
-		logger.Warn("repository exists but not found in database", "repo", repo)
+		d.logger.Warn("repository exists but not found in database", "repo", repo)
 		return nil, ErrRepoNotExist
 	}
 

server/backend/sqlite/user.go 🔗

@@ -180,7 +180,7 @@ func (d *SqliteBackend) CreateUser(username string, opts backend.UserOptions) (b
 		if len(opts.PublicKeys) > 0 {
 			userID, err := r.LastInsertId()
 			if err != nil {
-				logger.Error("error getting last insert id")
+				d.logger.Error("error getting last insert id")
 				return err
 			}
 

server/config/config.go 🔗

@@ -1,6 +1,7 @@
 package config
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"os"
@@ -88,6 +89,10 @@ type Config struct {
 	// Stats is the configuration for the stats server.
 	Stats StatsConfig `envPrefix:"STATS_" yaml:"stats"`
 
+	// LogFormat is the format of the logs.
+	// Valid values are "json", "logfmt", and "text".
+	LogFormat string `env:"LOG_FORMAT" yaml:"log_format"`
+
 	// InitialAdminKeys is a list of public keys that will be added to the list of admins.
 	InitialAdminKeys []string `env:"INITIAL_ADMIN_KEYS" envSeparator:"\n" yaml:"initial_admin_keys"`
 
@@ -101,8 +106,9 @@ type Config struct {
 func parseConfig(path string) (*Config, error) {
 	dataPath := filepath.Dir(path)
 	cfg := &Config{
-		Name:     "Soft Serve",
-		DataPath: dataPath,
+		Name:      "Soft Serve",
+		LogFormat: "text",
+		DataPath:  dataPath,
 		SSH: SSHConfig{
 			ListenAddr:    ":23231",
 			PublicURL:     "ssh://localhost:23231",
@@ -273,3 +279,21 @@ func parseAuthKeys(aks []string) []ssh.PublicKey {
 func (c *Config) AdminKeys() []ssh.PublicKey {
 	return parseAuthKeys(c.InitialAdminKeys)
 }
+
+var (
+	configCtxKey = struct{ string }{"config"}
+)
+
+// WithContext returns a new context with the configuration attached.
+func WithContext(ctx context.Context, cfg *Config) context.Context {
+	return context.WithValue(ctx, configCtxKey, cfg)
+}
+
+// FromContext returns the configuration from the context.
+func FromContext(ctx context.Context) *Config {
+	if c, ok := ctx.Value(configCtxKey).(*Config); ok {
+		return c
+	}
+
+	return DefaultConfig()
+}

server/config/file.go 🔗

@@ -12,6 +12,9 @@ var (
 # This is the name that will be displayed in the UI.
 name: "{{ .Name }}"
 
+# Log format to use. Valid values are "json", "logfmt", and "text".
+log_format: "{{ .LogFormat }}"
+
 # The SSH server configuration.
 ssh:
   # The address on which the SSH server will listen.

server/cron/cron.go 🔗

@@ -39,7 +39,7 @@ func (l cronLogger) Error(err error, msg string, keysAndValues ...interface{}) {
 
 // NewCronScheduler returns a new Cron.
 func NewCronScheduler(ctx context.Context) *CronScheduler {
-	logger := cronLogger{log.FromContext(ctx).WithPrefix("server.cron")}
+	logger := cronLogger{log.FromContext(ctx).WithPrefix("cron")}
 	return &CronScheduler{
 		Cron: cron.New(cron.WithLogger(logger)),
 	}

server/daemon/daemon.go 🔗

@@ -19,10 +19,6 @@ import (
 	"github.com/prometheus/client_golang/prometheus/promauto"
 )
 
-var (
-	logger = log.WithPrefix("server.daemon")
-)
-
 var (
 	uploadPackGitCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 		Namespace: "soft_serve",
@@ -81,6 +77,7 @@ func (m *connections) CloseAll() {
 
 // GitDaemon represents a Git daemon.
 type GitDaemon struct {
+	ctx      context.Context
 	listener net.Listener
 	addr     string
 	finished chan struct{}
@@ -88,16 +85,20 @@ type GitDaemon struct {
 	cfg      *config.Config
 	wg       sync.WaitGroup
 	once     sync.Once
+	logger   *log.Logger
 }
 
 // NewDaemon returns a new Git daemon.
-func NewGitDaemon(cfg *config.Config) (*GitDaemon, error) {
+func NewGitDaemon(ctx context.Context) (*GitDaemon, error) {
+	cfg := config.FromContext(ctx)
 	addr := cfg.Git.ListenAddr
 	d := &GitDaemon{
+		ctx:      ctx,
 		addr:     addr,
 		finished: make(chan struct{}, 1),
 		cfg:      cfg,
 		conns:    connections{m: make(map[net.Conn]struct{})},
+		logger:   log.FromContext(ctx).WithPrefix("gitdaemon"),
 	}
 	listener, err := net.Listen("tcp", d.addr)
 	if err != nil {
@@ -122,7 +123,7 @@ func (d *GitDaemon) Start() error {
 			case <-d.finished:
 				return ErrServerClosed
 			default:
-				logger.Debugf("git: error accepting connection: %v", err)
+				d.logger.Debugf("git: error accepting connection: %v", err)
 			}
 			if ne, ok := err.(net.Error); ok && ne.Temporary() {
 				if tempDelay == 0 {
@@ -141,8 +142,8 @@ func (d *GitDaemon) Start() error {
 
 		// Close connection if there are too many open connections.
 		if d.conns.Size()+1 >= d.cfg.Git.MaxConnections {
-			logger.Debugf("git: max connections reached, closing %s", conn.RemoteAddr())
-			fatal(conn, git.ErrMaxConnections)
+			d.logger.Debugf("git: max connections reached, closing %s", conn.RemoteAddr())
+			d.fatal(conn, git.ErrMaxConnections)
 			continue
 		}
 
@@ -154,10 +155,10 @@ func (d *GitDaemon) Start() error {
 	}
 }
 
-func fatal(c net.Conn, err error) {
+func (d *GitDaemon) fatal(c net.Conn, err error) {
 	git.WritePktline(c, err)
 	if err := c.Close(); err != nil {
-		logger.Debugf("git: error closing connection: %v", err)
+		d.logger.Debugf("git: error closing connection: %v", err)
 	}
 }
 
@@ -185,10 +186,10 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
 		if !s.Scan() {
 			if err := s.Err(); err != nil {
 				if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
-					fatal(c, git.ErrTimeout)
+					d.fatal(c, git.ErrTimeout)
 				} else {
-					logger.Debugf("git: error scanning pktline: %v", err)
-					fatal(c, git.ErrSystemMalfunction)
+					d.logger.Debugf("git: error scanning pktline: %v", err)
+					d.fatal(c, git.ErrSystemMalfunction)
 				}
 			}
 			return
@@ -199,14 +200,14 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
 	select {
 	case <-ctx.Done():
 		if err := ctx.Err(); err != nil {
-			logger.Debugf("git: connection context error: %v", err)
+			d.logger.Debugf("git: connection context error: %v", err)
 		}
 		return
 	case <-readc:
 		line := s.Bytes()
 		split := bytes.SplitN(line, []byte{' '}, 2)
 		if len(split) != 2 {
-			fatal(c, git.ErrInvalidRequest)
+			d.fatal(c, git.ErrInvalidRequest)
 			return
 		}
 
@@ -220,36 +221,36 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
 			gitPack = git.UploadArchive
 			counter = uploadArchiveGitCounter
 		default:
-			fatal(c, git.ErrInvalidRequest)
+			d.fatal(c, git.ErrInvalidRequest)
 			return
 		}
 
 		opts := bytes.Split(split[1], []byte{'\x00'})
 		if len(opts) == 0 {
-			fatal(c, git.ErrInvalidRequest)
+			d.fatal(c, git.ErrInvalidRequest)
 			return
 		}
 
 		if !d.cfg.Backend.AllowKeyless() {
-			fatal(c, git.ErrNotAuthed)
+			d.fatal(c, git.ErrNotAuthed)
 			return
 		}
 
 		name := utils.SanitizeRepo(string(opts[0]))
-		logger.Debugf("git: connect %s %s %s", c.RemoteAddr(), cmd, name)
-		defer logger.Debugf("git: disconnect %s %s %s", c.RemoteAddr(), cmd, name)
+		d.logger.Debugf("git: connect %s %s %s", c.RemoteAddr(), cmd, name)
+		defer d.logger.Debugf("git: disconnect %s %s %s", c.RemoteAddr(), cmd, name)
 		// git bare repositories should end in ".git"
 		// https://git-scm.com/docs/gitrepository-layout
 		repo := name + ".git"
 		reposDir := filepath.Join(d.cfg.DataPath, "repos")
 		if err := git.EnsureWithin(reposDir, repo); err != nil {
-			fatal(c, err)
+			d.fatal(c, err)
 			return
 		}
 
 		auth := d.cfg.Backend.AccessLevel(name, "")
 		if auth < backend.ReadOnlyAccess {
-			fatal(c, git.ErrNotAuthed)
+			d.fatal(c, git.ErrNotAuthed)
 			return
 		}
 
@@ -260,7 +261,7 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
 		}
 
 		if err := gitPack(ctx, c, c, c, filepath.Join(reposDir, repo), envs...); err != nil {
-			fatal(c, err)
+			d.fatal(c, err)
 			return
 		}
 

server/daemon/daemon_test.go 🔗

@@ -32,13 +32,14 @@ func TestMain(m *testing.M) {
 	os.Setenv("SOFT_SERVE_GIT_MAX_TIMEOUT", "100")
 	os.Setenv("SOFT_SERVE_GIT_IDLE_TIMEOUT", "1")
 	os.Setenv("SOFT_SERVE_GIT_LISTEN_ADDR", fmt.Sprintf(":%d", test.RandomPort()))
+	ctx := context.TODO()
 	cfg := config.DefaultConfig()
-	d, err := NewGitDaemon(cfg)
+	ctx = config.WithContext(ctx, cfg)
+	d, err := NewGitDaemon(ctx)
 	if err != nil {
 		log.Fatal(err)
 	}
-	ctx := context.TODO()
-	fb, err := sqlite.NewSqliteBackend(ctx, cfg)
+	fb, err := sqlite.NewSqliteBackend(ctx)
 	if err != nil {
 		log.Fatal(err)
 	}

server/git/git.go 🔗

@@ -12,6 +12,7 @@ import (
 
 	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/git"
+	"github.com/charmbracelet/soft-serve/server/config"
 	"github.com/go-git/go-git/v5/plumbing/format/pktline"
 	"golang.org/x/sync/errgroup"
 )
@@ -78,12 +79,16 @@ func ReceivePack(ctx context.Context, in io.Reader, out io.Writer, er io.Writer,
 
 // RunGit runs a git command in the given repo.
 func RunGit(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, dir string, envs []string, args ...string) error {
-	logger := log.WithPrefix("server.git")
+	cfg := config.FromContext(ctx)
+	logger := log.FromContext(ctx).WithPrefix("rungit")
 	c := exec.CommandContext(ctx, "git", args...)
 	c.Dir = dir
 	c.Env = append(c.Env, envs...)
-	c.Env = append(c.Env, "SOFT_SERVE_DEBUG="+os.Getenv("SOFT_SERVE_DEBUG"))
 	c.Env = append(c.Env, "PATH="+os.Getenv("PATH"))
+	c.Env = append(c.Env, "SOFT_SERVE_DEBUG="+os.Getenv("SOFT_SERVE_DEBUG"))
+	if cfg != nil {
+		c.Env = append(c.Env, "SOFT_SERVE_LOG_FORMAT="+cfg.LogFormat)
+	}
 
 	stdin, err := c.StdinPipe()
 	if err != nil {

server/jobs.go 🔗

@@ -5,7 +5,6 @@ import (
 	"path/filepath"
 
 	"github.com/charmbracelet/soft-serve/git"
-	"github.com/charmbracelet/soft-serve/server/config"
 )
 
 var (
@@ -15,9 +14,10 @@ var (
 )
 
 // mirrorJob runs the (pull) mirror job task.
-func mirrorJob(cfg *config.Config) func() {
+func (s *Server) mirrorJob() func() {
+	cfg := s.Config
 	b := cfg.Backend
-	logger := logger.WithPrefix("server.mirrorJob")
+	logger := s.logger
 	return func() {
 		repos, err := b.Repositories()
 		if err != nil {

server/server.go 🔗

@@ -20,10 +20,6 @@ import (
 	"golang.org/x/sync/errgroup"
 )
 
-var (
-	logger = log.WithPrefix("server")
-)
-
 // Server is the Soft Serve server.
 type Server struct {
 	SSHServer   *sshsrv.SSHServer
@@ -33,7 +29,9 @@ type Server struct {
 	Cron        *cron.CronScheduler
 	Config      *config.Config
 	Backend     backend.Backend
-	ctx         context.Context
+
+	logger *log.Logger
+	ctx    context.Context
 }
 
 // NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
@@ -41,10 +39,12 @@ type Server struct {
 // key can be provided with authKey. If authKey is provided, access will be
 // restricted to that key. If authKey is not provided, the server will be
 // publicly writable until configured otherwise by cloning the `config` repo.
-func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) {
+func NewServer(ctx context.Context) (*Server, error) {
+	cfg := config.FromContext(ctx)
+
 	var err error
 	if cfg.Backend == nil {
-		sb, err := sqlite.NewSqliteBackend(ctx, cfg)
+		sb, err := sqlite.NewSqliteBackend(ctx)
 		if err != nil {
 			return nil, fmt.Errorf("create backend: %w", err)
 		}
@@ -56,28 +56,29 @@ func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) {
 		Cron:    cron.NewCronScheduler(ctx),
 		Config:  cfg,
 		Backend: cfg.Backend,
+		logger:  log.FromContext(ctx).WithPrefix("server"),
 		ctx:     ctx,
 	}
 
 	// Add cron jobs.
-	srv.Cron.AddFunc(jobSpecs["mirror"], mirrorJob(cfg))
+	srv.Cron.AddFunc(jobSpecs["mirror"], srv.mirrorJob())
 
-	srv.SSHServer, err = sshsrv.NewSSHServer(cfg)
+	srv.SSHServer, err = sshsrv.NewSSHServer(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("create ssh server: %w", err)
 	}
 
-	srv.GitDaemon, err = daemon.NewGitDaemon(cfg)
+	srv.GitDaemon, err = daemon.NewGitDaemon(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("create git daemon: %w", err)
 	}
 
-	srv.HTTPServer, err = web.NewHTTPServer(cfg)
+	srv.HTTPServer, err = web.NewHTTPServer(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("create http server: %w", err)
 	}
 
-	srv.StatsServer, err = stats.NewStatsServer(cfg)
+	srv.StatsServer, err = stats.NewStatsServer(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("create stats server: %w", err)
 	}
@@ -101,38 +102,37 @@ func start(ctx context.Context, fn func() error) error {
 
 // Start starts the SSH server.
 func (s *Server) Start() error {
-	logger := log.FromContext(s.ctx).WithPrefix("server")
 	errg, ctx := errgroup.WithContext(s.ctx)
 	errg.Go(func() error {
-		logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
+		s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
 		if err := start(ctx, s.GitDaemon.Start); !errors.Is(err, daemon.ErrServerClosed) {
 			return err
 		}
 		return nil
 	})
 	errg.Go(func() error {
-		logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
+		s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
 		if err := start(ctx, s.HTTPServer.ListenAndServe); !errors.Is(err, http.ErrServerClosed) {
 			return err
 		}
 		return nil
 	})
 	errg.Go(func() error {
-		logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
+		s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
 		if err := start(ctx, s.SSHServer.ListenAndServe); !errors.Is(err, ssh.ErrServerClosed) {
 			return err
 		}
 		return nil
 	})
 	errg.Go(func() error {
-		logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
+		s.logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
 		if err := start(ctx, s.StatsServer.ListenAndServe); !errors.Is(err, http.ErrServerClosed) {
 			return err
 		}
 		return nil
 	})
 	errg.Go(func() error {
-		logger.Print("Starting cron scheduler")
+		s.logger.Print("Starting cron scheduler")
 		s.Cron.Start()
 		return nil
 	})

server/server_test.go 🔗

@@ -25,10 +25,11 @@ func setupServer(tb testing.TB) (*Server, *config.Config, string) {
 	tb.Setenv("SOFT_SERVE_INITIAL_ADMIN_KEY", authorizedKey(pub))
 	tb.Setenv("SOFT_SERVE_SSH_LISTEN_ADDR", sshPort)
 	tb.Setenv("SOFT_SERVE_GIT_LISTEN_ADDR", fmt.Sprintf(":%d", test.RandomPort()))
+	ctx := context.TODO()
 	cfg := config.DefaultConfig()
+	ctx = config.WithContext(ctx, cfg)
 	tb.Log("configuring server")
-	ctx := context.TODO()
-	s, err := NewServer(ctx, cfg)
+	s, err := NewServer(ctx)
 	if err != nil {
 		tb.Fatal(err)
 	}

server/ssh/session_test.go 🔗

@@ -59,7 +59,8 @@ func setup(tb testing.TB) (*gossh.Session, func() error) {
 	})
 	ctx := context.TODO()
 	cfg := config.DefaultConfig()
-	fb, err := sqlite.NewSqliteBackend(ctx, cfg)
+	ctx = config.WithContext(ctx, cfg)
+	fb, err := sqlite.NewSqliteBackend(ctx)
 	if err != nil {
 		log.Fatal(err)
 	}

server/ssh/ssh.go 🔗

@@ -26,10 +26,6 @@ import (
 	gossh "golang.org/x/crypto/ssh"
 )
 
-var (
-	logger = log.WithPrefix("server.ssh")
-)
-
 var (
 	publicKeyCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 		Namespace: "soft_serve",
@@ -76,15 +72,22 @@ var (
 
 // SSHServer is a SSH server that implements the git protocol.
 type SSHServer struct {
-	srv *ssh.Server
-	cfg *config.Config
+	srv    *ssh.Server
+	cfg    *config.Config
+	ctx    context.Context
+	logger *log.Logger
 }
 
 // NewSSHServer returns a new SSHServer.
-func NewSSHServer(cfg *config.Config) (*SSHServer, error) {
+func NewSSHServer(ctx context.Context) (*SSHServer, error) {
+	cfg := config.FromContext(ctx)
 	var err error
-	s := &SSHServer{cfg: cfg}
-	logger := logger.StandardLog(log.StandardLogOptions{ForceLevel: log.DebugLevel})
+	s := &SSHServer{
+		cfg:    cfg,
+		ctx:    ctx,
+		logger: log.FromContext(ctx).WithPrefix("ssh"),
+	}
+	logger := s.logger.StandardLog(log.StandardLogOptions{ForceLevel: log.DebugLevel})
 	mw := []wish.Middleware{
 		rm.MiddlewareWithLogger(
 			logger,
@@ -151,7 +154,7 @@ func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) (allowed
 	}(&allowed)
 
 	ac := s.cfg.Backend.AccessLevelByPublicKey("", pk)
-	logger.Debugf("access level for %q: %s", ak, ac)
+	s.logger.Debugf("access level for %q: %s", ak, ac)
 	allowed = ac >= backend.ReadOnlyAccess
 	return
 }
@@ -168,7 +171,7 @@ func (s *SSHServer) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.Keyboard
 // checked for access on a per repo basis for a ssh.Session public key.
 // Hooks.Push and Hooks.Fetch will be called on successful completion of
 // their commands.
-func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
+func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 	return func(sh ssh.Handler) ssh.Handler {
 		return func(s ssh.Session) {
 			func() {
@@ -196,7 +199,7 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 						"SOFT_SERVE_PUBLIC_KEY=" + ak,
 					}
 
-					logger.Debug("git middleware", "cmd", gc, "access", access.String())
+					ss.logger.Debug("git middleware", "cmd", gc, "access", access.String())
 					repoDir := filepath.Join(reposDir, repo)
 					switch gc {
 					case git.ReceivePackBin:

server/stats/stats.go 🔗

@@ -11,15 +11,18 @@ import (
 
 // StatsServer is a server for collecting and reporting statistics.
 type StatsServer struct {
+	ctx    context.Context
 	cfg    *config.Config
 	server *http.Server
 }
 
 // NewStatsServer returns a new StatsServer.
-func NewStatsServer(cfg *config.Config) (*StatsServer, error) {
+func NewStatsServer(ctx context.Context) (*StatsServer, error) {
+	cfg := config.FromContext(ctx)
 	mux := http.NewServeMux()
 	mux.Handle("/metrics", promhttp.Handler())
 	return &StatsServer{
+		ctx: ctx,
 		cfg: cfg,
 		server: &http.Server{
 			Addr:              cfg.Stats.ListenAddr,

server/web/http.go 🔗

@@ -24,10 +24,6 @@ import (
 	"goji.io/pattern"
 )
 
-var (
-	logger = log.WithPrefix("server.web")
-)
-
 var (
 	gitHttpCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 		Namespace: "soft_serve",
@@ -64,18 +60,17 @@ func (r *logWriter) WriteHeader(code int) {
 	r.ResponseWriter.WriteHeader(code)
 }
 
-func loggingMiddleware(next http.Handler) http.Handler {
-	logger := logger.WithPrefix("server.http")
+func (s *HTTPServer) loggingMiddleware(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		start := time.Now()
 		writer := &logWriter{code: http.StatusOK, ResponseWriter: w}
-		logger.Debug("request",
+		s.logger.Debug("request",
 			"method", r.Method,
 			"uri", r.RequestURI,
 			"addr", r.RemoteAddr)
 		next.ServeHTTP(writer, r)
 		elapsed := time.Since(start)
-		logger.Debug("response",
+		s.logger.Debug("response",
 			"status", fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code)),
 			"bytes", humanize.Bytes(uint64(writer.bytes)),
 			"time", elapsed)
@@ -84,15 +79,20 @@ func loggingMiddleware(next http.Handler) http.Handler {
 
 // HTTPServer is an http server.
 type HTTPServer struct {
+	ctx        context.Context
 	cfg        *config.Config
 	server     *http.Server
 	dirHandler http.Handler
+	logger     *log.Logger
 }
 
-func NewHTTPServer(cfg *config.Config) (*HTTPServer, error) {
+func NewHTTPServer(ctx context.Context) (*HTTPServer, error) {
+	cfg := config.FromContext(ctx)
 	mux := goji.NewMux()
 	s := &HTTPServer{
+		ctx:        ctx,
 		cfg:        cfg,
+		logger:     log.FromContext(ctx).WithPrefix("http"),
 		dirHandler: http.FileServer(http.Dir(filepath.Join(cfg.DataPath, "repos"))),
 		server: &http.Server{
 			Addr:              cfg.HTTP.ListenAddr,
@@ -104,7 +104,7 @@ func NewHTTPServer(cfg *config.Config) (*HTTPServer, error) {
 		},
 	}
 
-	mux.Use(loggingMiddleware)
+	mux.Use(s.loggingMiddleware)
 	for _, m := range []Matcher{
 		getInfoRefs,
 		getHead,
@@ -306,7 +306,7 @@ func (s *HTTPServer) handleGit(w http.ResponseWriter, r *http.Request) {
 	repo := pat.Param(r, "repo")
 	repo = utils.SanitizeRepo(repo) + ".git"
 	if _, err := s.cfg.Backend.Repository(repo); err != nil {
-		logger.Debug("repository not found", "repo", repo, "err", err)
+		s.logger.Debug("repository not found", "repo", repo, "err", err)
 		http.NotFound(w, r)
 		return
 	}

ui/common/common.go 🔗

@@ -3,6 +3,7 @@ package common
 import (
 	"context"
 
+	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/server/config"
 	"github.com/charmbracelet/soft-serve/ui/keymap"
@@ -30,6 +31,7 @@ type Common struct {
 	KeyMap        *keymap.KeyMap
 	Zone          *zone.Manager
 	Output        *termenv.Output
+	Logger        *log.Logger
 }
 
 // NewCommon returns a new Common struct.
@@ -45,6 +47,7 @@ func NewCommon(ctx context.Context, out *termenv.Output, width, height int) Comm
 		Styles: styles.DefaultStyles(),
 		KeyMap: keymap.DefaultKeyMap(),
 		Zone:   zone.New(),
+		Logger: log.FromContext(ctx).WithPrefix("ui"),
 	}
 }
 

ui/pages/repo/log.go 🔗

@@ -393,7 +393,7 @@ func (l *Log) countCommitsCmd() tea.Msg {
 	}
 	count, err := r.CountCommits(l.ref)
 	if err != nil {
-		logger.Debugf("ui: error counting commits: %v", err)
+		l.common.Logger.Debugf("ui: error counting commits: %v", err)
 		return common.ErrorMsg(err)
 	}
 	return LogCountMsg(count)
@@ -423,7 +423,7 @@ func (l *Log) updateCommitsCmd() tea.Msg {
 	// CommitsByPage pages start at 1
 	cc, err := r.CommitsByPage(l.ref, page+1, limit)
 	if err != nil {
-		logger.Debugf("ui: error loading commits: %v", err)
+		l.common.Logger.Debugf("ui: error loading commits: %v", err)
 		return common.ErrorMsg(err)
 	}
 	for i, c := range cc {
@@ -445,12 +445,12 @@ func (l *Log) selectCommitCmd(commit *git.Commit) tea.Cmd {
 func (l *Log) loadDiffCmd() tea.Msg {
 	r, err := l.repo.Open()
 	if err != nil {
-		logger.Debugf("ui: error loading diff repository: %v", err)
+		l.common.Logger.Debugf("ui: error loading diff repository: %v", err)
 		return common.ErrorMsg(err)
 	}
 	diff, err := r.Diff(l.selectedCommit)
 	if err != nil {
-		logger.Debugf("ui: error loading diff: %v", err)
+		l.common.Logger.Debugf("ui: error loading diff: %v", err)
 		return common.ErrorMsg(err)
 	}
 	return LogDiffMsg(diff)

ui/pages/repo/refs.go 🔗

@@ -181,7 +181,7 @@ func (r *Refs) updateItemsCmd() tea.Msg {
 	}
 	refs, err := rr.References()
 	if err != nil {
-		logger.Debugf("ui: error getting references: %v", err)
+		r.common.Logger.Debugf("ui: error getting references: %v", err)
 		return common.ErrorMsg(err)
 	}
 	for _, ref := range refs {
@@ -228,10 +228,8 @@ func UpdateRefCmd(repo backend.Repository) tea.Cmd {
 		}
 		ref, err := r.HEAD()
 		if err != nil {
-			logger.Debugf("ui: error getting HEAD reference: %v", err)
 			return common.ErrorMsg(err)
 		}
-		logger.Debugf("HEAD: %s", ref.Name())
 		return RefMsg(ref)
 	}
 }

ui/pages/repo/repo.go 🔗

@@ -8,7 +8,6 @@ import (
 	"github.com/charmbracelet/bubbles/spinner"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/server/backend"
 	"github.com/charmbracelet/soft-serve/ui/common"
@@ -17,10 +16,6 @@ import (
 	"github.com/charmbracelet/soft-serve/ui/components/tabs"
 )
 
-var (
-	logger = log.WithPrefix("ui.repo")
-)
-
 type state int
 
 const (
@@ -92,6 +87,7 @@ func New(c common.Common) *Repo {
 	for i, t := range []tab{readmeTab, filesTab, commitsTab, branchesTab, tagsTab} {
 		ts[i] = t.String()
 	}
+	c.Logger = c.Logger.WithPrefix("ui.repo")
 	tb := tabs.New(c, ts)
 	readme := NewReadme(c)
 	log := NewLog(c)

ui/pages/selection/selection.go 🔗

@@ -8,7 +8,6 @@ import (
 	"github.com/charmbracelet/bubbles/list"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/server/backend"
 	"github.com/charmbracelet/soft-serve/ui/common"
 	"github.com/charmbracelet/soft-serve/ui/components/code"
@@ -20,10 +19,6 @@ const (
 	defaultNoContent = "No readme found.\n\nCreate a `.soft-serve` repository and add a `README.md` file to display readme."
 )
 
-var (
-	logger = log.WithPrefix("ui.selection")
-)
-
 type pane int
 
 const (
@@ -219,7 +214,7 @@ func (s *Selection) Init() tea.Cmd {
 		if al >= backend.ReadOnlyAccess {
 			item, err := NewItem(r, cfg)
 			if err != nil {
-				logger.Debugf("ui: failed to create item for %s: %v", r.Name(), err)
+				s.common.Logger.Debugf("ui: failed to create item for %s: %v", r.Name(), err)
 				continue
 			}
 			sortedItems = append(sortedItems, item)

ui/ui.go 🔗

@@ -7,7 +7,6 @@ import (
 	"github.com/charmbracelet/bubbles/list"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/server/backend"
 	"github.com/charmbracelet/soft-serve/ui/common"
 	"github.com/charmbracelet/soft-serve/ui/components/footer"
@@ -17,10 +16,6 @@ import (
 	"github.com/charmbracelet/soft-serve/ui/pages/selection"
 )
 
-var (
-	logger = log.WithPrefix("ui")
-)
-
 type page int
 
 const (
@@ -165,7 +160,7 @@ func (ui *UI) IsFiltering() bool {
 
 // Update implements tea.Model.
 func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	logger.Debugf("msg received: %T", msg)
+	ui.common.Logger.Debugf("msg received: %T", msg)
 	cmds := make([]tea.Cmd, 0)
 	switch msg := msg.(type) {
 	case tea.WindowSizeMsg:
@@ -295,7 +290,7 @@ func (ui *UI) openRepo(rn string) (backend.Repository, error) {
 	}
 	repos, err := cfg.Backend.Repositories()
 	if err != nil {
-		logger.Debugf("ui: failed to list repos: %v", err)
+		ui.common.Logger.Debugf("ui: failed to list repos: %v", err)
 		return nil, err
 	}
 	for _, r := range repos {