log

package
v0.3.1-0...-3c12cd1 Latest Latest
Warning

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

Go to latest
Published: May 4, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package log is the shared logging foundation for the Beacon ecosystem. Every Beacon service (Pulse, Pilot, Prism, Haul, …) uses this package so logs across the stack share field names, rendering, level controls, and external-sink wiring.

Goals:

  • **Story-telling stdout**: structured JSON to Docker, with consistent field names so a user grepping logs sees verb + object + outcome on every line.
  • **In-app viewer**: every service exposes /api/v1/system/logs (and /api/v1/system/log-level) so a user can search, filter by level, and bump verbosity at runtime — no restart.
  • **Pluggable sinks**: ship to Loki, Vector, files, syslog, etc. via the Plugin interface. Beacon stays unopinionated about which backend you run.
  • **Always works**: a broken plugin doesn't break stdout or the in-app viewer. A misconfigured env var doesn't fail startup.

Quickstart for a service:

logger, system := log.New(log.Config{
    Service: "pulse",
    Level:   "info",
})
defer system.Close(ctx)

// Plugins are added through `system.Add(plugin)`. They show
// up in subsequent log calls without re-creating the logger.
if url := os.Getenv("BEACON_LOG_LOKI_URL"); url != "" {
    p, err := loki.New(loki.Config{URL: url, Service: "pulse"})
    if err == nil { system.Add(p) }
}

// Mount the runtime endpoints onto the service's Huma API.
log.RegisterRoutes(api, system)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterRoutes

func RegisterRoutes(api huma.API, sys *System)

RegisterRoutes wires the system-log endpoints onto api. The System returned by log.New is the source of truth for the buffer + level. Idempotent: safe to call once per service from main.go.

Pass docker=nil to omit the Docker-stdout endpoint (e.g. unit tests, services that intentionally don't read their own container).

func RegisterRoutesWithDocker

func RegisterRoutesWithDocker(api huma.API, sys *System, docker *DockerLogsReader)

RegisterRoutesWithDocker is RegisterRoutes plus the Docker stdout reader for full history. Pass NewDockerLogsReader() (or nil to skip).

Types

type Config

type Config struct {
	// Service identifies the emitting service ("pulse", "pilot", …).
	// Stamped on every record. Required.
	Service string

	// Level is the initial level: "debug", "info", "warn", "error".
	// Unknown values fall back to "info" rather than failing — bad
	// env vars shouldn't block startup. Override at runtime via
	// PUT /api/v1/system/log-level.
	Level string

	// Format is "json" (default) or "text". JSON is what Docker logs
	// + central log servers expect; text is human-readable for local
	// `go run` development.
	Format string

	// BufferSize is the in-memory ring buffer capacity used by the
	// /api/v1/system/logs endpoint when Docker stdout isn't
	// reachable. Default 1000 entries. Set to 0 to disable the
	// buffer entirely (Docker stdout becomes mandatory for the UI).
	BufferSize int

	// Output is where stdout-formatted records go. Defaults to
	// os.Stdout. Override in tests.
	Output *os.File
}

Config configures a logger for one service. Service is required — it shows up in every log line as the "service" field so cross- service grepping works without inferring from the container name.

type DockerLogsReader

type DockerLogsReader struct {
	// SocketPath is the unix socket the daemon listens on. Default
	// "/var/run/docker.sock".
	SocketPath string

	// ContainerID is the container we're reading from. Determined
	// at startup by reading /etc/hostname (Docker stamps the short
	// container ID there) or via the env var BEACON_CONTAINER_ID.
	// Empty string disables the reader (returns "not in docker"
	// errors from every call).
	ContainerID string
}

DockerLogsReader reads stdout from the running container via the Docker socket. Methods are safe to call concurrently from HTTP handlers — each call opens its own connection and drains it.

func NewDockerLogsReader

func NewDockerLogsReader() *DockerLogsReader

NewDockerLogsReader auto-discovers the socket + container ID. Returns a usable reader even when discovery fails — the failure surfaces only on the first call (so service startup doesn't hard-fail just because logs are unreadable).

func (*DockerLogsReader) Available

func (r *DockerLogsReader) Available() bool

Available reports whether the reader is ready to serve. False means either the socket isn't mounted or we couldn't determine our container ID. The HTTP layer maps Available=false to 503 so the UI can fall back to the ring buffer cleanly.

func (*DockerLogsReader) FetchLogs

func (r *DockerLogsReader) FetchLogs(ctx context.Context, opts FetchOptions) ([]Entry, error)

FetchLogs returns the container's stdout/stderr lines as Entries. Lines that parse as JSON (the slog handler emits JSON) populate the structured fields; everything else falls back to a plain "msg" with level=INFO so it still renders in the viewer.

type Entry

type Entry struct {
	Time    time.Time      `json:"time"`
	Level   string         `json:"level"`
	Message string         `json:"msg"`
	Fields  map[string]any `json:"fields,omitempty"`
}

Entry is a single log entry stored in the ring buffer. The shape mirrors what the slog JSON handler emits to stdout — same field names so the in-app Logs UI can render Docker-stdout entries and ring-buffer entries through one component.

type FetchOptions

type FetchOptions struct {
	// Tail is the number of trailing lines to fetch. 0 = all.
	Tail int
	// Since is the lower bound on log timestamps. Zero = no bound.
	Since time.Time
}

FetchOptions narrows what FetchLogs returns.

type LevelKnob

type LevelKnob struct {
	*slog.LevelVar
}

LevelKnob holds the runtime-mutable level. It IS a *slog.LevelVar (embedded so it satisfies slog's Leveler interface directly) plus string-friendly helpers for the HTTP layer.

func NewLevelKnob

func NewLevelKnob(level string) *LevelKnob

NewLevelKnob initialises the knob at the given level string. Falls back to "info" on unknown input — never errors at construction.

func (*LevelKnob) Set

func (k *LevelKnob) Set(level string) error

Set parses level (debug | info | warn | error, case-insensitive) and updates the knob. Returns an error if the input is unknown rather than silently coercing — callers in the HTTP layer return 400 on unknown levels so a typo doesn't get accepted.

func (*LevelKnob) String

func (k *LevelKnob) String() string

String returns the current level as a lowercase keyword suitable for the API response.

type Plugin

type Plugin interface {
	// Name returns a stable identifier for logs/diagnostics
	// ("loki", "file", "vector", …).
	Name() string

	// Handler returns the slog.Handler this plugin contributes to
	// the tee fan-out. Called once at startup.
	Handler() slog.Handler

	// Close flushes any pending writes and releases resources. Called
	// on graceful shutdown. Plugins MUST be tolerant of being closed
	// before they've fully drained — losing a handful of buffered
	// records on shutdown is acceptable.
	Close(ctx context.Context) error
}

Plugin wraps an slog.Handler with metadata + lifecycle. Build plugins by:

  1. Implementing this interface in pulse/pkg/log/plugins/<name>/.
  2. Returning a constructor func(context.Context, Config) (Plugin, error).
  3. Wiring the constructor into the host service's main.go via env-var detection (or making it always-on if it's a default).

The pattern mirrors the existing Pilot plugin layout (downloaders, indexers) so the convention is familiar to anyone touching Beacon's plugin code.

type RingBuffer

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

RingBuffer is a fixed-size, thread-safe circular buffer. Append is O(1); reads copy the snapshot under lock so iteration is safe even while writes continue.

func NewRingBuffer

func NewRingBuffer(size int) *RingBuffer

NewRingBuffer creates a buffer that holds up to size entries before it starts overwriting the oldest.

func (*RingBuffer) Add

func (rb *RingBuffer) Add(e Entry)

Add appends an entry. Overwrites the oldest when full.

func (*RingBuffer) Entries

func (rb *RingBuffer) Entries() []Entry

Entries returns a snapshot of all buffered entries, oldest first.

func (*RingBuffer) Len

func (rb *RingBuffer) Len() int

Len returns how many entries are currently stored.

type System

type System struct {
	Service string
	Level   *LevelKnob
	Buffer  *RingBuffer
	// contains filtered or unexported fields
}

System is the runtime handle to a service's logging stack. Holds the level knob, ring buffer, plugin list, and the underlying tee handler. Each service gets one from log.New and passes it to RegisterRoutes for the HTTP layer.

func New

func New(cfg Config) (*slog.Logger, *System)

New constructs the service's logger and returns it alongside the System handle. Callers typically:

logger, system := log.New(cfg)
slog.SetDefault(logger)
defer system.Close(context.Background())

SetDefault is intentional — Beacon services have lots of code paths that grab slog.Default() implicitly (e.g. the SDK), and we want all of those to see the configured handler too.

func (*System) Add

func (s *System) Add(p Plugin)

Add registers a plugin's handler with the tee fan-out. Plugins added after New start receiving subsequent records — there's no replay of past entries. Safe to call concurrently with logging.

func (*System) Close

func (s *System) Close(ctx context.Context)

Close shuts down all plugins. Idempotent; errors from individual plugins are logged at debug level (so the operator can see them without spam in normal shutdown). Best-effort.

type TeeHandler

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

TeeHandler dispatches records to N sinks plus the ring buffer.

func NewTeeHandler

func NewTeeHandler(buf *RingBuffer, sinks ...slog.Handler) *TeeHandler

NewTeeHandler creates a tee with one or more initial sinks.

func (*TeeHandler) AddSink

func (h *TeeHandler) AddSink(s slog.Handler)

AddSink adds another handler to the fan-out. Visible to every derivative because the underlying registry is shared.

func (*TeeHandler) Enabled

func (h *TeeHandler) Enabled(ctx context.Context, level slog.Level) bool

Enabled honors the LevelKnob first; if no knob, asks the sinks; if neither answers, accepts iff a ring buffer is attached.

func (*TeeHandler) Handle

func (h *TeeHandler) Handle(ctx context.Context, r slog.Record) error

Handle dispatches the record to every sink + the ring buffer.

func (*TeeHandler) WithAttrs

func (h *TeeHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs appends an 'a' op to the chain.

func (*TeeHandler) WithGroup

func (h *TeeHandler) WithGroup(name string) slog.Handler

WithGroup appends a 'g' op to the chain.

Directories

Path Synopsis
plugins
file
Package file is a simple log-to-disk plugin with size-based rotation.
Package file is a simple log-to-disk plugin with size-based rotation.
loki
Package loki ships Beacon logs to a Grafana Loki instance via /loki/api/v1/push.
Package loki ships Beacon logs to a Grafana Loki instance via /loki/api/v1/push.

Jump to

Keyboard shortcuts

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