v2.31.0-dev22 8d342c4

formatter.go RawHistoryFolder

1package accesslog
2
3import (
4	"encoding/json"
5	"fmt"
6	"strings"
7
8	out "github.com/bnema/gordon/internal/boundaries/out"
9)
10
11// jsonEntry is the stable JSON schema for access log entries.
12// Field order is controlled by the struct definition.
13type jsonEntry struct {
14	Time       string  `json:"time"`
15	ClientIP   string  `json:"client_ip"`
16	Method     string  `json:"method"`
17	Host       string  `json:"host"`
18	Path       string  `json:"path"`
19	Query      string  `json:"query"`
20	Status     int     `json:"status"`
21	BytesSent  int     `json:"bytes_sent"`
22	DurationMS float64 `json:"duration_ms"`
23	UserAgent  string  `json:"user_agent"`
24	Referer    string  `json:"referer"`
25	RequestID  string  `json:"request_id"`
26	Proto      string  `json:"proto"`
27}
28
29// clfTimestamp is the standard Common Log Format timestamp layout.
30const clfTimestamp = "02/Jan/2006:15:04:05 -0700"
31
32// jsonTimestamp is the access log JSON timestamp layout: RFC3339 with
33// millisecond precision in UTC.
34const jsonTimestamp = "2006-01-02T15:04:05.000Z07:00"
35
36// formatJSON serializes entry as a single-line JSON object.
37// Timestamps are RFC3339 with millisecond precision in UTC.
38// time.Time's default JSON marshaling is NOT used to ensure stable precision.
39func formatJSON(e out.AccessLogEntry) (string, error) {
40	je := jsonEntry{
41		Time:       e.Time.UTC().Format(jsonTimestamp),
42		ClientIP:   e.ClientIP,
43		Method:     e.Method,
44		Host:       e.Host,
45		Path:       e.Path,
46		Query:      e.Query,
47		Status:     e.Status,
48		BytesSent:  e.BytesSent,
49		DurationMS: e.DurationMS,
50		UserAgent:  e.UserAgent,
51		Referer:    e.Referer,
52		RequestID:  e.RequestID,
53		Proto:      e.Proto,
54	}
55	b, err := json.Marshal(je)
56	if err != nil {
57		return "", fmt.Errorf("formatJSON: %w", err)
58	}
59	return string(b), nil
60}
61
62// formatCLF serializes entry in Common Log Format (NCSA).
63//
64//	<ip> - - [timestamp] "METHOD target PROTO" status bytes
65func formatCLF(e out.AccessLogEntry) (string, error) {
66	ts := e.Time.UTC().Format(clfTimestamp)
67	target := buildRequestTarget(e.Path, e.Query)
68	return fmt.Sprintf(`%s - - [%s] "%s %s %s" %d %d`,
69		e.ClientIP,
70		ts,
71		escapeQuoted(e.Method),
72		escapeQuoted(target),
73		escapeQuoted(e.Proto),
74		e.Status,
75		e.BytesSent,
76	), nil
77}
78
79// formatCombined serializes entry in Combined Log Format (CLF + referer + UA).
80//
81//	<ip> - - [timestamp] "METHOD target PROTO" status bytes "referer" "ua"
82func formatCombined(e out.AccessLogEntry) (string, error) {
83	ts := e.Time.UTC().Format(clfTimestamp)
84	target := buildRequestTarget(e.Path, e.Query)
85	referer := e.Referer
86	if referer == "" {
87		referer = "-"
88	}
89	ua := e.UserAgent
90	if ua == "" {
91		ua = "-"
92	}
93	return fmt.Sprintf(`%s - - [%s] "%s %s %s" %d %d "%s" "%s"`,
94		e.ClientIP,
95		ts,
96		escapeQuoted(e.Method),
97		escapeQuoted(target),
98		escapeQuoted(e.Proto),
99		e.Status,
100		e.BytesSent,
101		escapeQuoted(referer),
102		escapeQuoted(ua),
103	), nil
104}
105
106// buildRequestTarget builds the request target portion of a CLF/Combined line.
107// When query is empty only the path is used; otherwise "path?query".
108func buildRequestTarget(path, query string) string {
109	if query == "" {
110		return path
111	}
112	return path + "?" + query
113}
114
115// escapeQuoted escapes backslashes, double-quotes, and control characters inside
116// a value that will be enclosed in double-quotes in the log line.
117// All ASCII control characters (0x00-0x1F) are escaped to prevent log-injection
118// via user-controlled fields such as User-Agent or Referer.
119func escapeQuoted(s string) string {
120	var b strings.Builder
121	b.Grow(len(s))
122	for _, r := range s {
123		switch {
124		case r == '\\':
125			b.WriteString(`\\`)
126		case r == '"':
127			b.WriteString(`\"`)
128		case r == '\n':
129			b.WriteString(`\n`)
130		case r == '\r':
131			b.WriteString(`\r`)
132		case r == '\t':
133			b.WriteString(`\t`)
134		case r < 0x20:
135			fmt.Fprintf(&b, `\x%02x`, r)
136		default:
137			b.WriteRune(r)
138		}
139	}
140	return b.String()
141}

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