blob: 03bb6ff05883d31bcbf50797899f13ba795a51e9 [file] [log] [blame]
//go:build linux
// +build linux
package logtree
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
"golang.org/x/sys/unix"
)
const (
loglevelEmergency = 0
loglevelAlert = 1
loglevelCritical = 2
loglevelError = 3
loglevelWarning = 4
loglevelNotice = 5
loglevelInfo = 6
loglevelDebug = 7
)
// KmsgPipe pipes logs from the kernel kmsg interface at /dev/kmsg into the
// given logger.
func KmsgPipe(ctx context.Context, lt LeveledLogger) error {
publisher, ok := lt.(*leveledPublisher)
if !ok {
// Fail fast, as this is a programming error.
panic("Expected *leveledPublisher in LeveledLogger from supervisor")
}
kmsgFile, err := os.Open("/dev/kmsg")
if err != nil {
return err
}
defer kmsgFile.Close()
var lastOverflow time.Time
// PRINTK_MESSAGE_MAX in @linux//kernel/printk:internal.h
linebuf := make([]byte, 2048)
for {
n, err := kmsgFile.Read(linebuf)
// Best-effort, in Go it is not possible to cancel a Read on-demand.
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if errors.Is(err, unix.EPIPE) {
now := time.Now()
// Rate-limit to 1 per second
if lastOverflow.Add(1 * time.Second).Before(now) {
lt.Warning("Lost messages due to kernel ring buffer overflow")
lastOverflow = now
}
continue
}
if err != nil {
return fmt.Errorf("while reading from kmsg: %w", err)
}
var monotonicRaw unix.Timespec
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC_RAW, &monotonicRaw); err != nil {
return fmt.Errorf("while getting monotonic timestamp: %w", err)
}
p := parseKmsg(time.Now(), time.Duration(monotonicRaw.Nano())*time.Nanosecond, linebuf[:n])
if p == nil {
continue
}
e := &entry{
origin: publisher.node.dn,
leveled: p,
}
publisher.node.tree.journal.append(e)
publisher.node.tree.journal.notify(e)
}
}
// See https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg for format.
func parseKmsg(now time.Time, monotonicSinceBoot time.Duration, data []byte) *LeveledPayload {
meta, message, ok := bytes.Cut(data, []byte(";"))
if !ok {
// Unknown message format
return nil
}
endOfMsgIdx := bytes.IndexByte(message, '\n')
if endOfMsgIdx == -1 {
return nil
}
message = message[:endOfMsgIdx]
metaFields := strings.FieldsFunc(string(meta), func(r rune) bool { return r == ',' })
if len(metaFields) < 4 {
return nil
}
loglevel, err := strconv.ParseUint(metaFields[0], 10, 64)
if err != nil {
return nil
}
monotonicMicro, err := strconv.ParseUint(metaFields[2], 10, 64)
if err != nil {
return nil
}
// Kmsg entries are timestamped with CLOCK_MONOTONIC_RAW, a clock which does
// not have a direct correspondence with civil time (UTC). To assign best-
// effort timestamps, use the current monotonic clock reading to determine
// the elapsed time between the kmsg entry and now on the monotonic clock.
// This does not correspond well to elapsed UTC time on longer timescales as
// CLOCK_MONOTONIC_RAW is not trimmed to run true to UTC, but up to in the
// order of hours this is close. As the pipe generally processes messages
// very close to their creation date, the elapsed time and thus the accrued
// error is extremely small.
monotonic := time.Duration(monotonicMicro) * time.Microsecond
monotonicFromNow := monotonic - monotonicSinceBoot
var severity Severity
switch loglevel {
case loglevelEmergency, loglevelAlert:
severity = FATAL
case loglevelCritical, loglevelError:
severity = ERROR
case loglevelWarning:
severity = WARNING
case loglevelNotice, loglevelInfo, loglevelDebug:
severity = INFO
default:
severity = INFO
}
return &LeveledPayload{
timestamp: now.Add(monotonicFromNow),
severity: severity,
messages: []string{string(message)},
}
}