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 ¶
- func RegisterRoutes(api huma.API, sys *System)
- func RegisterRoutesWithDocker(api huma.API, sys *System, docker *DockerLogsReader)
- type Config
- type DockerLogsReader
- type Entry
- type FetchOptions
- type LevelKnob
- type Plugin
- type RingBuffer
- type System
- type TeeHandler
- func (h *TeeHandler) AddSink(s slog.Handler)
- func (h *TeeHandler) Enabled(ctx context.Context, level slog.Level) bool
- func (h *TeeHandler) Handle(ctx context.Context, r slog.Record) error
- func (h *TeeHandler) WithAttrs(attrs []slog.Attr) slog.Handler
- func (h *TeeHandler) WithGroup(name string) slog.Handler
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RegisterRoutes ¶
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 ¶
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 ¶
NewLevelKnob initialises the knob at the given level string. Falls back to "info" on unknown input — never errors at construction.
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:
- Implementing this interface in pulse/pkg/log/plugins/<name>/.
- Returning a constructor func(context.Context, Config) (Plugin, error).
- 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 ¶
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.
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 ¶
Enabled honors the LevelKnob first; if no knob, asks the sinks; if neither answers, accepts iff a ring buffer is attached.
Source Files
¶
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. |