Add ASN to logtail, collector, aggregator, frontend and CLI
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -13,18 +14,20 @@ type LogRecord struct {
|
||||
URI string
|
||||
Status string
|
||||
IsTor bool
|
||||
ASN int32
|
||||
}
|
||||
|
||||
// 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
|
||||
// $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 (0 or 1) is optional for backward compatibility with
|
||||
// older log files that omit it; it defaults to false when absent.
|
||||
// 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 9 fields.
|
||||
fields := strings.SplitN(line, "\t", 9)
|
||||
// SplitN caps allocations; we need up to 10 fields.
|
||||
fields := strings.SplitN(line, "\t", 10)
|
||||
if len(fields) < 8 {
|
||||
return LogRecord{}, false
|
||||
}
|
||||
@@ -39,7 +42,14 @@ func ParseLine(line string, v4bits, v6bits int) (LogRecord, bool) {
|
||||
return LogRecord{}, false
|
||||
}
|
||||
|
||||
isTor := len(fields) == 9 && fields[8] == "1"
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return LogRecord{
|
||||
Website: fields[0],
|
||||
@@ -47,6 +57,7 @@ func ParseLine(line string, v4bits, v6bits int) (LogRecord, bool) {
|
||||
URI: uri,
|
||||
Status: fields[5],
|
||||
IsTor: isTor,
|
||||
ASN: asn,
|
||||
}, true
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,58 @@ func TestParseLine(t *testing.T) {
|
||||
IsTor: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "asn field parsed",
|
||||
line: "asn.example.com\t1.2.3.4\t0\tGET\t/\t200\t0\t0.001\t0\t12345",
|
||||
wantOK: true,
|
||||
want: LogRecord{
|
||||
Website: "asn.example.com",
|
||||
ClientPrefix: "1.2.3.0/24",
|
||||
URI: "/",
|
||||
Status: "200",
|
||||
IsTor: false,
|
||||
ASN: 12345,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "asn field with is_tor=1",
|
||||
line: "both.example.com\t1.2.3.4\t0\tGET\t/\t200\t0\t0.001\t1\t65535",
|
||||
wantOK: true,
|
||||
want: LogRecord{
|
||||
Website: "both.example.com",
|
||||
ClientPrefix: "1.2.3.0/24",
|
||||
URI: "/",
|
||||
Status: "200",
|
||||
IsTor: true,
|
||||
ASN: 65535,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing asn field defaults to 0 (backward compat)",
|
||||
line: "noasn.example.com\t1.2.3.4\t0\tGET\t/\t200\t0\t0.001\t1",
|
||||
wantOK: true,
|
||||
want: LogRecord{
|
||||
Website: "noasn.example.com",
|
||||
ClientPrefix: "1.2.3.0/24",
|
||||
URI: "/",
|
||||
Status: "200",
|
||||
IsTor: true,
|
||||
ASN: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid asn field defaults to 0",
|
||||
line: "badann.example.com\t1.2.3.4\t0\tGET\t/\t200\t0\t0.001\t0\tnot-a-number",
|
||||
wantOK: true,
|
||||
want: LogRecord{
|
||||
Website: "badann.example.com",
|
||||
ClientPrefix: "1.2.3.0/24",
|
||||
URI: "/",
|
||||
Status: "200",
|
||||
IsTor: false,
|
||||
ASN: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@@ -15,7 +15,7 @@ type Store struct {
|
||||
source string
|
||||
|
||||
// live map — written only by the Run goroutine; no locking needed on writes
|
||||
live map[st.Tuple5]int64
|
||||
live map[st.Tuple6]int64
|
||||
liveLen int
|
||||
|
||||
// ring buffers — protected by mu for reads
|
||||
@@ -36,7 +36,7 @@ type Store struct {
|
||||
func NewStore(source string) *Store {
|
||||
return &Store{
|
||||
source: source,
|
||||
live: make(map[st.Tuple5]int64, liveMapCap),
|
||||
live: make(map[st.Tuple6]int64, liveMapCap),
|
||||
subs: make(map[chan st.Snapshot]struct{}),
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func NewStore(source string) *Store {
|
||||
// ingest records one log record into the live map.
|
||||
// Must only be called from the Run goroutine.
|
||||
func (s *Store) ingest(r LogRecord) {
|
||||
key := st.Tuple5{Website: r.Website, Prefix: r.ClientPrefix, URI: r.URI, Status: r.Status, IsTor: r.IsTor}
|
||||
key := st.Tuple6{Website: r.Website, Prefix: r.ClientPrefix, URI: r.URI, Status: r.Status, IsTor: r.IsTor, ASN: r.ASN}
|
||||
if _, exists := s.live[key]; !exists {
|
||||
if s.liveLen >= liveMapCap {
|
||||
return
|
||||
@@ -77,7 +77,7 @@ func (s *Store) rotate(now time.Time) {
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
s.live = make(map[st.Tuple5]int64, liveMapCap)
|
||||
s.live = make(map[st.Tuple6]int64, liveMapCap)
|
||||
s.liveLen = 0
|
||||
|
||||
s.broadcast(fine)
|
||||
|
||||
Reference in New Issue
Block a user