Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
136 lines
4.4 KiB
Go
136 lines
4.4 KiB
Go
// Package core holds what the parts of a pipeline share: the LogEntry and the
|
|
// TransportEvent sinks get, the Tap a viewer reads a flow's decisions from,
|
|
// the level names, plugin capabilities, size limits and timings, and Network
|
|
// and Listen, which keep a listener or dialer to its host's address family.
|
|
package core
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// LogEntry represents a single log record flowing through the pipeline
|
|
type LogEntry struct {
|
|
Time time.Time `json:"time"`
|
|
Node string `json:"node,omitempty"` // origin node identity for chained topologies; first hop stamps, relays preserve
|
|
Source string `json:"source"`
|
|
Level string `json:"level,omitempty"`
|
|
Message string `json:"message"`
|
|
Fields json.RawMessage `json:"fields,omitempty"`
|
|
RawSize int64 `json:"-"`
|
|
}
|
|
|
|
// TransportEvent contains the final payload and minimal metadata needed by sinks
|
|
type TransportEvent struct {
|
|
Time time.Time
|
|
// Formatted, serialized log payload
|
|
Payload []byte
|
|
// Structured entry for re-serializing sinks (chain links). Zero Time => absent
|
|
Entry LogEntry
|
|
}
|
|
|
|
// Tapped is what a pipeline's flow decided about one entry: Stage names the
|
|
// stage that dropped Event.Entry (rate_limit, filters at Index, format), or is
|
|
// "" when it passed and Event.Payload is what dispatch offered the sinks; a
|
|
// full sink input drops it for that sink only.
|
|
type Tapped struct {
|
|
Pipeline string
|
|
Source string // the source's id; "" for a heartbeat
|
|
Stage string
|
|
Index int
|
|
Event TransportEvent
|
|
}
|
|
|
|
// Tap hands Tapped records to a viewer without ever waiting: a full tap drops
|
|
// and counts. Unless All is set it takes only what passed, so a strict filter
|
|
// cannot crowd those out.
|
|
type Tap struct {
|
|
ch chan Tapped
|
|
All atomic.Bool
|
|
drops atomic.Uint64
|
|
}
|
|
|
|
func NewTap(size int) *Tap { return &Tap{ch: make(chan Tapped, size)} }
|
|
|
|
// Offer clips only what it can keep, so a full tap costs the pipeline nothing;
|
|
// a reader racing it costs a drop, never a wait
|
|
func (t *Tap) Offer(r Tapped) {
|
|
switch {
|
|
case r.Stage != "" && !t.All.Load():
|
|
return
|
|
case len(t.ch) == cap(t.ch):
|
|
t.drops.Add(1)
|
|
return
|
|
}
|
|
e := &r.Event.Entry
|
|
r.Event.Payload = clip(r.Event.Payload, TapEntryBytes)
|
|
e.Message, e.Fields = clip(e.Message, TapEntryBytes), clip(e.Fields, TapEntryBytes)
|
|
e.Level, e.Source, e.Node = clip(e.Level, TapNameBytes), clip(e.Source, TapNameBytes), clip(e.Node, TapNameBytes)
|
|
select {
|
|
case t.ch <- r:
|
|
default:
|
|
t.drops.Add(1)
|
|
}
|
|
}
|
|
|
|
func (t *Tap) Records() <-chan Tapped { return t.ch }
|
|
|
|
// Dropped counts the records a full tap could not take
|
|
func (t *Tap) Dropped() uint64 { return t.drops.Load() }
|
|
|
|
// clip copies the first n bytes of a longer s, so a kept record does not pin
|
|
// its line
|
|
func clip[T ~string | ~[]byte](s T, n int) T {
|
|
if len(s) <= n {
|
|
return s
|
|
}
|
|
head := make([]byte, n)
|
|
copy(head, s)
|
|
return T(head)
|
|
}
|
|
|
|
// Level is a level: the words naming it in a line, and the further values a
|
|
// level key may hold, which prose would misread ("critical path")
|
|
type Level struct {
|
|
Level string `json:"level"`
|
|
Names []string `json:"names"`
|
|
Keyed []string `json:"-"`
|
|
}
|
|
|
|
// Levels are the entry levels by rising severity: the sources, the console
|
|
// sink's painter and the viewer share them
|
|
var Levels = []Level{
|
|
{"TRACE", []string{"TRACE"}, []string{"VERBOSE"}},
|
|
{"DEBUG", []string{"DEBUG", "DBG"}, nil},
|
|
{"INFO", []string{"INFO", "INF"}, []string{"I", "NOTICE", "INFORMATION"}},
|
|
{"WARN", []string{"WARN", "WARNING"}, []string{"W"}},
|
|
{"ERROR", []string{"ERROR", "ERR", "FATAL"}, []string{"E", "CRIT", "CRITICAL", "ALERT", "EMERG", "EMERGENCY", "PANIC", "DPANIC"}},
|
|
}
|
|
|
|
// LevelWord finds the first whole word of text, in any case, that names a
|
|
// level, or names want when it is set; it returns the level and the word's
|
|
// bounds, and "" when no word does.
|
|
func LevelWord[T string | []byte](text T, want string) (level string, start, end int) {
|
|
for i := 0; i < len(text); i = end {
|
|
for i < len(text) && !wordByte(text[i]) {
|
|
i++
|
|
}
|
|
for end = i; end < len(text) && wordByte(text[end]); end++ {
|
|
}
|
|
for _, l := range Levels {
|
|
for _, name := range l.Names {
|
|
if end-i == len(name) && (want == "" || want == l.Level) && strings.EqualFold(string(text[i:end]), name) {
|
|
return l.Level, i, end
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return "", 0, 0
|
|
}
|
|
|
|
func wordByte(c byte) bool {
|
|
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_'
|
|
}
|