package main import ( "fmt" "net" "strings" ) // LogRecord holds the four dimensions extracted from a single nginx log line. type LogRecord struct { Website string ClientPrefix string URI string Status string } // ParseLine parses a tab-separated logtail log line: // // $host \t $remote_addr \t $msec \t $request_method \t $request_uri \t $status \t $body_bytes_sent \t $request_time // // Returns false for lines with fewer than 8 fields. func ParseLine(line string, v4bits, v6bits int) (LogRecord, bool) { // SplitN caps allocations; we need exactly 8 fields. fields := strings.SplitN(line, "\t", 8) if len(fields) < 8 { return LogRecord{}, false } uri := fields[4] if i := strings.IndexByte(uri, '?'); i >= 0 { uri = uri[:i] } prefix, ok := truncateIP(fields[1], v4bits, v6bits) if !ok { return LogRecord{}, false } return LogRecord{ Website: fields[0], ClientPrefix: prefix, URI: uri, Status: fields[5], }, true } // truncateIP masks addr to the given prefix length depending on IP version. // Returns the CIDR string (e.g. "1.2.3.0/24") and true on success. func truncateIP(addr string, v4bits, v6bits int) (string, bool) { ip := net.ParseIP(addr) if ip == nil { return "", false } var bits int if ip.To4() != nil { ip = ip.To4() bits = v4bits } else { ip = ip.To16() bits = v6bits } mask := net.CIDRMask(bits, len(ip)*8) masked := make(net.IP, len(ip)) for i := range ip { masked[i] = ip[i] & mask[i] } return fmt.Sprintf("%s/%d", masked.String(), bits), true }