fix: UDP listener parses batched datagrams
nginx-ipng-stats-plugin's ipng_stats_logtail directive buffers many log lines into a single UDP datagram (default buffer=64k flush=1s). The listener was treating each datagram as exactly one log line, so any datagram with N>1 lines failed the v1 field-count check and dropped silently. In production this showed up as logtail_udp_packets_received_total roughly 4x logtail_udp_loglines_success_total — matching typical burst-coalesced 4-lines-per-batch ratios. Fix: strip trailing CRLF, split the payload on '\n', parse each non-empty line independently. Counter semantics now match the names: packets_received — datagrams off the socket (one per recvfrom) loglines_success — log lines parsed OK (may be many per datagram) loglines_consumed — log lines forwarded to the store (not dropped) After the fix, loglines_success ≈ packets_received × avg_lines_per_batch. Regression test TestUDPListenerBatchedDatagram sends one datagram with three '\n'-separated v1 lines and asserts all three LogRecords arrive, plus loglines_success >= 3 * packets_received. Docs (user-guide.md, design.md) now explain the datagram-vs-line unit distinction so operators don't misread the ratio. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,21 +74,31 @@ func (u *UDPListener) Run(ctx context.Context) {
|
||||
if u.prom != nil {
|
||||
u.prom.IncUDPPacket()
|
||||
}
|
||||
line := strings.TrimRight(string(buf[:n]), "\r\n")
|
||||
rec, ok := ParseUDPLine(line, u.v4bits, u.v6bits)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if u.prom != nil {
|
||||
u.prom.IncUDPSuccess()
|
||||
}
|
||||
select {
|
||||
case u.ch <- rec:
|
||||
if u.prom != nil {
|
||||
u.prom.IncUDPConsumed()
|
||||
// nginx-ipng-stats-plugin batches log lines into a single UDP
|
||||
// datagram (default buffer=64k flush=1s), so one packet may carry
|
||||
// many lines. nginx's log_format always ends a rendered line with
|
||||
// '\n'; split on that and process each line independently.
|
||||
payload := strings.TrimRight(string(buf[:n]), "\r\n")
|
||||
for _, line := range strings.Split(payload, "\n") {
|
||||
line = strings.TrimSuffix(line, "\r")
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
rec, ok := ParseUDPLine(line, u.v4bits, u.v6bits)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if u.prom != nil {
|
||||
u.prom.IncUDPSuccess()
|
||||
}
|
||||
select {
|
||||
case u.ch <- rec:
|
||||
if u.prom != nil {
|
||||
u.prom.IncUDPConsumed()
|
||||
}
|
||||
default:
|
||||
// Channel full — drop rather than block the read loop.
|
||||
}
|
||||
default:
|
||||
// Channel full — drop rather than block the read loop.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user