1// Package logging configures component-scoped zerowrap logging.
2package logging
3
4import (
5 "context"
6 "fmt"
7 "os"
8 "path/filepath"
9 "sort"
10 "strings"
11 "time"
12
13 "github.com/bnema/zerowrap"
14 "github.com/rs/zerolog"
15 "golang.org/x/term"
16)
17
18var components = map[string]bool{
19 "core": true, "wayland": true, "input": true, "drm": true, "seat": true,
20 "render": true, "sync": true, "config": true, "app": true,
21}
22
23// categories are debug switches narrower than a component, only on when
24// named: "all" leaves them off. input-motion logs every pointer motion,
25// input-keys every key (typed text), drm-flip every frame and commit
26// completion with its timing. Naming one turns on debug for its component.
27var categories = map[string]string{"input-motion": "input", "input-keys": "input", "drm-flip": "drm"}
28
29type debugKey struct{}
30type levelKey struct{}
31type debugSet map[string]bool
32
33func ParseDebug(value string) (map[string]bool, error) {
34 selected := make(debugSet)
35 if value == "" {
36 return selected, nil
37 }
38 for _, name := range strings.Split(value, ",") {
39 if name == "all" {
40 selected[name] = true
41 continue
42 }
43 if _, ok := categories[name]; !components[name] && !ok {
44 return nil, fmt.Errorf("invalid debug component %q", name)
45 }
46 selected[name] = true
47 }
48 return selected, nil
49}
50
51// keepRuns is how many per-run log files Open retains.
52const keepRuns = 20
53
54// Open creates runs/<backend>/<timestamp>.log, points latest.log in that
55// directory at it, prunes old runs of that backend and attaches the logger
56// to ctx. Each backend keeps its own runs: headless test runs never rotate
57// a DRM session's log away.
58// The caller must invoke close after all logging is complete.
59func Open(ctx context.Context, backend, level, debug string) (context.Context, func() error, error) {
60 selected, err := ParseDebug(debug)
61 if err != nil {
62 return nil, nil, err
63 }
64 state := os.Getenv("XDG_STATE_HOME")
65 if state == "" {
66 home, err := os.UserHomeDir()
67 if err != nil {
68 return nil, nil, err
69 }
70 state = filepath.Join(home, ".local", "state")
71 }
72 dir := filepath.Join(state, "neferwl", "runs", backend)
73 if err := os.MkdirAll(dir, 0o700); err != nil {
74 return nil, nil, err
75 }
76 name := time.Now().Format("20060102-150405.000") + ".log"
77 file, err := os.OpenFile(filepath.Join(dir, name), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
78 if err != nil {
79 return nil, nil, err
80 }
81 latest := filepath.Join(dir, "latest.log")
82 _ = os.Remove(latest)
83 if err := os.Symlink(name, latest); err != nil {
84 _ = file.Close()
85 return nil, nil, err
86 }
87 pruneRuns(dir)
88 var output = zerowrap.Config{Level: level, Format: "json", Output: file}
89 if term.IsTerminal(int(os.Stderr.Fd())) {
90 output.Output = &consoleAndFile{file: file}
91 }
92 ctx = zerowrap.WithCtx(ctx, zerowrap.New(output))
93 ctx = context.WithValue(ctx, debugKey{}, debugSet(selected))
94 ctx = context.WithValue(ctx, levelKey{}, level)
95 return ctx, file.Close, nil
96}
97
98func pruneRuns(dir string) {
99 runs, _ := filepath.Glob(filepath.Join(dir, "2*.log"))
100 sort.Strings(runs)
101 for len(runs) > keepRuns {
102 _ = os.Remove(runs[0])
103 runs = runs[1:]
104 }
105}
106
107type consoleAndFile struct{ file *os.File }
108
109func (w *consoleAndFile) Write(p []byte) (int, error) {
110 // JSON remains the canonical output; terminal users also see each entry.
111 _, _ = os.Stderr.Write(p)
112 return w.file.Write(p)
113}
114
115// Enabled reports whether a debug category was named in --debug.
116func Enabled(ctx context.Context, category string) bool {
117 selected, _ := ctx.Value(debugKey{}).(debugSet)
118 return selected[category]
119}
120
121// For returns a component logger with debug enabled only for selected components.
122func For(ctx context.Context, component string) zerowrap.Logger {
123 log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component)
124 selected, _ := ctx.Value(debugKey{}).(debugSet)
125 level := zerolog.InfoLevel
126 switch ctx.Value(levelKey{}) {
127 case "debug":
128 level = zerolog.DebugLevel
129 case "warn":
130 level = zerolog.WarnLevel
131 case "error":
132 level = zerolog.ErrorLevel
133 }
134 if selected["all"] || selected[component] {
135 level = zerolog.DebugLevel
136 }
137 for name, owner := range categories {
138 if selected[name] && owner == component {
139 level = zerolog.DebugLevel
140 }
141 }
142 log = zerowrap.Logger{Logger: log.Level(level)}
143 return log
144}