printLoadedConf modified to only print explicitly set config values (via env or config file)

This commit is contained in:
bwilliams committed 2026-10-14 15:38:38 +01:00
1 parent d6688a6754
commit 803a8929e6
3 files changed
+119 -4

No files matched your search

+7 -3
View File
@@ -24,17 +24,21 @@ func printEnv() {
} }
} }
// printLoadedConfig logs every key Viper has loaded, with its value and type, // printLoadedConfig logs every key set by the config file or an environment
// at debug level. // variable (see isExplicitlySet), with its value and type, at debug level.
func printLoadedConfig() { func printLoadedConfig() {
slog.Debug("Loaded config:") slog.Debug("Loaded config:")
keys := viper.AllKeys() keys := viper.AllKeys()
for i := range keys { for i := range keys {
k := keys[i] k := keys[i]
if isExplicitlySet(k) {
v := viper.Get(k) v := viper.Get(k)
t := reflect.TypeOf(v) t := reflect.TypeOf(v)
slog.Debug( slog.Debug(
"\t" + k + " = " + fmt.Sprintf("%#v", v) + " <" + t.Kind().String() + ">") "\t" + k + " = " + fmt.Sprintf("%#v", v) +
" <" + t.Kind().String() + ">",
)
}
} }
} }
+24 -1
View File
@@ -3,12 +3,35 @@ package config
import ( import (
"fmt" "fmt"
"log/slog" "log/slog"
"os"
"strings" "strings"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// envKeyReplacer turns a dotted key into its environment variable's suffix
// (logging.level to LOGGING_LEVEL, once upper-cased).
var envKeyReplacer = strings.NewReplacer(".", "_")
// envVarName returns the environment variable that sets key, e.g.
// JOBSEARCH_LOGGING_LEVEL for logging.level.
func envVarName(key string) string {
return EnvPrefix + "_" + strings.ToUpper(envKeyReplacer.Replace(key))
}
// isExplicitlySet reports whether key's value in Viper was set by the config
// file or by its environment variable, rather than coming from its default.
// As Viper does, it ignores an empty environment variable. Values set with
// viper.Set (e.g. by StoreValue) are not counted.
func isExplicitlySet(key string) bool {
if viper.InConfig(key) {
return true
}
value, ok := os.LookupEnv(envVarName(key))
return ok && value != ""
}
// loadEnv loads environment variables from a .env file in the working // loadEnv loads environment variables from a .env file in the working
// directory. // directory.
func loadEnv() error { func loadEnv() error {
@@ -37,7 +60,7 @@ func loadConfig(cfgFile string) error {
LoadDefaultConfig() LoadDefaultConfig()
viper.SetEnvPrefix(EnvPrefix) viper.SetEnvPrefix(EnvPrefix)
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.SetEnvKeyReplacer(envKeyReplacer)
viper.AutomaticEnv() // read in environment variables that match viper.AutomaticEnv() // read in environment variables that match
err := viper.ReadInConfig() err := viper.ReadInConfig()
+88
View File
@@ -7,6 +7,8 @@ import (
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/spf13/viper"
) )
// initDir makes a temp working directory holding a .env file (if env is not // initDir makes a temp working directory holding a .env file (if env is not
@@ -167,3 +169,89 @@ func TestInitEnvOverride(t *testing.T) {
t.Errorf("FTSMock.Interval = %v; want 45s from the environment", conf.Sources.FTSMock.Interval) t.Errorf("FTSMock.Interval = %v; want 45s from the environment", conf.Sources.FTSMock.Interval)
} }
} }
func TestEnvVarName(t *testing.T) {
tests := map[string]string{
"logging.level": "JOBSEARCH_LOGGING_LEVEL",
"sources.fts_mock.interval": "JOBSEARCH_SOURCES_FTS_MOCK_INTERVAL",
"server.websocket.connection_limit.enabled": "JOBSEARCH_SERVER_WEBSOCKET_CONNECTION_LIMIT_ENABLED",
}
for key, want := range tests {
if got := envVarName(key); got != want {
t.Errorf("envVarName(%q) = %q; want %q", key, got, want)
}
}
}
// explicitConfigYAML sets a top-level and a nested key in a config file.
const explicitConfigYAML = `logging:
level: info
server:
websocket:
connection_limit:
enabled: true
`
// loadExplicit sets up Viper as Init does, with the defaults, the config
// file explicitConfigYAML and the JOBSEARCH_* environment variables.
func loadExplicit(t *testing.T) {
t.Helper()
resetViper(t)
LoadDefaultConfig()
viper.SetEnvPrefix(EnvPrefix)
viper.SetEnvKeyReplacer(envKeyReplacer)
viper.AutomaticEnv()
viper.SetConfigType("yaml")
if err := viper.ReadConfig(strings.NewReader(explicitConfigYAML)); err != nil {
t.Fatal(err)
}
}
// A key is explicitly set by the config file or a non-empty environment
// variable, not by its default or viper.Set.
func TestIsExplicitlySet(t *testing.T) {
t.Setenv("JOBSEARCH_DATABASE_FILE", "/tmp/js.db")
t.Setenv("JOBSEARCH_LOGGING_FILE", "")
loadExplicit(t)
viper.Set("sources.sleeper.enabled", true)
tests := map[string]bool{
"logging.level": true, // config file
"server.websocket.connection_limit.enabled": true, // nested, config file
"database.file": true, // environment
"logging.file": false, // empty environment variable
"sources.sleeper.interval": false, // default only
"sources.sleeper.enabled": false, // viper.Set
"server.websocket.connection_limit.burst": false, // default, beside a set key
}
for key, want := range tests {
if got := isExplicitlySet(key); got != want {
t.Errorf("isExplicitlySet(%q) = %v; want %v", key, got, want)
}
}
}
// printLoadedConfig logs only the keys set by the config file or the
// environment.
func TestPrintLoadedConfig(t *testing.T) {
t.Setenv("JOBSEARCH_DATABASE_FILE", "/tmp/js.db")
loadExplicit(t)
logs := captureLogs(t, slog.LevelDebug)
printLoadedConfig()
out := logs.String()
for _, want := range []string{
`logging.level = \"info\" <string>`,
`server.websocket.connection_limit.enabled = true <bool>`,
`database.file = \"/tmp/js.db\" <string>`,
} {
if !strings.Contains(out, want) {
t.Errorf("missing %q in:\n%s", want, out)
}
}
for _, unwanted := range []string{"sources.", "logging.file", "connection_limit.burst"} {
if strings.Contains(out, unwanted) {
t.Errorf("logged default key %q:\n%s", unwanted, out)
}
}
}