logging.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close