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 }