v3.0.0-alpha.1 6fdddb8

writer_test.go RawHistoryFolder

1package accesslog
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"os"
8	"path/filepath"
9	"strings"
10	"sync"
11	"testing"
12	"time"
13
14	"github.com/stretchr/testify/assert"
15	"github.com/stretchr/testify/require"
16
17	out "github.com/bnema/gordon/internal/boundaries/out"
18)
19
20var sampleEntry = out.AccessLogEntry{
21	Time:       time.Date(2026, 4, 3, 14, 20, 54, 167_000_000, time.UTC),
22	ClientIP:   "10.0.0.1",
23	Method:     "POST",
24	Host:       "api.example.com",
25	Path:       "/api/v1/data",
26	Query:      "limit=10",
27	Status:     200,
28	BytesSent:  512,
29	DurationMS: 3.5,
30	UserAgent:  "curl/7.88",
31	Referer:    "https://example.com",
32	RequestID:  "abc123",
33	Proto:      "HTTP/2.0",
34}
35
36// --- New() validation ---
37
38func TestNew_InvalidFormat(t *testing.T) {
39	_, err := New(Config{Format: "xml", Output: "stdout"})
40	require.Error(t, err)
41	assert.Contains(t, err.Error(), "invalid logging.access_log.format")
42}
43
44func TestNew_InvalidOutput(t *testing.T) {
45	_, err := New(Config{Format: "json", Output: "kafka"})
46	require.Error(t, err)
47	assert.Contains(t, err.Error(), "invalid logging.access_log.output")
48}
49
50func TestNew_FileOutput_EmptyPath(t *testing.T) {
51	_, err := New(Config{Format: "json", Output: "file", FilePath: ""})
52	require.Error(t, err)
53}
54
55func TestNew_FileOutput_CreatesDirectory(t *testing.T) {
56	dir := t.TempDir()
57	path := filepath.Join(dir, "sub", "access.log")
58	w, err := New(Config{
59		Format:   "json",
60		Output:   "file",
61		FilePath: path,
62		MaxSize:  10,
63	})
64	require.NoError(t, err)
65	defer w.Close()
66
67	assert.DirExists(t, filepath.Join(dir, "sub"))
68}
69
70// --- stdout sink ---
71
72func TestWriter_Stdout_JSON(t *testing.T) {
73	// Replace stdout to capture output.
74	orig := os.Stdout
75	r, w, err := os.Pipe()
76	require.NoError(t, err)
77	os.Stdout = w
78
79	writer, err := New(Config{Format: "json", Output: "stdout"})
80	require.NoError(t, err)
81
82	require.NoError(t, writer.Write(sampleEntry))
83	require.NoError(t, writer.Close())
84
85	w.Close()
86	os.Stdout = orig
87
88	var buf bytes.Buffer
89	_, _ = buf.ReadFrom(r)
90	line := strings.TrimSpace(buf.String())
91
92	var got map[string]any
93	require.NoError(t, json.Unmarshal([]byte(line), &got), "output must be valid JSON: %s", line)
94	assert.Equal(t, "10.0.0.1", got["client_ip"])
95	assert.Equal(t, "POST", got["method"])
96	assert.InDelta(t, 200, got["status"], 0)
97}
98
99func TestWriter_Stdout_CLF(t *testing.T) {
100	orig := os.Stdout
101	r, w, err := os.Pipe()
102	require.NoError(t, err)
103	os.Stdout = w
104
105	writer, err := New(Config{Format: "clf", Output: "stdout"})
106	require.NoError(t, err)
107	require.NoError(t, writer.Write(sampleEntry))
108	require.NoError(t, writer.Close())
109
110	w.Close()
111	os.Stdout = orig
112
113	var buf bytes.Buffer
114	_, _ = buf.ReadFrom(r)
115	line := strings.TrimSpace(buf.String())
116
117	assert.True(t, strings.HasPrefix(line, "10.0.0.1 - - ["))
118	assert.Contains(t, line, `"POST /api/v1/data?limit=10 HTTP/2.0"`)
119	assert.Contains(t, line, " 200 512")
120}
121
122func TestWriter_Stdout_Combined(t *testing.T) {
123	orig := os.Stdout
124	r, w, err := os.Pipe()
125	require.NoError(t, err)
126	os.Stdout = w
127
128	writer, err := New(Config{Format: "combined", Output: "stdout"})
129	require.NoError(t, err)
130	require.NoError(t, writer.Write(sampleEntry))
131	require.NoError(t, writer.Close())
132
133	w.Close()
134	os.Stdout = orig
135
136	var buf bytes.Buffer
137	_, _ = buf.ReadFrom(r)
138	line := strings.TrimSpace(buf.String())
139
140	assert.Contains(t, line, `"https://example.com"`)
141	assert.Contains(t, line, `"curl/7.88"`)
142}
143
144// --- file sink ---
145
146func TestWriter_File_WritesLines(t *testing.T) {
147	dir := t.TempDir()
148	path := filepath.Join(dir, "access.log")
149
150	writer, err := New(Config{
151		Format:     "json",
152		Output:     "file",
153		FilePath:   path,
154		MaxSize:    10,
155		MaxBackups: 1,
156	})
157	require.NoError(t, err)
158
159	require.NoError(t, writer.Write(sampleEntry))
160	require.NoError(t, writer.Write(sampleEntry))
161	require.NoError(t, writer.Close())
162
163	data, err := os.ReadFile(path)
164	require.NoError(t, err)
165
166	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
167	assert.Len(t, lines, 2)
168	for _, l := range lines {
169		var obj map[string]any
170		assert.NoError(t, json.Unmarshal([]byte(l), &obj), "each line must be valid JSON")
171	}
172}
173
174// --- concurrency ---
175
176func TestWriter_ConcurrentWrites(t *testing.T) {
177	dir := t.TempDir()
178	path := filepath.Join(dir, "concurrent.log")
179
180	writer, err := New(Config{
181		Format:   "json",
182		Output:   "file",
183		FilePath: path,
184		MaxSize:  100,
185	})
186	require.NoError(t, err)
187	defer writer.Close()
188
189	const goroutines = 50
190	var wg sync.WaitGroup
191	wg.Add(goroutines)
192	for i := range goroutines {
193		go func(n int) {
194			defer wg.Done()
195			e := sampleEntry
196			e.RequestID = fmt.Sprintf("req-%d", n)
197			assert.NoError(t, writer.Write(e))
198		}(i)
199	}
200	wg.Wait()
201
202	data, err := os.ReadFile(path)
203	require.NoError(t, err)
204
205	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
206	assert.Len(t, lines, goroutines, "each goroutine should produce exactly one line")
207	for _, l := range lines {
208		var obj map[string]any
209		assert.NoError(t, json.Unmarshal([]byte(l), &obj), "each line must be valid JSON")
210	}
211}

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