flightrecorder

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 10 Imported by: 0

README

flightrecorder

Go runtime execution trace flight recorder middleware for cmdguard.

Wraps Go 1.25+ runtime/trace.FlightRecorder to continuously buffer execution traces in memory and automatically capture .trace snapshots when commands are slow or error. Zero external dependencies — uses only the Go standard library.

Installation

go get github.com/larsartmann/cmdguard/flightrecorder

Quick Start

package main

import (
    "time"

    "github.com/larsartmann/cmdguard/flightrecorder"
    v4 "github.com/larsartmann/cmdguard/v4/pkg/cmdguard/v4"
)

type Config struct{}

func main() {
    cli, _ := v4.NewCLI[Config]("myapp", "My CLI", Config{},
        flightrecorder.WithFlightRecorder[Config](flightrecorder.Config{
            CaptureOnSlow:  true,
            SlowThreshold:  200 * time.Millisecond,
            CaptureOnError: true,
            OutputDir:      "/tmp/myapp-traces",
        }),
    )
    _ = cli.Execute()
}

Snapshots are written as {prefix}-{command}-{reason}-{timestamp}.trace files in OutputDir (defaults to os.TempDir()). Analyze them with:

go tool trace /tmp/myapp-traces/cmdguard-deploy-error-20260801-154032.000000000.trace

Manual Lifecycle Control

For explicit Start/Stop control (e.g. multiple CLIs sharing one recorder):

rec := flightrecorder.New(flightrecorder.DefaultConfig())

if err := rec.Start(); err != nil {
    log.Fatal(err)
}
defer rec.Stop()

// Capture a snapshot at any time:
path, err := rec.Capture(ctx, "manual-snapshot", flightrecorder.CaptureReasonSlow)

// Or write to an arbitrary io.Writer:
n, err := rec.CaptureToWriter(ctx, os.Stdout, "deploy", flightrecorder.CaptureReasonError)

Configuration

Field Type Default Description
MinAge time.Duration 5s Minimum time trace data is retained in the in-memory buffer
MaxBytes uint64 10 MiB Maximum in-memory buffer size
CaptureOnSlow bool true Auto-capture when a command exceeds SlowThreshold
SlowThreshold time.Duration 100ms Duration above which a command triggers a slow snapshot
CaptureOnError bool false Auto-capture when a command returns an error
OutputDir string "" (tmpdir) Directory for .trace snapshot files (created if missing)
FilenamePrefix string "cmdguard" Prepended to snapshot filenames
Log func(...) nil (stderr) Diagnostic log function (snapshot captured, start failed, etc.)

Constraints

  • Process-wide singleton: at most one flight recorder may be active per process (runtime/trace limitation). If you create multiple Recorder instances, only the first Start() succeeds; subsequent starts return ErrAlreadyStarted.
  • Non-blocking: the middleware never blocks command execution on recorder operations. Snapshot capture runs in a background goroutine.
  • Error precedence: when both CaptureOnSlow and CaptureOnError are enabled and a command is both slow AND errors, the error reason takes precedence.

Documentation

Overview

Package flightrecorder provides Go runtime execution trace recording for cmdguard CLIs via the flight recorder pattern (Go 1.25+ runtime/trace).

The flight recorder continuously buffers the last few seconds of execution trace in memory. When something goes wrong — a slow command, an error, or a panic — you can snapshot the buffer to capture exactly the problematic window of time. The resulting .trace file can be analyzed with `go tool trace`.

This is an optional module — it has zero external dependencies (uses only the Go standard library runtime/trace package). Import it only when you need execution trace diagnostics.

Quick start

Wire it as a single CLIOption and every command that exceeds the slow threshold or returns an error will automatically produce a .trace file:

import (
    v4 "github.com/larsartmann/cmdguard/v4/pkg/cmdguard/v4"
    "github.com/larsartmann/cmdguard/flightrecorder"
)

cli, _ := v4.NewCLI[Config]("app", "My app", Config{},
    flightrecorder.WithFlightRecorder[Config](flightrecorder.Config{
        CaptureOnSlow:    true,
        SlowThreshold:    200 * time.Millisecond,
        CaptureOnError:   true,
    }),
)

Manual control

For advanced use cases (custom capture triggers, shared recorder across multiple CLIs), use the Recorder and Middleware types directly:

rec := flightrecorder.New(flightrecorder.DefaultConfig())
if err := rec.Start(); err != nil {
    log.Printf("flight recorder: %v", err)
}
defer rec.Stop()

cli, _ := v4.NewCLI[Config]("app", "My app", Config{},
    v4.WithMiddleware(flightrecorder.Middleware[Config](rec)),
)

Analyzing traces

After a snapshot is captured, analyze it with:

go tool trace /path/to/snapshot.trace

This launches a local web server with an interactive trace viewer.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrAlreadyStarted is returned when Start is called on an already-running recorder.
	ErrAlreadyStarted = errors.New("flightrecorder: recorder already started")

	// ErrNotEnabled is returned when an operation requires an active recorder but none is running.
	ErrNotEnabled = errors.New("flightrecorder: recorder not enabled")
)

Sentinel errors for Recorder operations.

Functions

func Middleware

func Middleware[T any](rec *Recorder) v4.Middleware[T]

Middleware returns a cmdguard middleware that manages the flight recorder lifecycle and automatically captures trace snapshots when configured conditions are met.

The recorder is lazily started on the first command invocation. Snapshots are only captured during the run phase to avoid duplicate captures from pre-run/post-run phases.

The middleware never blocks command execution on recorder operations: if Start fails (e.g. another recorder is already active), the command proceeds without tracing. Snapshot capture runs in a background goroutine so the command response is not delayed.

The returned error from next is always passed through unchanged — the middleware only adds observability, it never changes program behavior.

func WithFlightRecorder

func WithFlightRecorder[T any](cfg Config) v4.CLIOption

WithFlightRecorder is a convenience CLIOption that creates a Recorder from the given Config and registers it as middleware via v4.WithMiddleware.

The recorder is lazily started on first command execution. For explicit lifecycle control (Start/Stop), use New + Middleware instead.

Example:

cli, _ := v4.NewCLI[Config]("app", "My app", Config{},
    flightrecorder.WithFlightRecorder[Config](flightrecorder.Config{
        CaptureOnSlow:  true,
        SlowThreshold:  200 * time.Millisecond,
        CaptureOnError: true,
    }),
)

func WithFlightRecorderRecorder

func WithFlightRecorderRecorder[T any](rec *Recorder) v4.CLIOption

WithFlightRecorderRecorder is a CLIOption that registers a pre-created Recorder as middleware. Unlike WithFlightRecorder (which creates a new Recorder from Config), this variant gives the caller explicit lifecycle control — useful when multiple CLIs share a single recorder or when Start/Stop must be called at specific times.

Example:

rec := flightrecorder.New(flightrecorder.DefaultConfig())
defer rec.Stop()

cli, _ := v4.NewCLI[Config]("app", "My app", Config{},
    flightrecorder.WithFlightRecorderRecorder[Config](rec),
)

Types

type CaptureReason

type CaptureReason string

CaptureReason describes why a snapshot was taken.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/cmdguard/flightrecorder"
)

func main() {
	fmt.Println(flightrecorder.CaptureReasonSlow)
	fmt.Println(flightrecorder.CaptureReasonError)

}
Output:
slow
error
const (
	// CaptureReasonSlow indicates the command exceeded the configured slow threshold.
	CaptureReasonSlow CaptureReason = "slow"

	// CaptureReasonError indicates the command returned an error.
	CaptureReasonError CaptureReason = "error"
)

type Config

type Config struct {
	// MinAge is the duration for which trace data is reliably retained in the
	// in-memory buffer. Older data may be evicted. Suggested: ~2x the time
	// window of the event you are debugging (e.g. if debugging a 5-second
	// timeout, set to 10 seconds).
	//
	// Default: 5 * time.Second
	MinAge time.Duration

	// MaxBytes limits the in-memory buffer size to prevent unbounded memory
	// growth. Expect roughly 10 MB/s of trace data for a busy service.
	//
	// Default: 10 MiB (10 << 20)
	MaxBytes uint64

	// CaptureOnSlow controls whether a snapshot is automatically captured when
	// a command's run phase exceeds SlowThreshold.
	//
	// Default: true
	CaptureOnSlow bool

	// SlowThreshold is the duration above which a command is considered slow
	// and triggers a snapshot (when CaptureOnSlow is true).
	//
	// Default: 100 * time.Millisecond
	SlowThreshold time.Duration

	// CaptureOnError controls whether a snapshot is automatically captured when
	// a command returns a non-nil error from its run phase.
	//
	// Default: false
	CaptureOnError bool

	// OutputDir is the directory where snapshot .trace files are written.
	// The directory is created if it does not exist. If empty, uses os.TempDir().
	//
	// Default: "" (resolves to os.TempDir())
	OutputDir string

	// FilenamePrefix is prepended to snapshot filenames. Snapshot files are
	// named: {prefix}-{command}-{reason}-{timestamp}.trace
	//
	// Default: "cmdguard"
	FilenamePrefix string

	// Log receives diagnostic messages (e.g. snapshot captured, start failed).
	// If nil, messages are written to os.Stderr.
	//
	// Default: nil (os.Stderr)
	Log func(format string, args ...any)
}

Config configures the flight recorder behavior.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with sensible defaults for typical CLI usage.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/cmdguard/flightrecorder"
)

func main() {
	cfg := flightrecorder.DefaultConfig()

	fmt.Println("capture on slow:", cfg.CaptureOnSlow)
	fmt.Println("slow threshold:", cfg.SlowThreshold)
	fmt.Println("capture on error:", cfg.CaptureOnError)
	fmt.Println("filename prefix:", cfg.FilenamePrefix)

}
Output:
capture on slow: true
slow threshold: 100ms
capture on error: false
filename prefix: cmdguard

type Recorder

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

Recorder wraps runtime/trace.FlightRecorder with snapshot management. It is safe for concurrent use.

At most one flight recorder may be active per process at any given time (runtime/trace limitation). If you create multiple Recorders, only the first Start will succeed; subsequent starts return an error.

func New

func New(cfg Config) *Recorder

New creates a Recorder from the given Config. The recorder is NOT started — call Start to begin recording. Zero-valued Config fields are replaced with defaults.

Example
package main

import (
	"fmt"
	"time"

	"github.com/larsartmann/cmdguard/flightrecorder"
)

func main() {
	rec := flightrecorder.New(flightrecorder.Config{
		CaptureOnSlow:  true,
		SlowThreshold:  500 * time.Millisecond,
		CaptureOnError: true,
		OutputDir:      "/tmp/myapp-traces",
	})

	fmt.Println("enabled before start:", rec.Enabled())

	if err := rec.Start(); err != nil {
		fmt.Println("failed to start:", err)

		return
	}

	defer rec.Stop()

	fmt.Println("enabled after start:", rec.Enabled())

}
Output:
enabled before start: false
enabled after start: true

func (*Recorder) Capture

func (rec *Recorder) Capture(ctx context.Context, commandName string, reason CaptureReason) (string, error)

Capture writes a trace snapshot to a file in OutputDir. The filename includes the command name, capture reason, and timestamp. Returns the path to the written file.

If the recorder is not enabled, Capture returns an error without writing.

func (*Recorder) CaptureToWriter

func (rec *Recorder) CaptureToWriter(
	ctx context.Context,
	writer io.Writer,
	commandName string,
	reason CaptureReason,
) (int64, error)

CaptureToWriter writes a trace snapshot to the given writer instead of a file. This decouples snapshot writing from the filesystem, useful for piping to stdout, network storage, or testing. Returns the number of bytes written.

If the recorder is not enabled, CaptureToWriter returns an error without writing.

func (*Recorder) Config

func (rec *Recorder) Config() Config

Config returns the effective configuration (with defaults applied).

func (*Recorder) Enabled

func (rec *Recorder) Enabled() bool

Enabled reports whether the flight recorder is actively recording.

func (*Recorder) Start

func (rec *Recorder) Start() error

Start begins recording execution traces into the in-memory buffer. Returns an error if the recorder is already started or if the runtime rejects the start (e.g. another flight recorder is already active).

func (*Recorder) Stop

func (rec *Recorder) Stop()

Stop stops recording. Safe to call multiple times. After stopping, the recorder can be restarted with Start.

Stop waits for any in-flight snapshot capture (WriteTo/Capture) to complete before stopping the underlying recorder, preventing races between snapshot writes and recorder shutdown.

func (*Recorder) WriteTo

func (rec *Recorder) WriteTo(writer io.Writer) (int64, error)

WriteTo writes the current contents of the trace buffer to writer. Returns the number of bytes written. If the recorder is not enabled, returns an error.

Jump to

Keyboard shortcuts

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