v2.31.0-dev10 f49cffc

writer.go RawHistoryFolder

1// Package accesslog provides a dedicated HTTP access log writer for security
2// and observability tooling. It supports JSON, CLF, and Combined formats with
3// stdout, file, and journald output backends.
4package accesslog
5
6import (
7	"fmt"
8	"os"
9	"path/filepath"
10	"sync"
11
12	"gopkg.in/natefinch/lumberjack.v2"
13
14	out "github.com/bnema/gordon/internal/boundaries/out"
15)
16
17// Config holds configuration for the access log writer.
18type Config struct {
19	// Format is the output format: "json", "clf", or "combined".
20	Format string
21	// Output is the output backend: "stdout", "file", or "journald".
22	Output string
23	// FilePath is the path to the log file (only used when Output == "file").
24	// If empty, defaults to {data_dir}/logs/access.log (caller must resolve).
25	FilePath string
26	// MaxSize is the maximum size in MB before rotation (only used when Output == "file").
27	MaxSize int
28	// MaxBackups is the number of old log files to retain (only used when Output == "file").
29	MaxBackups int
30	// MaxAge is the maximum number of days to retain old log files (only used when Output == "file").
31	MaxAge int
32	// SyslogIdentifier is the journald SYSLOG_IDENTIFIER (only used when Output == "journald").
33	SyslogIdentifier string
34}
35
36// sink is the internal interface for output backends.
37type sink interface {
38	WriteLine(line string) error
39	Close() error
40}
41
42// Writer serializes access log entries and writes them to a configured sink.
43// It is safe for concurrent use. It implements out.AccessLogWriter.
44type Writer struct {
45	formatter func(out.AccessLogEntry) (string, error)
46	sink      sink
47	mu        sync.Mutex
48}
49
50// New creates a new Writer from cfg. Returns an error if the configuration is
51// invalid or the output backend cannot be initialized.
52func New(cfg Config) (*Writer, error) {
53	formatter, err := newFormatter(cfg.Format)
54	if err != nil {
55		return nil, fmt.Errorf("failed to create formatter: %w", err)
56	}
57
58	s, err := newSink(cfg)
59	if err != nil {
60		return nil, fmt.Errorf("failed to create sink: %w", err)
61	}
62
63	return &Writer{
64		formatter: formatter,
65		sink:      s,
66	}, nil
67}
68
69// Write serializes entry and writes it to the configured output. It is safe
70// for concurrent use. A write failure is returned as an error; callers should
71// log the error and continue — Write never panics.
72func (w *Writer) Write(entry out.AccessLogEntry) error {
73	line, err := w.formatter(entry)
74	if err != nil {
75		return fmt.Errorf("accesslog: format entry: %w", err)
76	}
77
78	w.mu.Lock()
79	defer w.mu.Unlock()
80
81	if err := w.sink.WriteLine(line); err != nil {
82		return fmt.Errorf("accesslog: write line: %w", err)
83	}
84	return nil
85}
86
87// Close releases any resources held by the writer (e.g. open file handles).
88func (w *Writer) Close() error {
89	w.mu.Lock()
90	defer w.mu.Unlock()
91	return w.sink.Close()
92}
93
94// newFormatter returns the format function for the given format name.
95func newFormatter(format string) (func(out.AccessLogEntry) (string, error), error) {
96	switch format {
97	case "json":
98		return formatJSON, nil
99	case "clf":
100		return formatCLF, nil
101	case "combined":
102		return formatCombined, nil
103	default:
104		return nil, fmt.Errorf("invalid logging.access_log.format: %q (must be json, clf, or combined)", format)
105	}
106}
107
108// newSink creates the output sink for the given config.
109func newSink(cfg Config) (sink, error) {
110	switch cfg.Output {
111	case "stdout", "":
112		return &stdoutSink{}, nil
113	case "file":
114		return newFileSink(cfg)
115	case "journald":
116		return newJournaldSink(cfg)
117	default:
118		return nil, fmt.Errorf("invalid logging.access_log.output: %q (must be stdout, file, or journald)", cfg.Output)
119	}
120}
121
122// stdoutSink writes lines to os.Stdout.
123type stdoutSink struct{}
124
125func (s *stdoutSink) WriteLine(line string) error {
126	_, err := fmt.Fprintln(os.Stdout, line)
127	return err
128}
129
130func (s *stdoutSink) Close() error { return nil }
131
132// fileSink writes lines to a rotating file via lumberjack.
133type fileSink struct {
134	logger *lumberjack.Logger
135}
136
137func newFileSink(cfg Config) (*fileSink, error) {
138	if cfg.FilePath == "" {
139		return nil, fmt.Errorf("accesslog: file output requires a non-empty file_path")
140	}
141
142	// Ensure parent directory exists.
143	if err := os.MkdirAll(filepath.Dir(cfg.FilePath), 0o700); err != nil {
144		return nil, fmt.Errorf("accesslog: create log directory: %w", err)
145	}
146
147	// lumberjack defaults when zero: MaxSize=100MB, MaxBackups=keep all, MaxAge=no limit.
148	// Config defaults in run.go set these to sensible values; zero here is valid.
149	return &fileSink{
150		logger: &lumberjack.Logger{
151			Filename:   cfg.FilePath,
152			MaxSize:    cfg.MaxSize,
153			MaxBackups: cfg.MaxBackups,
154			MaxAge:     cfg.MaxAge,
155			Compress:   true,
156		},
157	}, nil
158}
159
160func (s *fileSink) WriteLine(line string) error {
161	_, err := fmt.Fprintln(s.logger, line)
162	return err
163}
164
165func (s *fileSink) Close() error {
166	return s.logger.Close()
167}

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 7

Tags 106

↑↓ moveenter openesc close