- stderr and null drivers; per-component levels from log.components - SUMMER_LOG__ overrides parse as strings and overlay the defaults key by key - Validation names the key: driver, format, level, days, stack children and cycles, daily path outside the root - summer new writes config/log.yaml (stack of console and daily, river warn, http info) - docs/services/logging.md: channels, config/log.yaml, component loggers, daily files, redaction - Log keys in the configuration docs; ExampleFor backs the docs snippet
136 lines
4.9 KiB
Go
136 lines
4.9 KiB
Go
package diary_test
|
|
|
|
import (
|
|
"bytes"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
"git.golem15.com/golem15/summercms/modules/diary"
|
|
)
|
|
|
|
// openConfig opens a compass config from an empty config directory with
|
|
// only the given environment.
|
|
func openConfig(t *testing.T, environ ...string) *compass.Config {
|
|
t.Helper()
|
|
dir := filepath.Join(t.TempDir(), "config")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: append([]string{}, environ...)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// TestConfigEnvSmoke: SUMMER_LOG__ overrides arrive as strings, parse, and
|
|
// overlay the framework defaults key by key; every invalid value fails with
|
|
// an error naming its key.
|
|
func TestConfigEnvSmoke(t *testing.T) {
|
|
cfg := openConfig(t,
|
|
"SUMMER_LOG__CHANNELS__DAILY__DAYS=30",
|
|
"SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,daily",
|
|
"SUMMER_LOG__COMPONENTS__RIVER=error",
|
|
)
|
|
got, err := diary.ParseConfig(cfg)
|
|
if err != nil {
|
|
t.Fatalf("ParseConfig: %v", err)
|
|
}
|
|
want := diary.DefaultConfig()
|
|
daily := want.Channels["daily"]
|
|
daily.Days = 30
|
|
want.Channels["daily"] = daily
|
|
want.Components["river"] = slog.LevelError
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("parsed config:\n got %+v\nwant %+v", got, want)
|
|
}
|
|
if ch := got.Channels["stack"].Channels; !reflect.DeepEqual(ch, []string{"console", "daily"}) {
|
|
t.Errorf("stack children = %q, want console, daily", ch)
|
|
}
|
|
|
|
errorCases := []struct {
|
|
name string
|
|
environ []string
|
|
key string
|
|
}{
|
|
{"level", []string{"SUMMER_LOG__LEVEL=loud"}, "log.level"},
|
|
{"default", []string{"SUMMER_LOG__DEFAULT=missing"}, "log.default"},
|
|
{"driver", []string{"SUMMER_LOG__CHANNELS__DAILY__DRIVER=syslog"}, "log.channels.daily.driver"},
|
|
{"driver required", []string{"SUMMER_LOG__CHANNELS__EXTRA__LEVEL=info"}, "log.channels.extra.driver"},
|
|
{"format", []string{"SUMMER_LOG__CHANNELS__CONSOLE__FORMAT=xml"}, "log.channels.console.format"},
|
|
{"auto outside console", []string{"SUMMER_LOG__CHANNELS__DAILY__FORMAT=auto"}, "log.channels.daily.format"},
|
|
{"channel level", []string{"SUMMER_LOG__CHANNELS__CONSOLE__LEVEL=verbose"}, "log.channels.console.level"},
|
|
{"days", []string{"SUMMER_LOG__CHANNELS__DAILY__DAYS=-1"}, "log.channels.daily.days"},
|
|
{"days text", []string{"SUMMER_LOG__CHANNELS__DAILY__DAYS=two"}, "log.channels.daily.days"},
|
|
{"path escapes root", []string{"SUMMER_LOG__CHANNELS__DAILY__PATH=../outside/summer.log"}, "log.channels.daily.path"},
|
|
{"stack child", []string{"SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,nowhere"}, "log.channels.stack.channels"},
|
|
{"stack cycle", []string{
|
|
"SUMMER_LOG__DEFAULT=a",
|
|
"SUMMER_LOG__CHANNELS__A__DRIVER=stack",
|
|
"SUMMER_LOG__CHANNELS__A__CHANNELS=console,b",
|
|
"SUMMER_LOG__CHANNELS__B__DRIVER=stack",
|
|
"SUMMER_LOG__CHANNELS__B__CHANNELS=a",
|
|
}, "log.channels.a.channels"},
|
|
{"component level", []string{"SUMMER_LOG__COMPONENTS__RIVER=chatty"}, "log.components.river"},
|
|
}
|
|
for _, tc := range errorCases {
|
|
_, err := diary.ParseConfig(openConfig(t, tc.environ...))
|
|
if err == nil {
|
|
t.Errorf("%s: ParseConfig accepted %q", tc.name, tc.environ)
|
|
continue
|
|
}
|
|
if !strings.Contains(err.Error(), tc.key) {
|
|
t.Errorf("%s: error %q does not name %s", tc.name, err, tc.key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDailyDegradesSmoke: a daily channel whose directory cannot be created
|
|
// warns once on the console, is disabled, and never fails Setup or stops
|
|
// later records from reaching the console.
|
|
func TestDailyDegradesSmoke(t *testing.T) {
|
|
if os.Geteuid() == 0 {
|
|
t.Skip("root ignores directory permissions")
|
|
}
|
|
root := t.TempDir()
|
|
readOnly := filepath.Join(root, "readonly")
|
|
if err := os.Mkdir(readOnly, 0o555); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chmod(readOnly, 0o755) })
|
|
|
|
cfg := openConfig(t, "SUMMER_LOG__CHANNELS__DAILY__PATH=readonly/logs/summer.log")
|
|
var buf bytes.Buffer
|
|
t.Cleanup(diary.Reset)
|
|
if err := diary.Setup(cfg, diary.Options{Root: root, Stderr: &buf, Color: bonfire.ColorNever}); err != nil {
|
|
t.Fatalf("Setup failed because of the log file: %v", err)
|
|
}
|
|
|
|
logger := diary.For("journal")
|
|
logger.Warn("first warning")
|
|
logger.Warn("second warning")
|
|
logger.Info("still logging")
|
|
|
|
out := buf.String()
|
|
if n := strings.Count(out, "daily log file disabled"); n != 1 {
|
|
t.Errorf("daily warning appears %d times, want 1:\n%s", n, out)
|
|
}
|
|
if !strings.Contains(out, filepath.Join(readOnly, "logs")) {
|
|
t.Errorf("warning does not name the path:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "component=diary") {
|
|
t.Errorf("warning does not come from component diary:\n%s", out)
|
|
}
|
|
for _, want := range []string{"first warning", "second warning", "still logging"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("console lacks %q after the daily channel failed:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|