Documentation
¶
Overview ¶
Package logger is a pure-Go (CGO-free) port of the deterministic core of MRI 4.0.5's stdlib Logger (the logger-1.7.0 gem): the severity model, the default Formatter, level gating, and the LogDevice rotation *policy*.
It is the compute core that go-embedded-ruby's rbgo binds: rbgo wires the IO sink (the $stdout / File that receives the formatted bytes), the wall clock, and the process id; everything that is a pure decision — which bytes a log line is, whether a message is gated out by the level, and whether (and to what filename) a log file should rotate — lives here, with no IO and no Ruby runtime, so it is testable byte-for-byte against the `ruby` binary.
Index ¶
- Constants
- func NextRotateTime(now time.Time, period Period) (time.Time, error)
- func PeriodAgeFile(filename string, periodEnd time.Time, periodSuffix string, ...) string
- func PreviousPeriodEnd(now time.Time, period Period) (time.Time, error)
- func SeverityLabel(s Severity) string
- func ShouldRotateByPeriod(now, nextRotate time.Time) bool
- func ShouldRotateBySize(currentSize, shiftSize int64, shiftAge int) bool
- type Clock
- type Exception
- type Formatter
- type Inspector
- type Logger
- func (l *Logger) Add(severity Severity, message any, progname string) bool
- func (l *Logger) Debug(message any, progname string) bool
- func (l *Logger) DebugQ() bool
- func (l *Logger) Error(message any, progname string) bool
- func (l *Logger) ErrorQ() bool
- func (l *Logger) Fatal(message any, progname string) bool
- func (l *Logger) FatalQ() bool
- func (l *Logger) Info(message any, progname string) bool
- func (l *Logger) InfoQ() bool
- func (l *Logger) Log(severity Severity, message any, progname string) bool
- func (l *Logger) SetLevel(v any) error
- func (l *Logger) Unknown(message any, progname string) bool
- func (l *Logger) Warn(message any, progname string) bool
- func (l *Logger) WarnQ() bool
- func (l *Logger) Write(msg string) int
- type Period
- type Severity
- type ShiftMove
Constants ¶
const DefaultDatetimeFormat = "%Y-%m-%dT%H:%M:%S.%6N"
DefaultDatetimeFormat is MRI's Logger::Formatter::DatetimeFormat, the strftime pattern used when datetime_format is unset.
const DefaultPeriodSuffix = "%Y%m%d"
DefaultPeriodSuffix is MRI's default @shift_period_suffix ("%Y%m%d").
const DefaultShiftAge = 7
DefaultShiftAge is MRI's default integer @shift_age (keep 7 old files).
const DefaultShiftSize = 1048576
DefaultShiftSize is MRI's default @shift_size (1 MiB).
Variables ¶
This section is empty.
Functions ¶
func NextRotateTime ¶
NextRotateTime is a pure port of Logger::Period.next_rotate_time: it returns the next instant at which a calendar rotation of cadence period becomes due, relative to now. The arithmetic is performed in now's own location, so a host that wires local time gets MRI's Time.mktime behaviour. "now"/"everytime" return now unchanged. An unrecognised period is an error.
func PeriodAgeFile ¶
func PeriodAgeFile(filename string, periodEnd time.Time, periodSuffix string, exists func(string) bool) string
PeriodAgeFile returns the name a period rotation would rename the current log to, mirroring Logger::LogDevice#shift_log_period. The base name is "<filename>.<suffix>", where suffix is periodEnd formatted with the period suffix; if that name is in the host-supplied taken set, MRI appends ".1", ".2", … (up to 99) until a free name is found. exists reports whether a candidate name is already taken (the host's FileTest.exist?).
func PreviousPeriodEnd ¶
PreviousPeriodEnd is a pure port of Logger::Period.previous_period_end: it returns 23:59:59 on the last day of the period preceding now, used to name the rotated file. "now"/"everytime" return now unchanged; an unknown period errors.
func SeverityLabel ¶
SeverityLabel returns the textual label MRI uses for a severity, matching Logger#format_severity: SEV_LABEL[severity] || 'ANY'. Any value outside 0..5 yields "ANY".
func ShouldRotateByPeriod ¶
ShouldRotateByPeriod reports whether a calendar rotation is due, mirroring the period branch of check_shift_log: now >= @next_rotate_time.
func ShouldRotateBySize ¶
ShouldRotateBySize reports whether a size-based (integer shift_age) rotation is due. It mirrors Logger::LogDevice#check_shift_log's integer branch:
@filename && (@shift_age > 0) && (@dev.stat.size > @shift_size)
The filesize is injected (the host stat'd the device); this is the pure decision only.
Types ¶
type Clock ¶
Clock supplies the current instant. The default is time.Now; tests inject a fixed instant so the formatted lines are deterministic.
type Exception ¶
type Exception struct {
Message string // exception.message
Class string // exception.class.to_s
Backtrace []string // exception.backtrace (nil if the exception has none)
}
Exception is the message shape Logger::Formatter#msg2str special-cases. A host passes one of these when the logged object is a Ruby exception; the formatter renders "<message> (<class>)\n<backtrace>" exactly as MRI does.
type Formatter ¶
type Formatter struct {
// DatetimeFormat overrides the strftime pattern for the timestamp; empty
// means use DefaultDatetimeFormat (MRI's @datetime_format || DatetimeFormat).
DatetimeFormat string
// Inspect renders an arbitrary message value as msg.inspect would; see
// Inspector. May be nil.
Inspect Inspector
}
Formatter is a pure port of MRI's Logger::Formatter. It holds only the optional datetime_format override and the message Inspector; the clock, pid, severity-label, progname and message are all supplied per call, so Format is a deterministic function of its inputs.
func (*Formatter) Format ¶
func (f *Formatter) Format(severityLabel string, t time.Time, pid int, progname string, msg any) string
Format renders one log line byte-for-byte as MRI's Logger::Formatter#call would, given the severity label, the (injected) timestamp, the (injected) pid, the progname and the message:
sprintf("%.1s, [%s #%d] %5s -- %s: %s\n",
severity, format_datetime(time), pid, severity, progname, msg2str(msg))
type Inspector ¶
Inspector turns a non-String, non-Exception message into the text MRI's msg.inspect would produce. The host wires this from its own object model (Array#inspect, Hash#inspect, Integer#to_s, ...); the library never reaches into a Ruby object itself. A nil Inspector falls back to Go's %#v / fmt.Sprint only for the Go primitive forms a ruby-free test feeds it.
type Logger ¶
type Logger struct {
// Level is the threshold below which Add drops a message (MRI's @level).
Level Severity
// Progname is the default program name (MRI's @progname).
Progname string
// Formatter renders each line; if nil, a zero-value default Formatter is used
// (MRI's @formatter || @default_formatter).
Formatter *Formatter
// Sink receives each formatted line and each raw << write — this is the host
// IO device. A nil Sink models MRI's @logdev.nil? (Add returns true and
// writes nothing).
Sink func(string)
// Now supplies the timestamp for each line; defaults to time.Now.
Now Clock
// Pid supplies the process id stamped into each line; defaults to os.Getpid
// via PidFunc. Modeled as a function so tests inject a fixed pid.
Pid func() int
}
Logger is a pure port of MRI's Logger class minus the IO device. It decides what bytes each call would emit and whether a call is gated by the level, then hands the formatted line to the host Sink (rbgo's IO device). It never writes to a file or stream itself.
func New ¶
New returns a Logger writing formatted lines to sink, with the default Formatter, DEBUG level, and the real clock/pid wired. Pass a nil sink to model a logger with no device.
func (*Logger) Add ¶
Add mirrors Logger#add. severity defaults to UNKNOWN when negative (MRI's `severity ||= UNKNOWN`, modeled here with the sentinel -1); when there is no sink (MRI's @logdev.nil?) or the severity is below the level, it formats nothing and returns true. Otherwise it formats the line — picking up the default progname when progname is empty — and writes it to the sink, then returns true. Add always returns true, exactly as MRI does.
func (*Logger) Debug ¶
Debug logs message at DEBUG (Logger#debug). The remaining severity helpers mirror their MRI counterparts.
func (*Logger) DebugQ ¶
DebugQ mirrors Logger#debug?: level <= DEBUG. The predicates report whether a message at that severity would currently be emitted.
func (*Logger) SetLevel ¶
SetLevel mirrors Logger#level=, coercing a string or integer like MRI's Severity.coerce before assigning.
type Period ¶
type Period string
Period is a calendar rotation cadence, MRI's :shift_age string/symbol form ("daily"/"weekly"/"monthly", plus "now"/"everytime").
const ( Daily Period = "daily" Weekly Period = "weekly" Monthly Period = "monthly" Now Period = "now" Everytime Period = "everytime" )
The rotation periods MRI's Logger::Period accepts.
func ParsePeriod ¶
ParsePeriod coerces a host-supplied shift_age string/symbol into a Period, accepting the same spellings MRI's case statements do.
type Severity ¶
type Severity int
Severity is a log level, matching MRI's Logger::Severity numeric model.
DEBUG=0 INFO=1 WARN=2 ERROR=3 FATAL=4 UNKNOWN=5
A Severity is just an int; the named constants below mirror the Ruby ones so a host can pass MRI's integers straight through.
const ( DEBUG Severity = 0 // Low-level information, mostly for developers. INFO Severity = 1 // Generic (useful) information about system operation. WARN Severity = 2 // A warning. ERROR Severity = 3 // A handleable error condition. FATAL Severity = 4 // An unhandleable error that results in a program crash. UNKNOWN Severity = 5 // An unknown message that should always be logged. )
The MRI Logger::Severity constants (logger/severity.rb).
func CoerceSeverity ¶
CoerceSeverity mirrors Logger::Severity.coerce: an Integer passes through unchanged; a string (case-insensitively) maps via LEVELS, and anything else is an error ("invalid log level: ..."). It accepts an int or a string, the two forms a host level= would hand through.
type ShiftMove ¶
ShiftMove is one rename a rotation performs: From is renamed to To. From may not exist (the host skips those, as MRI's `if FileTest.exist?` does).
func ShiftAgeSequence ¶
ShiftAgeSequence returns the rename moves a size-based rotation performs, in order, mirroring Logger::LogDevice#shift_log_age:
(@shift_age-3).downto(0) { |i| rename "f.i" -> "f.i+1" if exist }
rename "f" -> "f.0"
Each move is {From, To}; the host applies them (renaming only those whose From exists), then opens a fresh @filename. For a shift_age of n, the moves cover suffixes n-3 .. 0 plus the base file, keeping at most n-1 numbered backups. A shift_age of 3 or less yields just the base -> "filename.0" move.
