Help us shape the future of Go! Take the 2026 Go Developer Survey

logging

package
v1.3.103 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Overview

Package logging pkg/logging/broadcaster.go c0-com-log

Package logging pkg/logging/formatter.go c0-com-log

Package logging pkg/logging/formatter_istty_native.go c0-com-log

Package logging pkg/logging/hooks.go c0-com-log

Package logging pkg/logging/logger.go c0-com-log

Package logging provides application logging utilities

Package logging pkg/logging/logging.go c0-com-log

Package logging pkg/logging/panics.go c0-com-log

Process-wide accounting for RECOVERED panics.

An unrecovered panic is already captured: the visor points debug.SetCrashOutput at local/log/skywire-crash.log, so the full traceback survives the crash. A RECOVERED one has no such coverage — it is handled, the process keeps running, and the only trace is whatever the recovering site chose to log. That is a hole in two directions:

  • The recovery sites disagree about what to log. Some attach debug.Stack(), most log only the panic value, so even when the line is found it often cannot say where the panic came from.
  • On js/wasm there is frequently nowhere to find that line at all: no stderr, the log server is off by default, the log file is not reachable from the page, and both the browser console and the exec output ring roll within minutes. A recovered panic there is simply invisible.

A recovered panic is still a defect — a recovered panic in an RPC handler drops that request — so a visor panicking steadily should not report itself healthy on every diagnostic surface. This records a count and a small ring of the most recent ones so `visor state --select diag` can show them.

Cost is confined to the panicking path: nothing here runs in the absence of a panic.

Package logging pkg/logging/ring.go c0-com-log

Per-app ring buffer on top of the Broadcaster hook.

The Broadcaster is already the single logrus hook the visor installs on its master logger, so its Fire sees every entry. In addition to fanning entries out to live subscribers (the `--verbose` gRPC stream), it records the most recent app-scoped entries into a small bounded ring keyed by app name. This lets a consumer (the proxy status page, `cli proxy log`) read back the recent route/transport events and app log lines for one app WITHOUT having held a subscription open the whole time — reusing the exact same tap the verbose stream uses rather than adding a second log sink.

Two buckets per app:

  • events: layered router / route-group / transport / setup / mux entries that happen on behalf of the app (tagged app_name=<n> by the router's scopedLog / RouteGroup.SetAppName). These are the "[router] …" / "[RouteGroup …] …" lines.
  • logs: the app's own output (_module=proc:<app>:<key> / app:<app>).

Both are bounded (ringPerApp) and the number of tracked apps is capped (ringMaxApps, LRU-evicted) so memory never grows unbounded.

Package logging pkg/logging/ringbuffer.go c0-com-log

Package logging pkg/logging/slog.go c0-com-log

Package logging pkg/logging/stdlog.go c0-com-log

Adapter that lets third-party libraries which only accept a stdlib *log.Logger (e.g. armon/go-socks5's Config.Logger) funnel their output through logrus, so their lines get the same timestamp format, colors, and hooks as the rest of skywire instead of being written raw to stdout.

Index

Constants

View Source
const DefaultHookLevel = logrus.InfoLevel

DefaultHookLevel is the most verbose level a capture hook accepts unless the caller asks for something else.

It is deliberately not logrus.AllLevels. A capture hook is a SECOND complete formatting pass over every entry it accepts: WriteHook.Fire below runs TextFormatter.Format into a fresh buffer for each entry, on top of the pass the logger already did for stdout. Measured on a production dmsg-server running at debug: ~67k lines/min reached the hook, ~10 MB/min of formatted output poured into a 256 KiB RingBuffer (DefaultRingBufferBytes). That is ~1.5 seconds of retained history on the /debug/log endpoint the ring feeds, bought by doubling the formatting cost of every log line in the process.

Info and above is what /debug/log is actually read for, and the services default to info anyway (skyenv.LogLevel), so this changes nothing about what the endpoint returns at the normal level — it only keeps the debug/trace firehose out of the second formatter.

View Source
const DefaultRingBufferBytes = 256 * 1024

DefaultRingBufferBytes is the default capacity of a RingBuffer.

View Source
const PanicRingSize = 8

PanicRingSize is how many recent recovered panics are retained. Small on purpose: each entry carries a full stack trace, and the count — not the ring — is what says whether this is happening a lot.

Variables

View Source
var ExitFunc = os.Exit

ExitFunc ends the program after a Fatal line. A host that runs commands in its own process replaces it (cmdutil.Exit).

View Source
var Stderr = func() io.Writer { return os.Stderr }

Stderr is where a logger with no output of its own writes: the process's stderr, or that of an in-process run (cmdutil replaces it).

Functions

func AddHook

func AddHook(hook logrus.Hook)

AddHook adds a hook to the global logger

func DebugEnabled

func DebugEnabled() bool

DebugEnabled reports whether the master logger emits at debug level or below. Use it to skip building log fields on a hot path: logrus's WithField allocates a new Entry and copies the field map whether or not the level is enabled, so an ungated WithField chain costs the same when the line is never printed.

func Disable

func Disable()

Disable disables the logger completely

func DisableColors

func DisableColors()

DisableColors disables colored logging

func EnableColors

func EnableColors()

EnableColors enables colored logging

func GetLevel

func GetLevel() logrus.Level

GetLevel returns the logger level

func HookLevel

func HookLevel(name string, def logrus.Level) logrus.Level

HookLevel returns the level named by a config's log_hook_level, or def when it is empty or not a level. debug puts debug lines into /debug/log.

func LevelFromString

func LevelFromString(s string) (logrus.Level, error)

LevelFromString returns a logrus.Level from a string identifier.

An unrecognized (or empty) identifier returns logrus.InfoLevel along with an error naming the offending string. It used to fall back to DebugLevel, which meant a missing or misspelled "log_level" silently ran a service at debug in production for callers that logged the error and carried on.

func LevelsUpTo

func LevelsUpTo(level logrus.Level) []logrus.Level

LevelsUpTo returns the logrus levels at least as severe as level, i.e. the slice a logrus.Hook returns from Levels() to be fired only for those.

func LogRecovered

func LogRecovered(log *Logger, site string, v interface{})

LogRecovered records and logs a panic that the caller has ALREADY recovered. Use it from inside a deferred function that has other cleanup to do as well, which is the common shape:

defer func() {
	if rec := recover(); rec != nil {
		logging.LogRecovered(log, "router RPC handler", rec)
	}
	conn.Close()
}()

The stack is captured here rather than taken from the caller, so every site gets one without having to remember debug.Stack(). log may be nil, in which case the panic is recorded but not logged.

func NewStdLogger

func NewStdLogger(log logrus.FieldLogger) *stdlog.Logger

NewStdLogger returns a stdlib *log.Logger that routes every line it receives through the given logrus logger (level auto-detected from a leading "[ERR]"/"[WARN]"/"[DEBUG]" tag, defaulting to info). Hand it to libraries that insist on *log.Logger so their output matches skywire's log format instead of writing uncolored lines straight to stdout.

func NewStdLoggerLevel

func NewStdLoggerLevel(log logrus.FieldLogger, maxLevel logrus.Level) *stdlog.Logger

NewStdLoggerLevel is like NewStdLogger but caps severity: no line is logged more severe than `maxLevel`. Use it for libraries (e.g. a SOCKS proxy serving external clients) whose "[ERR]" lines are routine, client-driven conditions rather than faults of this process.

func RecordPanic

func RecordPanic(site string, v interface{}, stack []byte)

RecordPanic files a recovered panic. site names the recovering location; v is the value recover() returned; stack is the traceback, which may be nil to have one captured here (callers that already hold one should pass it, as a stack taken at the recover point is the useful one).

Safe to call from any goroutine.

func Recover

func Recover(log *Logger, site string)

Recover is the whole-defer form, for a site whose deferred function does nothing but recover:

defer logging.Recover(log, "dmsg read loop")

It deliberately does NOT re-panic. A site that wants the panic to propagate should not be recovering in the first place.

func ResetPanicStats

func ResetPanicStats()

ResetPanicStats clears the count and the ring. For tests.

func SetLevel

func SetLevel(level logrus.Level)

SetLevel sets the logger's minimum log level

func SetOutputTo

func SetOutputTo(w io.Writer)

SetOutputTo sets the logger's output to an io.Writer

func Trace

func Trace(l logrus.FieldLogger, msg string)

Trace emits msg at trace level through a logrus.FieldLogger, whose interface omits the Trace methods (see Logger.Tracef). Callers on a hot path must guard it with TraceEnabled: reaching the entry allocates.

func TraceEnabled

func TraceEnabled() bool

TraceEnabled reports whether the master logger emits at trace level. Same rationale as DebugEnabled; trace carries the per-stream and per-frame diagnostics that are useless in aggregate and expensive to format at volume.

Types

type Broadcaster

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

Broadcaster is a logrus.Hook that fans out entries to a set of subscriber channels. Subscribers register a Filter; only entries that pass the filter are delivered.

The hook is on the hot logging path, so delivery is non-blocking: each subscriber owns a bounded channel, and entries are dropped on full rather than back-pressuring the producer. Drops are counted per-subscriber so the consumer can tell how lossy its window was.

func NewBroadcaster

func NewBroadcaster() *Broadcaster

NewBroadcaster constructs an empty Broadcaster.

func (*Broadcaster) Fire

func (b *Broadcaster) Fire(e *logrus.Entry) error

Fire delivers e to every subscriber whose filter matches. Never blocks on a slow consumer: full subscriber channels increment a drop counter and continue.

The hook is installed on the visor's master logger at AllLevels, so Fire runs on every entry the process emits — measured at ~67k lines/min on a busy node. Both halves are therefore short-circuited: the subscriber fan-out is skipped entirely (no RWMutex acquire, no map range) when nothing is subscribed, which is the steady state, and the ring capture is skipped when it has been turned off. Only when one of them is actually live does an entry pay for field resolution or allocation.

func (*Broadcaster) Levels

func (b *Broadcaster) Levels() []logrus.Level

Levels reports all logrus levels — Broadcaster does its own level filtering per-subscriber so the hook itself runs on every level.

func (*Broadcaster) RecentByApp

func (b *Broadcaster) RecentByApp(app string, minLevel logrus.Level) (events, logs []Record)

RecentByApp returns copies of the recent events and logs captured for app, filtered to entries at least as severe as minLevel, oldest first. Returns nil, nil when nothing has been captured for the app.

func (*Broadcaster) RecentMerged

func (b *Broadcaster) RecentMerged(app string, minLevel logrus.Level) []Record

RecentMerged returns events+logs for app merged into a single time-ordered slice (oldest first), filtered by minLevel. This is the "reading the log" view `cli proxy log` prints.

func (*Broadcaster) SetRingCapture

func (b *Broadcaster) SetRingCapture(enabled bool)

SetRingCapture turns the bounded per-app ring on or off. It is on by default; a process that never reads the ring back (nothing calls RecentByApp / RecentMerged) can turn it off so Fire returns on a single atomic load, before any field resolution or Record allocation.

func (*Broadcaster) Subscribe

func (b *Broadcaster) Subscribe(f Filter, capacity int) (<-chan *logrus.Entry, func() uint64)

Subscribe returns a receive-only channel of matching entries plus a cancel func. The channel is closed when cancel is called. Capacity is the per-subscriber buffer — entries beyond it are dropped (see Dropped).

type ColorScheme

type ColorScheme struct {
	InfoLevelStyle   string
	WarnLevelStyle   string
	ErrorLevelStyle  string
	FatalLevelStyle  string
	PanicLevelStyle  string
	DebugLevelStyle  string
	TraceLevelStyle  string
	PrefixStyle      string
	TimestampStyle   string
	CallContextStyle string
	CriticalStyle    string
}

ColorScheme configures the logging output colors

type Filter

type Filter struct {
	AppName  string
	Modules  []string
	MinLevel logrus.Level
}

Filter selects which entries reach a subscriber.

AppName, when set, matches entries whose _module equals AppName OR whose "app" or "app_name" field equals AppName. The launcher and proc-manager tag their per-app calls with app_name=<n>, and each app's stdout/stderr is field-tagged _module=<n> by the proc-spawning code, so AppName-only mode picks up everything that is genuinely scoped to the named app.

Module is matched as a prefix on the _module field (e.g. "router" matches "router", "router_setup", "router/foo"). Multiple modules are OR'd together. Empty Modules means "no module-prefix match"; AppName is the only path to acceptance.

AppName + Modules are OR-merged: an entry passes if either the app match fires or one of the module prefixes matches. With Modules non-empty the filter is a firehose for those modules — the visor has no per-app tagging on router/transport/AR entries, so prefix-matching them is "show me anything from layer X" rather than "show me what layer X is doing for app Y".

MinLevel is inclusive — entries strictly less severe than MinLevel are dropped. logrus levels descend from Panic (0) to Trace (6), so "less severe" means "higher numerically".

type Logger

type Logger struct {
	logrus.FieldLogger
}

Logger wraps logrus.FieldLogger

func MustGetLogger

func MustGetLogger(module string) *Logger

MustGetLogger returns a package-aware logger from the master logger

func ScopedLogger

func ScopedLogger(module string, level logrus.Level) *Logger

ScopedLogger returns a package-aware logger derived from the global master logger whose minimum level is independent of it. See MasterLogger.ScopedLogger.

func (*Logger) Critical

func (logger *Logger) Critical() logrus.FieldLogger

Critical adds special critical-level fields for specially highlighted logging, since logrus lacks a distinct critical field and does not have configurable log levels

func (*Logger) IsLevelEnabled added in v1.3.100

func (logger *Logger) IsLevelEnabled(level logrus.Level) bool

IsLevelEnabled reports whether entries at level would be written, so a hot path can skip building fields for a disabled level.

func (*Logger) Level

func (logger *Logger) Level() logrus.Level

Level reports the minimum level this logger emits at. Loggers built by PackageLogger or ScopedLogger report their own master's level; anything else falls back to the process-global level.

func (*Logger) Slog added in v1.3.99

func (logger *Logger) Slog() *slog.Logger

Slog returns a *slog.Logger that forwards to this Logger, for libraries that take a log/slog logger (e.g. github.com/0magnet/wisp). Levels map onto logrus' levels, so the logger's configured level still gates output, and an "err" attribute becomes the logrus error field.

func (*Logger) Tracef

func (logger *Logger) Tracef(format string, args ...interface{})

Tracef logs at trace level (below Debug). The embedded logrus.FieldLogger interface omits the Trace methods, so a *Logger otherwise can't emit at trace without reaching through WithField; this forwards through the entry. Used to keep low-value diagnostics (e.g. the CLI's CXO fetch-chain hit/miss notes) off ordinary stdout, since the CLI's package logger runs at Debug.

func (*Logger) WithAppName

func (logger *Logger) WithAppName(name string) *Logger

WithAppName returns a derived Logger that attaches app_name=<name> to every entry. Empty name is a no-op (returns the receiver). Used by router-side scoping so 'cli proxy start --verbose' can match on the field.

func (*Logger) WithTime

func (logger *Logger) WithTime(t time.Time) *logrus.Entry

WithTime overrides time, used by logger.

type MasterLogger

type MasterLogger struct {
	*logrus.Logger
}

MasterLogger wraps logrus.Logger and is able to create new package-aware loggers

func NewMasterLogger

func NewMasterLogger() *MasterLogger

NewMasterLogger creates a new package-aware logger with formatting string

func (*MasterLogger) DisableColors

func (logger *MasterLogger) DisableColors()

DisableColors disables colored logging

func (*MasterLogger) EnableColors

func (logger *MasterLogger) EnableColors()

EnableColors enables colored logging

func (*MasterLogger) PackageLogger

func (logger *MasterLogger) PackageLogger(moduleName string) *Logger

PackageLogger instantiates a package-aware logger

func (*MasterLogger) ScopedLogger

func (logger *MasterLogger) ScopedLogger(module string, level logrus.Level) *Logger

ScopedLogger returns a package-aware logger whose minimum level is independent of this master logger's, so setting it cannot change what any other logger in the process emits.

Output and formatter are shared with the parent; hooks are copied as of this call. A scoped logger therefore still writes where everything else writes — only the level is its own. Hooks installed on the parent afterwards do not reach it, which is fine for the one caller that needs this: services install their hooks (syslog) at startup, before any service builds its logger.

type PanicEntry

type PanicEntry struct {
	// Time is when the panic was recovered.
	Time time.Time `json:"time"`
	// Site names the recovering location, e.g. "router RPC handler". It is
	// supplied by the caller rather than derived, because by the time a
	// deferred recover runs the panicking frames are already gone.
	Site string `json:"site"`
	// Value is the panic value, formatted with %v.
	Value string `json:"value"`
	// Stack is the traceback captured at recovery time.
	Stack string `json:"stack,omitempty"`
}

PanicEntry is one recovered panic.

func PanicStats

func PanicStats() (count uint64, last []PanicEntry)

PanicStats reports the total number of recovered panics since start and the most recent PanicRingSize of them, oldest first. The count keeps rising after the ring has wrapped, which is the point: it says whether what the ring shows is the whole story.

type Record

type Record struct {
	Time    time.Time
	Level   logrus.Level
	Module  string
	Message string
	Fields  map[string]string
	// IsEvent is true for a layered route/transport event (events bucket),
	// false for the app's own log output (logs bucket).
	IsEvent bool
}

Record is a value-copy snapshot of a logrus entry captured into the ring. The entry pointer itself must never be retained — logrus pools and reuses Entry values after Fire returns — so the fields we care about are copied out at capture time.

func (Record) Format

func (r Record) Format() string

Format renders the Record as a single log line (no trailing newline), matching the visor's text log format: [timestamp] LEVEL [module]: message key=value…

type RingBuffer

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

RingBuffer is a thread-safe, fixed-capacity io.Writer that retains the most recent bytes written (oldest dropped, trimmed to a line boundary). It exists so a service can expose its recent log output over a debug endpoint without writing to disk: attach it with AddHook(NewWriteHook(rb)) and serve rb.Bytes().

The backing store is a single pre-allocated ring of `max` bytes: Write copies into it in place and never allocates or grows. (The previous append/re-slice implementation reallocated the whole buffer roughly every `max` bytes of log output; fired on EVERY log line of every service, that was a top allocation source and a needless GC driver on busy services like dmsg-discovery.)

func NewRingBuffer

func NewRingBuffer(maxBytes int) *RingBuffer

NewRingBuffer returns a RingBuffer holding up to maxBytes (DefaultRingBufferBytes when maxBytes <= 0).

func (*RingBuffer) Bytes

func (rb *RingBuffer) Bytes() []byte

Bytes returns a copy of the current buffer contents, oldest first. Once the ring has wrapped (is full) the result is trimmed to the next line boundary so it never starts mid-line — matching the original append-based behavior.

func (*RingBuffer) Write

func (rb *RingBuffer) Write(p []byte) (int, error)

Write copies p into the ring in place, dropping the oldest bytes when capacity is exceeded. Allocation-free in steady state. Never errors.

type TextFormatter

type TextFormatter struct {
	// Set to true to bypass checking for a TTY before outputting colors.
	ForceColors bool

	// Force disabling colors. For a TTY colors are enabled by default.
	DisableColors bool

	// Force formatted layout, even for non-TTY output.
	ForceFormatting bool

	// Disable timestamp logging. useful when output is redirected to logging
	// system that already adds timestamps.
	DisableTimestamp bool

	// Disable the conversion of the log levels to uppercase
	DisableUppercase bool

	// Enable logging the full timestamp when a TTY is attached instead of just
	// the time passed since beginning of execution.
	FullTimestamp bool

	// Timestamp format to use for display when a full timestamp is printed.
	TimestampFormat string

	// The fields are sorted by default for a consistent output. For applications
	// that log extremely frequently and don't use the JSON formatter this may not
	// be desired.
	DisableSorting bool

	// Wrap empty fields in quotes if true.
	QuoteEmptyFields bool

	// Can be set to the override the default quoting character "
	// with something else. For example: ', or `.
	QuoteCharacter string

	// Pad msg field with spaces on the right for display.
	// The value for this parameter will be the size of padding.
	// Its default value is zero, which means no padding will be applied for msg.
	SpacePadding int

	// Always use quotes for string values (except for empty fields)
	AlwaysQuoteStrings bool

	sync.Once
	// contains filtered or unexported fields
}

TextFormatter formats log output

func (*TextFormatter) Format

func (f *TextFormatter) Format(entry *logrus.Entry) ([]byte, error)

Format formats a logrus.Entry

func (*TextFormatter) SetColorScheme

func (f *TextFormatter) SetColorScheme(colorScheme *ColorScheme)

SetColorScheme sets the TextFormatter's color scheme configuration

type WriteHook

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

WriteHook is a logrus.Hook that logs to an io.Writer

func NewWriteHook

func NewWriteHook(w io.Writer) *WriteHook

NewWriteHook returns a new WriteHook accepting DefaultHookLevel and above.

func NewWriteHookLevel

func NewWriteHookLevel(w io.Writer, level logrus.Level) *WriteHook

NewWriteHookLevel returns a new WriteHook that fires only for entries at least as severe as level. Use it when the call site knows what it wants to capture; NewWriteHook is the default.

func (*WriteHook) Fire

func (f *WriteHook) Fire(e *logrus.Entry) error

Fire writes a logrus.Entry to the file

func (*WriteHook) Levels

func (f *WriteHook) Levels() []logrus.Level

Levels returns the levels accepted by the WriteHook. Entries below the configured level never reach Fire, and so never pay the second TextFormatter.Format pass.

Jump to

Keyboard shortcuts

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