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// Naming one turns on debug for its component.
26var categories = map[string]string{"input-motion": "input"}
27
28type debugKey struct{}
29type levelKey struct{}
30type debugSet map[string]bool
31
32func ParseDebug(value string) (map[string]bool, error) {
33 selected := make(debugSet)
34 if value == "" {
35 return selected, nil
36 }
37 for _, name := range strings.Split(value, ",") {
38 if name == "all" {
39 selected[name] = true
40 continue
41 }
42 if _, ok := categories[name]; !components[name] && !ok {
43 return nil, fmt.Errorf("invalid debug component %q", name)
44 }
45 selected[name] = true
46 }
47 return selected, nil
48}
49
50// keepRuns is how many per-run log files Open retains.
51const keepRuns = 20
52
53// Open creates runs/<timestamp>.log, points runs/latest.log at it, prunes old runs
54// and attaches the logger to ctx.
55// The caller must invoke close after all logging is complete.
56func Open(ctx context.Context, level, debug string) (context.Context, func() error, error) {
57 selected, err := ParseDebug(debug)
58 if err != nil {
59 return nil, nil, err
60 }
61 state := os.Getenv("XDG_STATE_HOME")
62 if state == "" {
63 home, err := os.UserHomeDir()
64 if err != nil {
65 return nil, nil, err
66 }
67 state = filepath.Join(home, ".local", "state")
68 }
69 dir := filepath.Join(state, "neferwl", "runs")
70 if err := os.MkdirAll(dir, 0o700); err != nil {
71 return nil, nil, err
72 }
73 name := time.Now().Format("20060102-150405.000") + ".log"
74 file, err := os.OpenFile(filepath.Join(dir, name), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
75 if err != nil {
76 return nil, nil, err
77 }
78 latest := filepath.Join(dir, "latest.log")
79 _ = os.Remove(latest)
80 if err := os.Symlink(name, latest); err != nil {
81 _ = file.Close()
82 return nil, nil, err
83 }
84 pruneRuns(dir)
85 var output = zerowrap.Config{Level: level, Format: "json", Output: file}
86 if term.IsTerminal(int(os.Stderr.Fd())) {
87 output.Output = &consoleAndFile{file: file}
88 }
89 ctx = zerowrap.WithCtx(ctx, zerowrap.New(output))
90 ctx = context.WithValue(ctx, debugKey{}, debugSet(selected))
91 ctx = context.WithValue(ctx, levelKey{}, level)
92 return ctx, file.Close, nil
93}
94
95func pruneRuns(dir string) {
96 runs, _ := filepath.Glob(filepath.Join(dir, "2*.log"))
97 sort.Strings(runs)
98 for len(runs) > keepRuns {
99 _ = os.Remove(runs[0])
100 runs = runs[1:]
101 }
102}
103
104type consoleAndFile struct{ file *os.File }
105
106func (w *consoleAndFile) Write(p []byte) (int, error) {
107 // JSON remains the canonical output; terminal users also see each entry.
108 _, _ = os.Stderr.Write(p)
109 return w.file.Write(p)
110}
111
112// Enabled reports whether a debug category was named in --debug.
113func Enabled(ctx context.Context, category string) bool {
114 selected, _ := ctx.Value(debugKey{}).(debugSet)
115 return selected[category]
116}
117
118// For returns a component logger with debug enabled only for selected components.
119func For(ctx context.Context, component string) zerowrap.Logger {
120 log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component)
121 selected, _ := ctx.Value(debugKey{}).(debugSet)
122 level := zerolog.InfoLevel
123 switch ctx.Value(levelKey{}) {
124 case "debug":
125 level = zerolog.DebugLevel
126 case "warn":
127 level = zerolog.WarnLevel
128 case "error":
129 level = zerolog.ErrorLevel
130 }
131 if selected["all"] || selected[component] {
132 level = zerolog.DebugLevel
133 }
134 for name, owner := range categories {
135 if selected[name] && owner == component {
136 level = zerolog.DebugLevel
137 }
138 }
139 log = zerowrap.Logger{Logger: log.Level(level)}
140 return log
141}