Skip to content

Commit 6102f87

Browse files
authored
Merge pull request #247 from Notifiarr/dn2_logger
Remove apachelog library, and all things it needed
2 parents 947db47 + 9eb90d1 commit 6102f87

11 files changed

Lines changed: 615 additions & 228 deletions

docs/api_docs.go

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,6 @@ const docTemplateapi = `{
6464
"type": "string",
6565
"description": "Environment: live, dev, etc."
6666
},
67-
"X-Request-Time": {
68-
"type": "string",
69-
"description": "How long the request elapsed."
70-
},
7167
"X-UserID": {
7268
"type": "string",
7369
"description": "MySQL ID for the user whose API key was provided."
@@ -87,14 +83,6 @@ const docTemplateapi = `{
8783
"X-API-Key": {
8884
"type": "string",
8985
"description": "API Key parsed from request."
90-
},
91-
"X-Key": {
92-
"type": "string",
93-
"description": "Masked API Key parsed from request."
94-
},
95-
"X-Length": {
96-
"type": "int",
97-
"description": "The length of the API key."
9886
}
9987
}
10088
}
@@ -418,9 +406,11 @@ const docTemplateapi = `{
418406
},
419407
"data": {},
420408
"hits": {
409+
"description": "Copied from 'hits' on read.",
421410
"type": "integer"
422411
},
423412
"lastAccess": {
413+
"description": "Copied from 'last' on read.",
424414
"type": "string"
425415
}
426416
}
@@ -469,6 +459,10 @@ const docTemplateapi = `{
469459
"webserver.Config": {
470460
"type": "object",
471461
"properties": {
462+
"cacheShards": {
463+
"description": "CacheShards is golift.io/cache partition count for users and servers; 0 means library default (single shard).",
464+
"type": "integer"
465+
},
472466
"connMaxIdleTime": {
473467
"$ref": "#/definitions/time.Duration"
474468
},

go.mod

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ go 1.26.1
55
require (
66
github.com/go-sql-driver/mysql v1.9.3
77
github.com/gorilla/mux v1.8.1
8-
github.com/lestrrat-go/apache-logformat/v2 v2.0.6
98
github.com/prometheus/client_golang v1.23.2
109
github.com/swaggo/swag v1.16.6
1110
golift.io/cache v1.0.1-0.20260406025202-1176587c97ab
@@ -31,9 +30,7 @@ require (
3130
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
3231
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
3332
github.com/kr/text v0.2.0 // indirect
34-
github.com/lestrrat-go/strftime v1.1.1 // indirect
3533
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
36-
github.com/pkg/errors v0.9.1 // indirect
3734
github.com/prometheus/client_model v0.6.2 // indirect
3835
github.com/prometheus/common v0.67.5 // indirect
3936
github.com/prometheus/procfs v0.20.1 // indirect

go.sum

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
99
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
1010
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
1111
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
12-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1312
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1413
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15-
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
16-
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
1714
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
1815
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
1916
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
@@ -55,18 +52,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5552
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5653
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
5754
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
58-
github.com/lestrrat-go/apache-logformat/v2 v2.0.6 h1:MDyexlEMjFnXXuNemorO/SgUjkpWz6rKmYPkx1CgQg8=
59-
github.com/lestrrat-go/apache-logformat/v2 v2.0.6/go.mod h1:meGwIaUOWH7yPDXUSxMTVTMdH9HkGTO1oN2z/4n/yPs=
60-
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
61-
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
62-
github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
63-
github.com/lestrrat-go/strftime v1.1.1 h1:zgf8QCsgj27GlKBy3SU9/8MMgegZ8UCzlCyHYrUF0QU=
64-
github.com/lestrrat-go/strftime v1.1.1/go.mod h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
6555
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
6656
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
67-
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
68-
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
69-
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
7057
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7158
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7259
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
@@ -79,8 +66,6 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy
7966
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
8067
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
8168
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
82-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
83-
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
8469
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
8570
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
8671
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=

pkg/webserver/accesslog.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package webserver
2+
3+
import (
4+
"io"
5+
"net"
6+
"net/http"
7+
"strconv"
8+
"strings"
9+
"sync"
10+
"time"
11+
)
12+
13+
/* High performance access logging. */
14+
15+
const accessLogInitialGrow = 512
16+
17+
// captureWriter records status and body size for access logging.
18+
type captureWriter struct {
19+
http.ResponseWriter
20+
21+
start time.Time
22+
status int
23+
size int64
24+
}
25+
26+
func (c *captureWriter) WriteHeader(code int) {
27+
if c.status == 0 {
28+
c.status = code
29+
}
30+
31+
c.ResponseWriter.WriteHeader(code)
32+
}
33+
34+
func (c *captureWriter) Write(p []byte) (int, error) {
35+
if c.status == 0 {
36+
c.status = http.StatusOK
37+
}
38+
39+
n, err := c.ResponseWriter.Write(p)
40+
c.size += int64(n)
41+
42+
return n, err //nolint:wrapcheck // delegate to underlying ResponseWriter
43+
}
44+
45+
func (c *captureWriter) statusCode() int {
46+
if c.status == 0 {
47+
return http.StatusOK
48+
}
49+
50+
return c.status
51+
}
52+
53+
// Get returns the first value for a response header field. key must already be in
54+
// canonical form (http.CanonicalHeaderKey). Unlike Header.Get it does not allocate
55+
// or re-canonicalize key on each call.
56+
func (c *captureWriter) Get(key string) string {
57+
if v := c.Header()[key]; len(v) > 0 {
58+
return v[0]
59+
}
60+
61+
return ""
62+
}
63+
64+
//nolint:gochecknoglobals // one pool per process for hot-path access log strings.Builder reuse
65+
var alBuilder = sync.Pool{New: func() any { return &strings.Builder{} }}
66+
67+
// accessLogWrap writes one Apache-style line per request to dst (same field order as the former alFmt).
68+
func accessLogWrap(next http.Handler, dst io.Writer) http.Handler {
69+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
70+
capture := &captureWriter{ResponseWriter: w, start: time.Now()}
71+
next.ServeHTTP(capture, req)
72+
capture.writeAccessLogLine(req, dst)
73+
})
74+
}
75+
76+
func (c *captureWriter) writeAccessLogLine(req *http.Request, dst io.Writer) {
77+
//nolint:forcetypeassert
78+
builder := alBuilder.Get().(*strings.Builder) // Get a string buffer:
79+
builder.Reset() // - reset it.
80+
builder.Grow(accessLogInitialGrow) // - grow it.
81+
c.writeAccessLogLinePrefix(builder, req) // - fill prefix.
82+
c.writeAccessLogLineTail(builder, req) // - fill suffix.
83+
_, _ = io.WriteString(dst, builder.String()) // - write it.
84+
alBuilder.Put(builder) // - put it back.
85+
}
86+
87+
func (c *captureWriter) writeAccessLogLinePrefix(builder *strings.Builder, req *http.Request) {
88+
// %V
89+
builder.WriteString(req.Host)
90+
builder.WriteByte(' ')
91+
// %{X-Forwarded-For}i — computed like former fixForwardedFor (not raw header).
92+
builder.WriteString(ClientIPForLog(req))
93+
builder.WriteByte(' ')
94+
// "%{X-Username}o"
95+
builder.WriteByte('"')
96+
builder.WriteString(c.Get("X-Username"))
97+
builder.WriteString("\" ")
98+
// %{X-UserID}o
99+
builder.WriteString(c.Get("X-Userid"))
100+
builder.WriteByte(' ')
101+
// %t — [02/Jan/2006:15:04:05 -0700]
102+
builder.WriteByte('[')
103+
builder.WriteString(c.start.Format("02/Jan/2006:15:04:05 -0700"))
104+
builder.WriteString("] ")
105+
// "%r"
106+
builder.WriteByte('"')
107+
builder.WriteString(req.Method)
108+
builder.WriteByte(' ')
109+
110+
if req.RequestURI != "" {
111+
builder.WriteString(req.RequestURI)
112+
} else {
113+
builder.WriteString(req.URL.RequestURI())
114+
}
115+
116+
builder.WriteString(" HTTP/1.1\" ")
117+
// %>s
118+
builder.WriteString(strconv.Itoa(c.statusCode()))
119+
builder.WriteByte(' ')
120+
// %b — response body size (0 when none).
121+
builder.WriteString(strconv.FormatInt(c.size, 10))
122+
123+
builder.WriteByte(' ')
124+
// "%{Referer}i" "%{User-agent}i" query:...
125+
builder.WriteByte('"')
126+
builder.WriteString(RefererPathForLog(req.Header))
127+
builder.WriteString("\" \"")
128+
builder.WriteString(req.UserAgent())
129+
builder.WriteByte('"')
130+
}
131+
132+
func (c *captureWriter) writeAccessLogLineTail(builder *strings.Builder, req *http.Request) {
133+
builder.WriteString(" req:")
134+
// %{ms}T — elapsed milliseconds (same as apache-logformat request duration).
135+
builder.WriteString(strconv.FormatInt(time.Since(c.start).Milliseconds(), 10))
136+
builder.WriteString("ms age:")
137+
builder.WriteString(c.Get("Age"))
138+
builder.WriteString(" env:")
139+
builder.WriteString(c.Get("X-Environment"))
140+
builder.WriteString(" key:")
141+
142+
masked, keyLenStr := c.maskedAPIKeyFromResponse()
143+
builder.WriteString(masked)
144+
builder.WriteByte('(')
145+
builder.WriteString(keyLenStr)
146+
builder.WriteString(") \"srv:")
147+
builder.WriteString(req.Header.Get("X-Server"))
148+
builder.WriteString("\"\n")
149+
}
150+
151+
// maskedAPIKeyFromResponse returns maskAPIKey(w X-Api-Key) for the access log, or ("", "")
152+
// when the handler did not set that response header.
153+
func (c *captureWriter) maskedAPIKeyFromResponse() (string, string) {
154+
key := c.Get("X-Api-Key")
155+
if key == "" {
156+
return "", ""
157+
}
158+
159+
return maskAPIKey(key)
160+
}
161+
162+
// RefererPathForLog returns the path part of X-Original-Uri (no query string) truncated before the
163+
// API key segment (keyPosition), using the same strings.Split(path, "/") rules as GetAPIKeyFromURIPath.
164+
// If the path has fewer than keyPosition+1 segments, it returns the full path (still without query).
165+
// When X-Original-Uri is missing, empty, or only a query string, it returns "".
166+
func RefererPathForLog(header http.Header) string {
167+
pathPart, _, _ := strings.Cut(header.Get("X-Original-Uri"), "?")
168+
if pathPart == "" {
169+
return ""
170+
}
171+
172+
var pos, segIdx int
173+
174+
for seg := range strings.SplitSeq(pathPart, "/") {
175+
if segIdx == keyPosition {
176+
return strings.TrimSuffix(pathPart[:pos], "/")
177+
}
178+
179+
pos += len(seg)
180+
if pos < len(pathPart) && pathPart[pos] == '/' {
181+
pos++
182+
}
183+
184+
segIdx++
185+
}
186+
187+
return pathPart
188+
}
189+
190+
// ClientIPForLog returns the client IP for access logs (same rules as the former fixForwardedFor middleware).
191+
func ClientIPForLog(req *http.Request) string {
192+
forwarded := req.Header.Get("X-Forwarded-For")
193+
if forwarded == "" {
194+
host, _, err := net.SplitHostPort(req.RemoteAddr)
195+
if err != nil {
196+
return strings.Trim(req.RemoteAddr, "[]")
197+
}
198+
199+
return host
200+
}
201+
202+
return strings.TrimSpace(strings.Split(forwarded, ",")[0])
203+
}

0 commit comments

Comments
 (0)