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 ¶
const DefaultSocket = "/run/systemd/journal/socket"
DefaultSocket is the journal socket that a Recorder writes to unless WithSocket names another one.
const MessageID = "8915bdd215c84cbab0c31c05a1a33e7e"
MessageID is the journal MESSAGE_ID of every problem record.
const Schema = "frostyard.problem/1"
Schema is the value of FROSTYARD_SCHEMA: the schema version whose patterns the record's values follow.
Variables ¶
var ErrInvalid = errors.New("problems: invalid record")
ErrInvalid is wrapped by every error that Validate returns.
Functions ¶
func DecodeEntry ¶
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 "_".
Types ¶
type Component ¶
type Component string
Component is the part of an app that recorded a problem (S8 component).
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 ¶
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 ¶
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 ¶
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 ¶
CrashOutput makes the Go runtime copy the report of a crash (an unrecovered panic in any goroutine, or a fatal error) to a crash file, through runtime/debug.SetCrashOutput. At the next start, CrashOutput turns each leftover crash file of a dead process into 1 record.
The folder is $STATE_DIRECTORY (its first entry), else $XDG_STATE_HOME, else $HOME/.local/state, then frostyard-problems/<appID>. The own file is created as <random>.crash-new, locked, given its header, and only then published as <random>.crash, so that a concurrent start never sees an unlocked *.crash file of a live process. Call it once, early in main; a later call in the same process does nothing. On an inert Recorder it returns Err and does nothing. A crash is recorded only at the next start of the app.
func (*Recorder) 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 ¶
Result holds the valid records that a read found, and the number of entries that it dropped because they failed validation.
func ParseJSON ¶
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 ¶
func Read(ctx context.Context, opts ReadOptions) (Result, error)
Read runs journalctl and returns the valid problem records of the user or the system journal:
journalctl --no-pager -q -o json --all --user|--system [--since=@<unix>] [-n <limit>] MESSAGE_ID=<MessageID>
--all keeps journalctl from printing a field longer than 4096 bytes as null (FROSTYARD_FRAMES can be longer). On a non-zero exit, Read returns an error with the start of journalctl's stderr.