kubernetes/third_party/forked/go-kmsg-parser/kmsgparser/kmsgparser_linux.go
Davanum Srinivas 0e1f1daae1
kubelet: read OOM events in-tree and drop the cadvisor oomparser dependency
The kubelet used cadvisor's lib/utils/oomparser to watch the kernel log for
OOM kills and to produce the container_oom_events_total metric. That made the
kubelet depend on the cadvisor lib module for OOM handling.

This moves OOM handling in-tree:

- The OOM kill parser is forked from cadvisor into
  third_party/forked/cadvisor/oomparser. It reads /dev/kmsg and extracts OOM
  events, adapted to take a context.Context and log through klog.
- The kmsg reader is forked from github.com/euank/go-kmsg-parser into
  third_party/forked/go-kmsg-parser/kmsgparser.
- The kubelet OOM watcher reads OOM events through the forked parser and
  records the container_oom_events_total metric in-tree.
- cadvisor/lib is bumped to v0.60.3, which no longer ships the OOM parser.
  This removes the transitive github.com/euank/go-kmsg-parser
  v2.0.0+incompatible dependency and the vendored cadvisor lib/utils/oomparser
  package.

Behavior is unchanged. OOM events are still emitted and the metric still
reports the same counts.
2026-06-26 20:35:19 -04:00

261 lines
7.4 KiB
Go

/*
Copyright 2016 Euan Kemp
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package kmsgparser implements a parser for the Linux `/dev/kmsg` format.
// More information about this format may be found here:
// https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
// Some parts of it are slightly inspired by rsyslog's contrib module:
// https://github.com/rsyslog/rsyslog/blob/v8.22.0/contrib/imkmsg/kmsg.c
//
// This package is forked from github.com/euank/go-kmsg-parser/v3 at v3.0.0:
// https://github.com/euank/go-kmsg-parser/blob/v3.0.0/kmsgparser/kmsgparser.go
// The upstream Logger abstraction (log.go) is dropped; the parser logs through
// klog instead.
package kmsgparser
import (
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"syscall"
"time"
"k8s.io/klog/v2"
)
// Parser is a parser for the kernel ring buffer found at /dev/kmsg
type Parser interface {
// SeekEnd moves the parser to the end of the kmsg queue.
SeekEnd() error
// Parse reads from kmsg and provides a channel of messages.
// Parse will always close the provided channel before returning.
// Parse may be canceled by calling 'Close' on the parser.
//
// The caller should drain the channel after calling [Close]. The caller must
// not close the channel passed in.
Parse(chan<- Message) error
Close() error
}
// Message represents a given kmsg logline, including its timestamp (as
// calculated based on offset from boot time), its possibly multi-line body,
// and so on. More information about these mssages may be found here:
// https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
type Message struct {
Priority int
SequenceNumber int
Timestamp time.Time
Message string
}
// NewParser constructs a new parser with the given Options.
func NewParser(opts ...Option) (Parser, error) {
f, err := os.Open("/dev/kmsg")
if err != nil {
return nil, err
}
bootTime, err := getBootTime()
if err != nil {
return nil, err
}
p := &parser{
kmsgReader: f,
bootTime: bootTime,
follow: true,
}
for _, opt := range opts {
opt(p)
}
return p, nil
}
// Option is a configuration option for [NewParser]
type Option func(p *parser)
// WithNoFollow configures [Parser] to stop reading and close the channel when
// it reaches the end of the kmsg buffer.
func WithNoFollow() func(p *parser) {
return func(p *parser) {
p.follow = false
}
}
type parser struct {
kmsgReader *os.File
bootTime time.Time
// follow indicates whether we should stop when we hit the end of the kmsg
// buffer, or keep reading. Similar to dmesg --follow.
// Default true
follow bool
}
func getBootTime() (time.Time, error) {
var sysinfo syscall.Sysinfo_t
err := syscall.Sysinfo(&sysinfo)
if err != nil {
return time.Time{}, fmt.Errorf("could not get boot time: %v", err)
}
// sysinfo only has seconds
return time.Now().Add(-1 * (time.Duration(sysinfo.Uptime) * time.Second)), nil
}
func (p *parser) SeekEnd() error {
_, err := p.kmsgReader.Seek(0, io.SeekEnd)
return err
}
func (p *parser) Close() error {
return p.kmsgReader.Close()
}
// Parse reads from the provided reader and provide a channel of messages
// parsed.
// If the provided reader *is not* a proper Linux kmsg device, Parse might not
// behave correctly since it relies on specific behavior of `/dev/kmsg`
//
// The caller should typically run 'Parse' in a goroutine.
func (p *parser) Parse(msgs chan<- Message) error {
// with follow (dmesg --follow), we can use go's usual way of reading thing (i.e. epoll + nonblocking IO).
if p.follow {
return p.readFollow(msgs)
}
return p.readNofollow(msgs)
}
func (p *parser) readFollow(msgs chan<- Message) error {
defer close(msgs)
msg := make([]byte, 8192)
for {
// Each read call gives us one full message.
// https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
n, err := p.kmsgReader.Read(msg)
switch {
case err == nil:
case errors.Is(err, syscall.EPIPE):
klog.Warningf("short read from kmsg; skipping")
continue
case errors.Is(err, io.EOF), errors.Is(err, os.ErrClosed):
// someone closed us
return nil
default:
return fmt.Errorf("unexpected kmsg reading error: %w", err)
}
msgStr := string(msg[:n])
message, err := p.parseMessage(msgStr)
if err != nil {
return fmt.Errorf("malformed kmsg message: %w", err)
}
msgs <- message
}
}
func (p *parser) readNofollow(msgs chan<- Message) error {
defer close(msgs)
rawReader, err := p.kmsgReader.SyscallConn()
if err != nil {
return err
}
// we're forced to put the fd into non-blocking mode to be able to detect the
// end of the buffer, but to not use go's built-in epoll
if ctrlErr := rawReader.Control(func(fd uintptr) {
err = syscall.SetNonblock(int(fd), true)
}); ctrlErr != nil {
return fmt.Errorf("error calling control on kmsg reader: %w", ctrlErr)
}
if err != nil {
return fmt.Errorf("unable to set nonblocking on fd: %w", err)
}
msg := make([]byte, 8192)
for {
// Each read call gives us one full message.
// https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
var err error
var n int
readErr := rawReader.Read(func(fd uintptr) bool {
n, err = syscall.Read(int(fd), msg)
return true
})
if readErr != nil && err == nil {
err = readErr
}
switch {
case err == nil:
case errors.Is(err, syscall.EPIPE):
klog.Warningf("short read from kmsg; skipping")
continue
case errors.Is(err, syscall.EAGAIN):
// end of ring buffer in nofollow mode, we're done
return nil
default:
return fmt.Errorf("unexpected kmsg reading error: %w", err)
}
msgStr := string(msg[:n])
message, err := p.parseMessage(msgStr)
if err != nil {
return fmt.Errorf("malformed kmsg message: %w", err)
}
msgs <- message
}
}
func (p *parser) parseMessage(input string) (Message, error) {
// Format:
// PRIORITY,SEQUENCE_NUM,TIMESTAMP,-;MESSAGE
parts := strings.SplitN(input, ";", 2)
if len(parts) != 2 {
return Message{}, fmt.Errorf("invalid kmsg; must contain a ';'")
}
metadata, message := parts[0], parts[1]
metadataParts := strings.Split(metadata, ",")
if len(metadataParts) < 3 {
return Message{}, fmt.Errorf("invalid kmsg: must contain at least 3 ',' separated pieces at the start")
}
priority, sequence, timestamp := metadataParts[0], metadataParts[1], metadataParts[2]
prioNum, err := strconv.Atoi(priority)
if err != nil {
return Message{}, fmt.Errorf("could not parse %q as priority: %v", priority, err)
}
sequenceNum, err := strconv.Atoi(sequence)
if err != nil {
return Message{}, fmt.Errorf("could not parse %q as sequence number: %v", priority, err)
}
timestampUsFromBoot, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return Message{}, fmt.Errorf("could not parse %q as timestamp: %v", priority, err)
}
// timestamp is offset in microsecond from boottime.
msgTime := p.bootTime.Add(time.Duration(timestampUsFromBoot) * time.Microsecond)
return Message{
Priority: prioNum,
SequenceNumber: sequenceNum,
Timestamp: msgTime,
Message: message,
}, nil
}