- config rules name their key; env strings overlay defaults; D-08 defaults - component floors, verbosity 1-3, quiet on the console only - daily rotation, retention, 0640 files, unwritable dir, lazy open, path confinement - redaction on every driver, console control-character stripping and prefix - loggers before Setup, Reset, HideOnTTY, 100 concurrent writers - testing/slogtest for the console handler and the JSON pipeline - sunscreen InstallDefault still installs a redacting logfmt handler
203 lines
7.9 KiB
Go
203 lines
7.9 KiB
Go
package diary_test
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/diary"
|
|
)
|
|
|
|
// TestConfigRules: every invalid value in config/log.yaml fails with an
|
|
// error naming its exact key; YAML lists and comma strings parse alike.
|
|
func TestConfigRules(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
yaml string
|
|
key string
|
|
}{
|
|
{"level", "level: loud\n", "log.level"},
|
|
{"default undefined", "default: nowhere\n", "log.default"},
|
|
{"default empty", "default: \"\"\n", "log.default"},
|
|
{"driver", "channels:\n daily:\n driver: syslog\n", "log.channels.daily.driver"},
|
|
{"driver missing", "channels:\n extra:\n level: info\n", "log.channels.extra.driver"},
|
|
{"format", "channels:\n console:\n format: xml\n", "log.channels.console.format"},
|
|
{"auto outside console", "channels:\n stderr:\n format: auto\n", "log.channels.stderr.format"},
|
|
{"channel level", "channels:\n daily:\n level: chatty\n", "log.channels.daily.level"},
|
|
{"days negative", "channels:\n daily:\n days: -3\n", "log.channels.daily.days"},
|
|
{"days fraction", "channels:\n daily:\n days: 1.5\n", "log.channels.daily.days"},
|
|
{"days text", "channels:\n daily:\n days: often\n", "log.channels.daily.days"},
|
|
{"unknown setting", "channels:\n daily:\n rotate: hourly\n", "log.channels.daily.rotate"},
|
|
{"channel not a map", "channels:\n daily: yes\n", "log.channels.daily"},
|
|
{"daily path missing", "channels:\n daily:\n path: \"\"\n", "log.channels.daily.path"},
|
|
{"path escapes", "channels:\n daily:\n path: ../outside/summer.log\n", "log.channels.daily.path"},
|
|
{"path escapes after clean", "channels:\n daily:\n path: storage/../../outside.log\n", "log.channels.daily.path"},
|
|
{"stack empty", "channels:\n stack:\n channels: []\n", "log.channels.stack.channels"},
|
|
{"stack child", "channels:\n stack:\n channels: [console, nowhere]\n", "log.channels.stack.channels"},
|
|
{"stack self", "channels:\n stack:\n channels: [console, stack]\n", "log.channels.stack.channels"},
|
|
{"stack cycle", "default: a\nchannels:\n a:\n driver: stack\n channels: [console, b]\n b:\n driver: stack\n channels: [a]\n", "log.channels.a.channels"},
|
|
{"component level", "components:\n river: chatty\n", "log.components.river"},
|
|
{"component nested", "components:\n golem15:\n blog: info\n", "log.components.golem15"},
|
|
{"components not a map", "components: warn\n", "log.components"},
|
|
{"channels not a map", "channels: console\n", "log.channels"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := diary.ParseConfig(openYAMLConfig(t, tc.yaml))
|
|
if err == nil {
|
|
t.Fatalf("ParseConfig accepted:\n%s", tc.yaml)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.key) {
|
|
t.Errorf("error %q does not name %s", err, tc.key)
|
|
}
|
|
})
|
|
}
|
|
|
|
// A cycle error names the path it found.
|
|
_, err := diary.ParseConfig(openYAMLConfig(t, cases[19].yaml))
|
|
if err == nil || !strings.Contains(err.Error(), "a -> b -> a") {
|
|
t.Errorf("cycle error = %v, want the path a -> b -> a", err)
|
|
}
|
|
|
|
// A YAML list, a comma string in YAML and a comma string from the
|
|
// environment give the same stack.
|
|
list, err := diary.ParseConfig(openYAMLConfig(t, "channels:\n stack:\n channels: [console, daily]\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
comma, err := diary.ParseConfig(openYAMLConfig(t, "channels:\n stack:\n channels: \" console , daily \"\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env, err := diary.ParseConfig(openConfig(t, "SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,daily"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(list, comma) || !reflect.DeepEqual(list, env) {
|
|
t.Errorf("list, comma and env stacks differ:\n list %+v\ncomma %+v\n env %+v", list.Channels["stack"], comma.Channels["stack"], env.Channels["stack"])
|
|
}
|
|
|
|
// Levels accept warning as warn and any case; days 0 keeps every file.
|
|
got, err := diary.ParseConfig(openYAMLConfig(t, "level: WARNING\nchannels:\n daily:\n days: 0\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Level != slog.LevelWarn || got.Channels["daily"].Days != 0 {
|
|
t.Errorf("level %v days %d, want WARN and 0", got.Level, got.Channels["daily"].Days)
|
|
}
|
|
|
|
// compass keeps a dotted YAML key literal, so it does not nest; only
|
|
// the nested form (above, or from SUMMER_LOG__ variables) is refused.
|
|
got, err = diary.ParseConfig(openYAMLConfig(t, "components:\n golem15.blog: debug\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Components["golem15.blog"] != slog.LevelDebug {
|
|
t.Errorf("literal dotted component = %v", got.Components)
|
|
}
|
|
}
|
|
|
|
// TestConfigEnvStrings: SUMMER_LOG__ overrides arrive as strings and
|
|
// overlay the defaults key by key, also on top of config/log.yaml.
|
|
func TestConfigEnvStrings(t *testing.T) {
|
|
got, err := diary.ParseConfig(openConfig(t,
|
|
"SUMMER_LOG__LEVEL=debug",
|
|
"SUMMER_LOG__CHANNELS__DAILY__LEVEL=error",
|
|
"SUMMER_LOG__CHANNELS__DAILY__DAYS=7",
|
|
"SUMMER_LOG__COMPONENTS__JOURNAL=debug",
|
|
))
|
|
if err != nil {
|
|
t.Fatalf("ParseConfig: %v", err)
|
|
}
|
|
want := diary.DefaultConfig()
|
|
want.Level = slog.LevelDebug
|
|
daily := want.Channels["daily"]
|
|
daily.Level, daily.HasLevel, daily.Days = slog.LevelError, true, 7
|
|
want.Channels["daily"] = daily
|
|
want.Components["journal"] = slog.LevelDebug
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("env overlay:\n got %+v\nwant %+v", got, want)
|
|
}
|
|
|
|
// The environment wins over the file, and keys the environment does
|
|
// not set keep the file's values.
|
|
got, err = diary.ParseConfig(openYAMLConfig(t,
|
|
"channels:\n daily:\n path: var/log/app.log\n days: 3\n",
|
|
"SUMMER_LOG__CHANNELS__DAILY__DAYS=9",
|
|
))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d := got.Channels["daily"]; d.Path != "var/log/app.log" || d.Days != 9 || d.Format != diary.FormatJSON || d.Level != slog.LevelWarn {
|
|
t.Errorf("daily channel = %+v, want path var/log/app.log, days 9 and the default format and level", d)
|
|
}
|
|
|
|
// A nil config gives the defaults.
|
|
none, err := diary.ParseConfig(nil)
|
|
if err != nil || !reflect.DeepEqual(none, diary.DefaultConfig()) {
|
|
t.Errorf("ParseConfig(nil) = %+v, %v; want the defaults", none, err)
|
|
}
|
|
}
|
|
|
|
// TestDefaultConfig: the defaults equal the D-08 example (components river
|
|
// warn), and the config/log.yaml that summer new writes parses to the
|
|
// defaults plus its http component.
|
|
func TestDefaultConfig(t *testing.T) {
|
|
example := `default: stack
|
|
level: info
|
|
channels:
|
|
stack:
|
|
driver: stack
|
|
channels: [console, daily]
|
|
console:
|
|
driver: console
|
|
format: auto
|
|
daily:
|
|
driver: daily
|
|
path: storage/logs/summer.log
|
|
days: 14
|
|
format: json
|
|
level: warn
|
|
components:
|
|
river: warn
|
|
`
|
|
got, err := diary.ParseConfig(openYAMLConfig(t, example))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(got, diary.DefaultConfig()) {
|
|
t.Errorf("D-08 example:\n got %+v\nwant %+v", got, diary.DefaultConfig())
|
|
}
|
|
def := diary.DefaultConfig()
|
|
if def.Default != "stack" || def.Level != slog.LevelInfo || def.Components["river"] != slog.LevelWarn || len(def.Components) != 1 {
|
|
t.Errorf("DefaultConfig head = %q %v %v", def.Default, def.Level, def.Components)
|
|
}
|
|
for _, name := range []string{"stack", "console", "daily", "stderr", "null"} {
|
|
if _, ok := def.Channels[name]; !ok {
|
|
t.Errorf("DefaultConfig lacks channel %q", name)
|
|
}
|
|
}
|
|
// The defaults are a fresh copy each time.
|
|
def.Components["river"] = slog.LevelDebug
|
|
if diary.DefaultConfig().Components["river"] != slog.LevelWarn {
|
|
t.Error("DefaultConfig returns shared maps")
|
|
}
|
|
|
|
generated, err := os.ReadFile(filepath.Join("..", "..", "internal", "build", "testdata", "new", "config__log.yaml.golden"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err = diary.ParseConfig(openYAMLConfig(t, string(generated)))
|
|
if err != nil {
|
|
t.Fatalf("generated config/log.yaml: %v", err)
|
|
}
|
|
want := diary.DefaultConfig()
|
|
want.Components["http"] = slog.LevelInfo
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("generated config/log.yaml:\n got %+v\nwant %+v", got, want)
|
|
}
|
|
}
|