Linux audit (auditd) source and LogStream internal logs
Audit source (source_type=audit), off until enabled in Settings > Sources: - follows /var/log/audit/audit.log (path configurable, AUDIT_LOG), with rotation (end of audit.log.1 read first) and an offset checkpoint in /data/audit-state.json; last AUDIT_BACKFILL (1h) read on first enable - groups the records of an event by msg=audit(time:serial), closed on EOE, after 2 s without a record, or once the log has moved 2 s past it - fields audit_type, audit_serial, auid, user (enriched name, host /etc/passwd or uid), exe, res, key, audit_raw; app = audit_type, host = node= / AUDIT_HOST / /host/etc/hostname; summary "who: what [key] → result"; warning when res=failed / success=no, info otherwise - hexadecimal values (proctitle, cmd, exec arguments…) decoded Internal logs (source_type=logstream, host=logstream, app=component): - logEvent() for VictoriaLogs unreachable / back, lost batches, spooled batches refused, syslog listener errors, Docker API and container streams lost, failing sources, start and stop - on by default from warning; switch and minimum level in Settings > Sources (/data/selflog.json) - bounded queue, never waits for the Store, same event at most once per 30 s with a count of the repeats: a failing Store cannot loop UI: violet (audit) and turquoise (LogStream) left edge and tint, AUDIT / LS badge before the app, raw audit record under the summary (folded in compact density until the row is opened), Audit and LogStream in the source filter, FR/EN translations. docker-compose: optional /var/log/audit, /etc/passwd and /etc/hostname mounts. README (EN/FR): host prerequisites (log_group, ACL for UID 10001). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f3373e1f66
commit
076c01aac4
20 files changed
+1948
-36
No files matched your search
+187
@@ -0,0 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// selfLogConfig is saved in /data/selflog.json and edited in Settings > Sources.
|
||||
type selfLogConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MinLevel string `json:"min_level"` // severity name: "err", "warning", "notice", "info"…
|
||||
}
|
||||
|
||||
// SelfLog turns the internal events of LogStream (storage errors, listener
|
||||
// failures, sources that stop…) into log entries, with host=logstream and
|
||||
// app=component. Events go through a bounded queue and a per-message
|
||||
// throttle: an event about a failing Store is itself stored through that
|
||||
// Store, so it must never be able to loop or pile up.
|
||||
type SelfLog struct {
|
||||
cfgPath string
|
||||
sink func(*Entry)
|
||||
queue chan *Entry
|
||||
interval time.Duration // one event per component and message format per interval
|
||||
|
||||
mu sync.Mutex
|
||||
cfg selfLogConfig
|
||||
last map[string]time.Time
|
||||
suppressed map[string]int
|
||||
emitted uint64
|
||||
dropped uint64
|
||||
throttled uint64
|
||||
}
|
||||
|
||||
// selfLog receives the events of logEvent; nil (tests) only writes them to
|
||||
// the process log.
|
||||
var selfLog *SelfLog
|
||||
|
||||
func NewSelfLog(dataDir string, sink func(*Entry)) *SelfLog {
|
||||
s := &SelfLog{
|
||||
cfgPath: filepath.Join(dataDir, "selflog.json"),
|
||||
sink: sink,
|
||||
queue: make(chan *Entry, 256),
|
||||
interval: 30 * time.Second,
|
||||
cfg: selfLogConfig{Enabled: true, MinLevel: "warning"},
|
||||
last: map[string]time.Time{},
|
||||
suppressed: map[string]int{},
|
||||
}
|
||||
if b, err := os.ReadFile(s.cfgPath); err == nil {
|
||||
var cfg selfLogConfig
|
||||
if json.Unmarshal(b, &cfg) == nil {
|
||||
s.cfg = normSelfLogConfig(cfg)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func normSelfLogConfig(cfg selfLogConfig) selfLogConfig {
|
||||
if sevNum(cfg.MinLevel) < 0 {
|
||||
cfg.MinLevel = "warning"
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func sevNum(name string) int {
|
||||
for i, n := range severityNames {
|
||||
if n == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// logEvent writes an internal event to the process log and, when the
|
||||
// self-logs source is on and the level is high enough, emits it as an entry.
|
||||
// sev is a syslog severity (3 err, 4 warning, 5 notice, 6 info).
|
||||
func logEvent(component string, sev int, format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
log.Printf("%s: %s", component, msg)
|
||||
if s := selfLog; s != nil {
|
||||
s.Emit(component, sev, format, msg, time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
// Emit queues an event without ever blocking. The same component and message
|
||||
// format are let through once per interval; the next one says how many were
|
||||
// left out in between.
|
||||
func (s *SelfLog) Emit(component string, sev int, format, msg string, now time.Time) {
|
||||
sev = min(max(sev, 0), 7)
|
||||
key := component + "\x00" + format
|
||||
s.mu.Lock()
|
||||
if !s.cfg.Enabled || sev > sevNum(s.cfg.MinLevel) {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if t, ok := s.last[key]; ok && now.Sub(t) < s.interval {
|
||||
s.suppressed[key]++
|
||||
s.throttled++
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.last[key] = now
|
||||
if n := s.suppressed[key]; n > 0 {
|
||||
msg += fmt.Sprintf(" (%d similar events not shown)", n)
|
||||
delete(s.suppressed, key)
|
||||
}
|
||||
if len(s.last) > 1000 { // formats are constants: only a bug could get here
|
||||
clear(s.last)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
e := &Entry{
|
||||
Time: now,
|
||||
Received: now,
|
||||
Host: "logstream",
|
||||
App: component,
|
||||
Facility: "syslog",
|
||||
Severity: severityNames[sev],
|
||||
SevNum: sev,
|
||||
Message: msg,
|
||||
Proto: "internal",
|
||||
SourceType: "logstream",
|
||||
// Wait stays false: when the Store queue is full, the event is dropped
|
||||
// instead of blocking the component that reports it.
|
||||
}
|
||||
select {
|
||||
case s.queue <- e:
|
||||
default:
|
||||
s.mu.Lock()
|
||||
s.dropped++
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Run hands the queued events to the sink until the context ends, then
|
||||
// delivers what is left (the stop event among them).
|
||||
func (s *SelfLog) Run(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case e := <-s.queue:
|
||||
s.deliver(e)
|
||||
case <-ctx.Done():
|
||||
for {
|
||||
select {
|
||||
case e := <-s.queue:
|
||||
s.deliver(e)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SelfLog) deliver(e *Entry) {
|
||||
s.sink(e)
|
||||
s.mu.Lock()
|
||||
s.emitted++
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Configure saves and applies the configuration.
|
||||
func (s *SelfLog) Configure(cfg selfLogConfig) error {
|
||||
cfg = normSelfLogConfig(cfg)
|
||||
s.mu.Lock()
|
||||
s.cfg = cfg
|
||||
s.mu.Unlock()
|
||||
return writeJSONFile(s.cfgPath, cfg)
|
||||
}
|
||||
|
||||
// Status is the state shown in Settings > Sources.
|
||||
func (s *SelfLog) Status() map[string]any {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return map[string]any{
|
||||
"enabled": s.cfg.Enabled,
|
||||
"min_level": s.cfg.MinLevel,
|
||||
"emitted": s.emitted,
|
||||
"dropped": s.dropped,
|
||||
"throttled": s.throttled,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user