package main import ( "fmt" "net" "strconv" "strings" ) // LogRecord holds the dimensions extracted from a single nginx log line. type LogRecord struct { Website string ClientPrefix string URI string Status string IsTor bool ASN int32 Method string BodyBytesSent int64 RequestTime float64 } // 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 \t $is_tor \t $asn // // The is_tor (field 9) and asn (field 10) fields are optional for backward // compatibility with older log files that omit them; they default to false/0 // when absent. // Returns false for lines with fewer than 8 fields. func ParseLine(line string, v4bits, v6bits int) (LogRecord, bool) { // SplitN caps allocations; we need up to 10 fields. fields := strings.SplitN(line, "\t", 10) 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 } isTor := len(fields) >= 9 && fields[8] == "1" var asn int32 if len(fields) == 10 { if n, err := strconv.ParseInt(fields[9], 10, 32); err == nil { asn = int32(n) } } var bodyBytes int64 if n, err := strconv.ParseInt(fields[6], 10, 64); err == nil { bodyBytes = n } var reqTime float64 if f, err := strconv.ParseFloat(fields[7], 64); err == nil { reqTime = f } return LogRecord{ Website: fields[0], ClientPrefix: prefix, URI: uri, Status: fields[5], IsTor: isTor, ASN: asn, Method: fields[3], BodyBytesSent: bodyBytes, RequestTime: reqTime, }, 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 }