nxstats/internal/parser/access.go
2026-08-24 14:44:54 -04:00

70 lines
1.6 KiB
Go

package parser
import (
"fmt"
"regexp"
"strconv"
"time"
)
// AccessEntry holds a parsed nginx combined-format access log entry.
type AccessEntry struct {
RemoteAddr string
RemoteUser string
Time time.Time
Method string
Path string
Protocol string
Status int
BytesSent int64
Referer string
UserAgent string
}
// accessLogRe matches nginx combined log format.
// Groups: remoteAddr, remoteUser, time, method, path, protocol, status, bytes, referer, userAgent
var accessLogRe = regexp.MustCompile(
`^(\S+) - (\S+) \[([^\]]+)\] "(\S+) (\S+) (\S+)" (\d+) (\d+|-) "([^"]*)" "([^"]*)"$`,
)
const accessTimeLayout = "02/Jan/2006:15:04:05 -0700"
// ParseAccessLine parses a single nginx access log line.
// Returns an error for malformed lines.
func ParseAccessLine(line string) (AccessEntry, error) {
m := accessLogRe.FindStringSubmatch(line)
if m == nil {
return AccessEntry{}, fmt.Errorf("access: no match: %q", line)
}
t, err := time.Parse(accessTimeLayout, m[3])
if err != nil {
return AccessEntry{}, fmt.Errorf("access: parse time %q: %w", m[3], err)
}
status, err := strconv.Atoi(m[7])
if err != nil {
return AccessEntry{}, fmt.Errorf("access: parse status %q: %w", m[7], err)
}
var bytes int64
if m[8] != "-" {
bytes, err = strconv.ParseInt(m[8], 10, 64)
if err != nil {
return AccessEntry{}, fmt.Errorf("access: parse bytes %q: %w", m[8], err)
}
}
return AccessEntry{
RemoteAddr: m[1],
RemoteUser: m[2],
Time: t,
Method: m[4],
Path: m[5],
Protocol: m[6],
Status: status,
BytesSent: bytes,
Referer: m[9],
UserAgent: m[10],
}, nil
}