First commit
This commit is contained in:
commit
8200c2bc87
17 files changed
+2592
No files matched your search
@@ -0,0 +1,330 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user