v2.31.0-dev3 cbca441

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, _ := os.Pipe()
76	os.Stdout = w
77
78	writer, err := New(Config{Format: "json", Output: "stdout"})
79	require.NoError(t, err)
80
81	require.NoError(t, writer.Write(sampleEntry))
82	require.NoError(t, writer.Close())
83
84	w.Close()
85	os.Stdout = orig
86
87	var buf bytes.Buffer
88	_, _ = buf.ReadFrom(r)
89	line := strings.TrimSpace(buf.String())
90
91	var got map[string]any
92	require.NoError(t, json.Unmarshal([]byte(line), &got), "output must be valid JSON: %s", line)
93	assert.Equal(t, "10.0.0.1", got["client_ip"])
94	assert.Equal(t, "POST", got["method"])
95	assert.InDelta(t, 200, got["status"], 0)
96}
97
98func TestWriter_Stdout_CLF(t *testing.T) {
99	orig := os.Stdout
100	r, w, _ := os.Pipe()
101	os.Stdout = w
102
103	writer, err := New(Config{Format: "clf", Output: "stdout"})
104	require.NoError(t, err)
105	require.NoError(t, writer.Write(sampleEntry))
106	require.NoError(t, writer.Close())
107
108	w.Close()
109	os.Stdout = orig
110
111	var buf bytes.Buffer
112	_, _ = buf.ReadFrom(r)
113	line := strings.TrimSpace(buf.String())
114
115	assert.True(t, strings.HasPrefix(line, "10.0.0.1 - - ["))
116	assert.Contains(t, line, `"POST /api/v1/data?limit=10 HTTP/2.0"`)
117	assert.Contains(t, line, " 200 512")
118}
119
120func TestWriter_Stdout_Combined(t *testing.T) {
121	orig := os.Stdout
122	r, w, _ := os.Pipe()
123	os.Stdout = w
124
125	writer, err := New(Config{Format: "combined", Output: "stdout"})
126	require.NoError(t, err)
127	require.NoError(t, writer.Write(sampleEntry))
128	require.NoError(t, writer.Close())
129
130	w.Close()
131	os.Stdout = orig
132
133	var buf bytes.Buffer
134	_, _ = buf.ReadFrom(r)
135	line := strings.TrimSpace(buf.String())
136
137	assert.Contains(t, line, `"https://example.com"`)
138	assert.Contains(t, line, `"curl/7.88"`)
139}
140
141// --- file sink ---
142
143func TestWriter_File_WritesLines(t *testing.T) {
144	dir := t.TempDir()
145	path := filepath.Join(dir, "access.log")
146
147	writer, err := New(Config{
148		Format:     "json",
149		Output:     "file",
150		FilePath:   path,
151		MaxSize:    10,
152		MaxBackups: 1,
153	})
154	require.NoError(t, err)
155
156	require.NoError(t, writer.Write(sampleEntry))
157	require.NoError(t, writer.Write(sampleEntry))
158	require.NoError(t, writer.Close())
159
160	data, err := os.ReadFile(path)
161	require.NoError(t, err)
162
163	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
164	assert.Len(t, lines, 2)
165	for _, l := range lines {
166		var obj map[string]any
167		assert.NoError(t, json.Unmarshal([]byte(l), &obj), "each line must be valid JSON")
168	}
169}
170
171// --- concurrency ---
172
173func TestWriter_ConcurrentWrites(t *testing.T) {
174	dir := t.TempDir()
175	path := filepath.Join(dir, "concurrent.log")
176
177	writer, err := New(Config{
178		Format:   "json",
179		Output:   "file",
180		FilePath: path,
181		MaxSize:  100,
182	})
183	require.NoError(t, err)
184	defer writer.Close()
185
186	const goroutines = 50
187	var wg sync.WaitGroup
188	wg.Add(goroutines)
189	for i := range goroutines {
190		go func(n int) {
191			defer wg.Done()
192			e := sampleEntry
193			e.RequestID = fmt.Sprintf("req-%d", n)
194			assert.NoError(t, writer.Write(e))
195		}(i)
196	}
197	wg.Wait()
198
199	data, err := os.ReadFile(path)
200	require.NoError(t, err)
201
202	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
203	assert.Len(t, lines, goroutines, "each goroutine should produce exactly one line")
204	for _, l := range lines {
205		var obj map[string]any
206		assert.NoError(t, json.Unmarshal([]byte(l), &obj), "each line must be valid JSON")
207	}
208}

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