problems

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 5, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package problems writes problem records (panics and coded errors) of a frostyard app to the local systemd journal, and reads them back.

The contract is frostyard/core docs/specs/problem-report.md (schema frostyard.problem/1, kind S8 app_problem, and the journal record registry). The package never sends anything over a network: a record is 1 datagram to the local journal socket, and a record that does not fit, or that the journal cannot take at once, is dropped.

A Recorder writes records. Read and ParseJSON read them back from journalctl, and Validate is the one validation path that every reader uses: anyone can write FROSTYARD_* fields to the journal, so every record is untrusted until it passes Validate.

Index

Constants

View Source
const DefaultSocket = "/run/systemd/journal/socket"

DefaultSocket is the journal socket that a Recorder writes to unless WithSocket names another one.

View Source
const MessageID = "8915bdd215c84cbab0c31c05a1a33e7e"

MessageID is the journal MESSAGE_ID of every problem record.

View Source
const Schema = "frostyard.problem/1"

Schema is the value of FROSTYARD_SCHEMA: the schema version whose patterns the record's values follow.

Variables

View Source
var ErrInvalid = errors.New("problems: invalid record")

ErrInvalid is wrapped by every error that Validate returns.

Functions

func DecodeEntry

func DecodeEntry(b []byte) (map[string]string, error)

DecodeEntry decodes 1 journal native protocol datagram. It reads the binary form (NAME\n, a little-endian uint64 length, the value, \n) and the plain form (NAME=value\n). It rejects an empty entry, a repeated field, a bad length, a missing final newline, and a field name that is not ^[A-Z0-9_]+$ or that starts with "_".

func MapFrame

func MapFrame(name string) string

MapFrame returns name when it is an allowed Go frame of schema v1 (it matches the v1 frame pattern and is at most 160 bytes), and "other" otherwise.

Types

type Component

type Component string

Component is the part of an app that recorded a problem (S8 component).

const (
	ComponentDaemon Component = "daemon"
	ComponentCLI    Component = "cli"
	ComponentGUI    Component = "gui"
	ComponentShell  Component = "shell"
	ComponentOther  Component = "other"
)

The S8 component enum.

type Detail

type Detail struct {
	AppID      string   `json:"app_id"`
	AppVersion string   `json:"app_version"`
	Runtime    string   `json:"runtime"`
	Component  string   `json:"component"`
	Class      string   `json:"class"`
	Code       string   `json:"code,omitempty"`
	Frames     []string `json:"frames,omitempty"`
	Location   string   `json:"location,omitempty"`
}

Detail holds the S8 app_problem detail fields of a valid record. Its JSON form is the S8 detail object.

func Validate

func Validate(fields map[string]string) (Detail, error)

Validate checks the journal fields of 1 record against schema v1 and the journal record registry, and returns its S8 detail. It is the one validation path of every reader: a record that fails is dropped. Fields that do not start with FROSTYARD_ (other than MESSAGE_ID) are not checked.

type Option

type Option func(*Recorder)

Option changes a Recorder made by New.

func WithSocket

func WithSocket(path string) Option

WithSocket sets the path of the journal socket. The default is DefaultSocket. Apps use it in their tests.

type ReadOptions

type ReadOptions struct {
	// User reads the user journal (--user); else Read reads the system
	// journal (--system).
	User bool
	// Since, when not zero, keeps only records at or after it (--since).
	Since time.Time
	// Limit, when above 0, keeps only the newest Limit entries (-n).
	Limit int
	// Journalctl is the path of journalctl; the default is "journalctl".
	Journalctl string
}

ReadOptions selects the records that Read returns.

type Record

type Record struct {
	// Trusted holds the fields that journald adds itself. A client cannot
	// forge them.
	Trusted Trusted
	// Detail holds the validated S8 detail fields: the only fields that can
	// reach a report.
	Detail Detail
	// Message is the MESSAGE text. It is local only and never goes into a
	// report.
	Message string
}

Record is 1 valid problem record.

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder writes problem records of 1 app to the local journal. Make it with New. A nil *Recorder is valid and inert. A Recorder is safe for use by many goroutines.

func New

func New(appID, version string, component Component, opts ...Option) *Recorder

New returns a Recorder for the app appID at release version, in the part component. It never fails: when appID is not in the S8 allowlist, version is not a spec <semver>, or component is not in the S8 enum, the Recorder is inert (it writes nothing) and Err returns why.

func (*Recorder) CrashOutput

func (r *Recorder) CrashOutput() error

CrashOutput does nothing on this OS. On an inert Recorder it returns Err.

func (*Recorder) Err

func (r *Recorder) Err() error

Err returns why the Recorder is inert, or nil when it writes records.

func (*Recorder) Error

func (r *Recorder) Error(code string, err error)

Error records a coded error: class error, PRIORITY 3, FROSTYARD_CODE code, and MESSAGE err.Error() (the code when err is nil), truncated to 2048 bytes. A code that fails ^[a-z][a-z0-9_.]{0,63}$ writes nothing. Error never blocks and never reports a failure to write.

func (*Recorder) RecoverPanic

func (r *Recorder) RecoverPanic()

RecoverPanic records a panic of the calling goroutine and then panics again with the same value. Use it only as `defer r.RecoverPanic()`: recover works only in a function that the defer statement calls directly. When there is no panic, it does nothing.

The record has class panic, PRIORITY 2, MESSAGE the panic value as text, and FROSTYARD_FRAMES the stack, innermost first, at most 32 frames, each mapped by MapFrame. After the re-panic, Go prints the original panic with "[recovered, repanicked]" and the process exits with code 2.

RecoverPanic covers only its own goroutine. It cannot catch a fatal error (concurrent map writes, out of memory, deadlock) or os.Exit; CrashOutput covers those at the next start.

type Result

type Result struct {
	Records []Record
	Dropped int
}

Result holds the valid records that a read found, and the number of entries that it dropped because they failed validation.

func ParseJSON

func ParseJSON(r io.Reader) (Result, error)

ParseJSON reads `journalctl -o json` output: 1 JSON object for each line. It validates each entry with Validate. An entry that is not a JSON object, that has a repeated field (journalctl gives it as an array of strings) or a null value, or that fails Validate, is dropped and counted. A value that journalctl gives as an array of byte numbers (not UTF-8) is decoded first. ParseJSON returns an error only when it cannot read r.

func Read

Read is not supported on this OS: it returns an error that wraps errors.ErrUnsupported.

type Trusted

type Trusted struct {
	UID             string    // _UID
	Comm            string    // _COMM
	SystemdUnit     string    // _SYSTEMD_UNIT
	SystemdUserUnit string    // _SYSTEMD_USER_UNIT
	Cursor          string    // __CURSOR
	Time            time.Time // __REALTIME_TIMESTAMP, in UTC
}

Trusted holds the journald fields of a record.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL