Files
logstream/syslog.go
T
2026-09-28 12:19:29 +02:00

331 lines
7.3 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bufio"
"context"
"io"
"log"
"net"
"strconv"
"strings"
"time"
"unicode/utf8"
)
var severityNames = [8]string{"emerg", "alert", "crit", "err", "warning", "notice", "info", "debug"}
var facilityNames = [24]string{
"kern", "user", "mail", "daemon", "auth", "syslog", "lpr", "news",
"uucp", "cron", "authpriv", "ftp", "ntp", "security", "console", "solaris-cron",
"local0", "local1", "local2", "local3", "local4", "local5", "local6", "local7",
}
// Entry est un message syslog décodé.
type Entry struct {
Time time.Time
Host string
App string
ProcID string
Facility string
Severity string
SevNum int
SD string // données structurées RFC 5424, brutes
Message string
Source string // IP de l'émetteur
Proto string // udp ou tcp
}
// Record renvoie l'entrée au format stocké dans VictoriaLogs et renvoyé par l'API.
func (e *Entry) Record() map[string]string {
r := map[string]string{
"_time": e.Time.UTC().Format(time.RFC3339Nano),
"_msg": e.Message,
"host": e.Host,
"app": e.App,
"facility": e.Facility,
"severity": e.Severity,
"sevnum": strconv.Itoa(e.SevNum),
"source": e.Source,
"proto": e.Proto,
}
if e.ProcID != "" {
r["procid"] = e.ProcID
}
if e.SD != "" {
r["sd"] = e.SD
}
return r
}
func (e *Entry) setPri(pri int) {
e.SevNum = pri % 8
e.Severity = severityNames[pri%8]
e.Facility = facilityNames[pri/8]
}
// ParseSyslog décode un message RFC 5424 ou RFC 3164 (BSD). Il ne renvoie
// jamais d'erreur : un message non conforme est conservé tel quel.
func ParseSyslog(raw []byte, source, proto string, now time.Time) *Entry {
s := strings.TrimRight(string(raw), "\r\n\x00 ")
if !utf8.ValidString(s) {
s = strings.ToValidUTF8(s, "�")
}
e := &Entry{Time: now, Host: source, Source: source, Proto: proto}
e.setPri(13) // user.notice, valeur par défaut de la RFC 3164
if len(s) > 2 && s[0] == '<' {
if end := strings.IndexByte(s, '>'); end > 1 && end <= 4 {
if pri, err := strconv.Atoi(s[1:end]); err == nil && pri >= 0 && pri <= 191 {
e.setPri(pri)
s = s[end+1:]
}
}
}
if strings.HasPrefix(s, "1 ") {
parse5424(e, s[2:])
} else {
parse3164(e, s)
}
if e.Message == "" {
e.Message = "(vide)"
}
return e
}
// parse5424 : TIMESTAMP HOSTNAME APP-NAME PROCID MSGID STRUCTURED-DATA MSG
func parse5424(e *Entry, s string) {
f := strings.SplitN(s, " ", 6)
if len(f) < 6 {
e.Message = s
return
}
if t, err := time.Parse(time.RFC3339Nano, f[0]); err == nil {
e.Time = t
}
if f[1] != "-" {
e.Host = f[1]
}
if f[2] != "-" {
e.App = f[2]
}
if f[3] != "-" {
e.ProcID = f[3]
}
rest := f[5]
switch {
case rest == "-" || strings.HasPrefix(rest, "- "):
rest = strings.TrimPrefix(rest[1:], " ")
case strings.HasPrefix(rest, "["):
n := sdLen(rest)
e.SD = rest[:n]
rest = strings.TrimPrefix(rest[n:], " ")
}
e.Message = strings.TrimPrefix(rest, "")
}
// sdLen renvoie la longueur du bloc de données structurées « [..][..] ».
func sdLen(s string) int {
i := 0
for i < len(s) && s[i] == '[' {
inQuote := false
for i++; i < len(s); i++ {
c := s[i]
if inQuote && c == '\\' {
i++
continue
}
if c == '"' {
inQuote = !inQuote
continue
}
if c == ']' && !inQuote {
i++
break
}
}
}
if i > len(s) {
i = len(s)
}
return i
}
// parse3164 : « Mmm dd hh:mm:ss HOST TAG[PID]: MSG », ou horodatage ISO (rsyslog).
func parse3164(e *Entry, s string) {
if len(s) >= 16 && s[15] == ' ' {
if t, err := time.ParseInLocation(time.Stamp, s[:15], time.Local); err == nil {
// L'année est absente : on prend l'année courante, sauf si la date tombe dans le futur.
now := e.Time
t = time.Date(now.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, time.Local)
if t.After(now.Add(24 * time.Hour)) {
t = t.AddDate(-1, 0, 0)
}
e.Time = t
parseHostTag(e, s[16:], true)
return
}
}
if sp := strings.IndexByte(s, ' '); sp >= 19 {
if t, err := time.Parse(time.RFC3339Nano, s[:sp]); err == nil {
e.Time = t
parseHostTag(e, s[sp+1:], true)
return
}
}
parseHostTag(e, s, false)
}
func parseHostTag(e *Entry, s string, withHost bool) {
if withHost {
if sp := strings.IndexByte(s, ' '); sp > 0 {
tok := s[:sp]
if !strings.HasSuffix(tok, ":") && !strings.Contains(tok, "[") {
e.Host = tok
s = s[sp+1:]
}
}
}
if i := strings.IndexAny(s, ":[ "); i > 0 && i <= 48 {
switch s[i] {
case ':':
if i+1 == len(s) || s[i+1] == ' ' {
e.App = s[:i]
s = strings.TrimPrefix(s[i+1:], " ")
}
case '[':
if j := strings.IndexByte(s[i:], ']'); j > 1 {
e.App = s[:i]
e.ProcID = s[i+1 : i+j]
s = strings.TrimPrefix(strings.TrimPrefix(s[i+j+1:], ":"), " ")
}
}
}
e.Message = s
}
// StartSyslog démarre les écoutes UDP et TCP sur la même adresse.
func StartSyslog(ctx context.Context, addr string, sink func(*Entry)) error {
pc, err := net.ListenPacket("udp", addr)
if err != nil {
return err
}
ln, err := net.Listen("tcp", addr)
if err != nil {
pc.Close()
return err
}
if uc, ok := pc.(*net.UDPConn); ok {
_ = uc.SetReadBuffer(8 << 20)
}
go func() {
<-ctx.Done()
pc.Close()
ln.Close()
}()
go serveUDP(ctx, pc, sink)
go serveTCP(ctx, ln, sink)
return nil
}
func serveUDP(ctx context.Context, pc net.PacketConn, sink func(*Entry)) {
buf := make([]byte, 65536)
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil {
if ctx.Err() != nil {
return
}
log.Printf("syslog udp : %v", err)
time.Sleep(100 * time.Millisecond)
continue
}
sink(ParseSyslog(buf[:n], hostOf(addr), "udp", time.Now()))
}
}
func serveTCP(ctx context.Context, ln net.Listener, sink func(*Entry)) {
for {
conn, err := ln.Accept()
if err != nil {
if ctx.Err() != nil {
return
}
log.Printf("syslog tcp : %v", err)
time.Sleep(100 * time.Millisecond)
continue
}
go handleTCP(ctx, conn, sink)
}
}
const maxFrame = 1 << 20
// handleTCP gère les deux découpages de la RFC 6587 : « octet counting »
// (« 123 <34>1 ... ») et un message par ligne.
func handleTCP(ctx context.Context, conn net.Conn, sink func(*Entry)) {
defer conn.Close()
stop := context.AfterFunc(ctx, func() { conn.Close() })
defer stop()
src := hostOf(conn.RemoteAddr())
r := bufio.NewReaderSize(conn, 64*1024)
for {
c, err := r.ReadByte()
if err != nil {
return
}
switch {
case c == '\n' || c == '\r' || c == ' ' || c == 0:
continue
case c >= '1' && c <= '9':
n := int(c - '0')
for {
c, err = r.ReadByte()
if err != nil {
return
}
if c == ' ' {
break
}
if c < '0' || c > '9' || n > maxFrame {
log.Printf("syslog tcp %s : trame invalide, connexion fermée", src)
return
}
n = n*10 + int(c-'0')
}
if n > maxFrame {
return
}
msg := make([]byte, n)
if _, err := io.ReadFull(r, msg); err != nil {
return
}
sink(ParseSyslog(msg, src, "tcp", time.Now()))
default:
_ = r.UnreadByte()
line, err := r.ReadSlice('\n')
if len(line) > 0 {
sink(ParseSyslog(line, src, "tcp", time.Now()))
}
// Ligne plus longue que le tampon : on ignore la suite.
for err == bufio.ErrBufferFull {
_, err = r.ReadSlice('\n')
}
if err != nil {
return
}
}
}
}
func hostOf(a net.Addr) string {
if a == nil {
return ""
}
h, _, err := net.SplitHostPort(a.String())
if err != nil {
return a.String()
}
return h
}