Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions cmd/leafwiki/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func writeUsage(w io.Writer) {
leafwiki --help

Options:
--log-format Log output format: text or json (default: text)
--host Host/IP address to bind the server to (default: 127.0.0.1)
--port Port to run the server on (default: 8080)
--unix-socket Path to a unix domain socket to listen on (overrides --host and --port)
Expand Down Expand Up @@ -129,6 +130,7 @@ func writeUsage(w io.Writer) {
LEAFWIKI_JWT_SECRET
LEAFWIKI_TOTP_ENCRYPTION_KEY
LEAFWIKI_LOG_LEVEL
LEAFWIKI_LOG_FORMAT
LEAFWIKI_ADMIN_PASSWORD
LEAFWIKI_ADMIN_USERNAME
LEAFWIKI_ADMIN_EMAIL
Expand Down Expand Up @@ -180,7 +182,7 @@ func printUsage() {
writeUsage(os.Stdout)
}

func setupLogger() {
func setupLogger(w io.Writer, format string) {
level := slog.LevelInfo
switch os.Getenv("LEAFWIKI_LOG_LEVEL") {
case "debug":
Expand All @@ -191,10 +193,17 @@ func setupLogger() {
level = slog.LevelWarn
}

handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
opts := &slog.HandlerOptions{
Level: level,
AddSource: true,
})
}

var handler slog.Handler
if format == "json" {
handler = slog.NewJSONHandler(w, opts)
} else {
handler = slog.NewTextHandler(w, opts)
}

slog.SetDefault(slog.New(handler))
}
Expand All @@ -207,6 +216,7 @@ func fail(msg string, args ...any) {
var gracefulShutdownTimeout = 10 * time.Second

type cliFlags struct {
logFormat *string
host *string
port *string
unixSocket *string
Expand Down Expand Up @@ -259,6 +269,7 @@ type cliFlags struct {

func registerFlags(fs *flag.FlagSet) *cliFlags {
return &cliFlags{
logFormat: fs.String("log-format", "", "log output format: text or json (default: text)"),
host: fs.String("host", "", "host/IP address to bind the server to (e.g. 127.0.0.1 or 0.0.0.0)"),
port: fs.String("port", "", "port to run the server on"),
unixSocket: fs.String("unix-socket", "", "path to a unix domain socket to listen on; overrides --host and --port"),
Expand Down Expand Up @@ -311,7 +322,6 @@ func registerFlags(fs *flag.FlagSet) *cliFlags {
}

func main() {
setupLogger()
exitCode := 0
defer func() {
if exitCode != 0 {
Expand All @@ -330,6 +340,9 @@ func main() {
visited := map[string]bool{}
flag.Visit(func(f *flag.Flag) { visited[f.Name] = true })

logFormat := resolveLogFormat("log-format", *flags.logFormat, visited, "LEAFWIKI_LOG_FORMAT", "text")
setupLogger(os.Stdout, logFormat)

host := resolveString("host", *flags.host, visited, "LEAFWIKI_HOST", "127.0.0.1")
port := resolveString("port", *flags.port, visited, "LEAFWIKI_PORT", "8080")
unixSocket := resolveString("unix-socket", *flags.unixSocket, visited, "LEAFWIKI_UNIX_SOCKET", "")
Expand Down Expand Up @@ -798,6 +811,34 @@ func resolveString(flagName, flagVal string, visited map[string]bool, envVar str
return def
}

// CLI > ENV > default
func resolveLogFormat(flagName, flagVal string, visited map[string]bool, envVar string, def string) string {
if visited[flagName] {
if f, ok := parseLogFormat(flagVal); ok {
return f
}
fail("Invalid flag value", "flag", flagName, "value", flagVal, "expected", "text or json")
}
if env := strings.TrimSpace(os.Getenv(envVar)); env != "" {
if f, ok := parseLogFormat(env); ok {
return f
}
// If env var is set but invalid, fail fast (helps operators)
fail(errInvalidEnvVarValue, "variable", envVar, "value", env, "expected", "text or json")
}
return def
}

func parseLogFormat(s string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "text":
return "text", true
case "json":
return "json", true
}
return "", false
}

// CLI > ENV > default(flag)
func resolveBool(flagName string, flagVal bool, visited map[string]bool, envVar string) bool {
if visited[flagName] {
Expand Down
74 changes: 74 additions & 0 deletions cmd/leafwiki/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"io"
"log/slog"
"net"
"net/http"
"os"
Expand Down Expand Up @@ -36,7 +38,9 @@ func TestWriteUsage_UsesLongFlags(t *testing.T) {
"--metrics-port",
"--data-dir",
"--unix-socket",
"--log-format",
"LEAFWIKI_UNIX_SOCKET",
"LEAFWIKI_LOG_FORMAT",
"LEAFWIKI_ADMIN_USERNAME",
"LEAFWIKI_ADMIN_EMAIL",
"LEAFWIKI_ENABLE_METRICS",
Expand Down Expand Up @@ -201,6 +205,76 @@ func TestResolveString_TrimsCLIFlagValue(t *testing.T) {
}
}

func TestResolveLogFormat_Precedence(t *testing.T) {
tests := []struct {
name string
flagVal string
visited bool
envVal string
wantForm string
}{
{
name: "neither set falls back to default",
wantForm: "text",
},
{
name: "env var sets json",
envVal: "json",
wantForm: "json",
},
{
name: "env var is case-insensitive",
envVal: "JSON",
wantForm: "json",
},
{
name: "cli flag overrides env var",
flagVal: "text",
visited: true,
envVal: "json",
wantForm: "text",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("LEAFWIKI_LOG_FORMAT", tc.envVal)
visited := map[string]bool{}
if tc.visited {
visited["log-format"] = true
}
got := resolveLogFormat("log-format", tc.flagVal, visited, "LEAFWIKI_LOG_FORMAT", "text")
if got != tc.wantForm {
t.Fatalf("resolveLogFormat() = %q, want %q", got, tc.wantForm)
}
})
}
}

func TestSetupLogger_SelectsHandlerByFormat(t *testing.T) {
t.Run("text format writes non-JSON output", func(t *testing.T) {
var buf bytes.Buffer
setupLogger(&buf, "text")
slog.Default().Info("hello")

if json.Valid(buf.Bytes()) {
t.Fatalf("expected non-JSON text output, got %q", buf.String())
}
if !strings.Contains(buf.String(), "msg=hello") {
t.Fatalf("expected text output to contain msg=hello, got %q", buf.String())
}
})

t.Run("json format writes valid JSON output", func(t *testing.T) {
var buf bytes.Buffer
setupLogger(&buf, "json")
slog.Default().Info("hello")

if !json.Valid(buf.Bytes()) {
t.Fatalf("expected valid JSON output, got %q", buf.String())
}
})
}

func TestValidateListenConfig(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading