Module: nupp.log

nupp.log#

Leveled logging whose disabled path is the path it is designed around.

Each severity accepts a string.format directive string and the arguments it calls for. The directives and their argument types are checked where the call is written, by the same machinery that checks string.format, so a missing argument or a %d handed a string is a compile error rather than a line that fails at run time.

nupp.log.error("id %d")
  error: NUPP2006: omitted argument 2 supplies nil, not number

Because %s accepts anything, nothing needs tostring and a nil argument prints as nil rather than raising.

The call is an intrinsic#

A severity call in statement position whose format is a literal is lowered rather than called:

const __nuppModule = _G.nupp.log.forModule("amb");   -- once, in the prologue
if __nuppModule.on[1] then __nuppModule.emit(1,47,string.format("id %d",id)) end

Three things follow, and they are the reason this is a compiler intrinsic rather than a library.

A filtered call evaluates nothing. The level test stands at the call site, so the arguments of a suppressed line are never computed. nupp.log.debug("%s", render(state)) does not call render when debug is off. No library can decline to evaluate its own arguments.

The module name and line are constants. The compiler is generating the file, so it writes both in directly — the module once in the prologue, the line at each site. Nothing is recovered at run time, nothing depends on the debug library, and there is no per-module boilerplate to write or to keep in step with a rename.

A filtered call costs an upvalue read, an array index and a branch. The view is bound once per module, and on is an array indexed by severity, shared with every other module so a level change is seen everywhere at once.

When it is not lowered#

Every other spelling keeps an ordinary call meaning exactly the same thing, only slower — the module shows as ?, and the arguments are evaluated:

 Spelling                             Lowered   Why not
 ───────────────────────────────────  ────────  ─────────────────────────────
 nupp.log.info("id %d", id)           yes
 nupp.log.info(format, id)            no        format is not a literal
 local f = nupp.log.info              no        a value, not a call
 x = nupp.log.info("hi")              no        not statement position
 local nupp = ...                     no        nupp is not the ambient one
 logger:info("id %d", id)             no        a named logger, not the path

Levels#

"off" | "error" | "warn" | "info" | "debug", each admitting itself and everything above it, so "warn" emits warnings and errors. The default is "warn". A level that is not one of the five is a compile error where it is a literal and an ordinary raise where it is not.

nupp.log.enabled(level) answers whether a level would emit. Use it to guard preparation spanning more than one call, which no single lowered site can elide:

Swapping the back end#

A host that logs through its own facility installs a sink function and takes over completely — it receives the parts, not a rendered line, and pays for no formatting it would discard:

level is 1 error, 2 warn, 3 info, 4 debug; nupp.log.levelName turns one back into its name. line is 0 for a line the compiler could not attribute, which is every line from a named logger.

Passing anything file-like instead keeps the built-in rendering and only moves where it goes:

A file-like target renders through the formatter, which is replaceable on its own:

Both setters answer the value they replaced, so a host can restore what it found.

Timestamps#

nupp.log.timestamp() answers the current time formatted, recomputed at most once per wall-clock second and shared by every logger. It is a pull rather than something pushed to sinks, so a host that stamps its own lines never pays for one.

The second itself is read through the FFI rather than os.time, which is NYI in LuaJIT and stitches the trace it stands on. os.time is still used once at startup to validate the symbol, because some Windows CRTs inline time to _time64 or give it a 32-bit time_t.

Named loggers#

For subsystems that do not correspond to a module, and for call sites the intrinsic cannot reach. Repeating a name answers the same logger. Their methods are replaced when the level or target changes, so a filtered call reaches an empty function rather than a test — but the arguments are still evaluated, which is the cost of a name chosen at run time.

What is emitted where#

The installer lands only in modules that reach nupp.log, like every other ambient facility. A module that never logs carries nothing.

Leveled logging over a swappable destination.

Module contents

Functions

FunctionKindDescription
debugfunctionLogs at debug.
enabledfunctionWhether a level would emit.
errorfunctionLogs at error.
infofunctionLogs at info.
levelNamefunctionThe name for a sink's numeric level.
namedfunctionA logger with a fixed name.
timestampfunctionThe current time formatted, recomputed at most once a second and shared by every logger.
warnfunctionLogs at warn.

Values

ValueKindDescription
formattervariableReads the line format, or sets it and answers the one it replaced.
levelvariableReads the threshold, or sets it and answers the one it replaced.
sinkvariableReads the destination, or sets it and answers the one it replaced.
timestampFormatvariableReads the timestamp format, or sets it and answers the one it replaced.

Functions#

debugfunction#

Logs at debug. Accepts string.format directives.

local debug: function<F is string>(fmt: F, ...: unpackof __NuppFormatArguments<F>): nil

Type parameters

NameDescription
F

Arguments

NameTypeDescription
fmtF
...unpackof __NuppFormatArguments<F>

Returns

TypeDescription
nil

enabledfunction#

Whether a level would emit. Guards preparation spanning more than one call, which no single rewritten site can elide.

local enabled: function(level: Level): boolean

Arguments

NameTypeDescription
levelLevel

the level to ask about

Returns

TypeDescription
boolean

whether a call at that level would reach the sink

errorfunction#

Logs at error. Accepts string.format directives.

local error: function<F is string>(fmt: F, ...: unpackof __NuppFormatArguments<F>): nil

Type parameters

NameDescription
F

Arguments

NameTypeDescription
fmtF
...unpackof __NuppFormatArguments<F>

Returns

TypeDescription
nil

infofunction#

Logs at info. Accepts string.format directives.

local info: function<F is string>(fmt: F, ...: unpackof __NuppFormatArguments<F>): nil

Type parameters

NameDescription
F

Arguments

NameTypeDescription
fmtF
...unpackof __NuppFormatArguments<F>

Returns

TypeDescription
nil

levelNamefunction#

The name for a sink's numeric level.

local levelName: function(level: Severity): Level

Arguments

NameTypeDescription
levelSeverity

the numeric level a sink received

Returns

TypeDescription
Level

the level's name

namedfunction#

A logger with a fixed name. Repeating a name answers the same logger.

local named: function(name: string): Logger

Arguments

NameTypeDescription
namestring

the name its lines carry

Returns

TypeDescription
Logger

the logger

timestampfunction#

The current time formatted, recomputed at most once a second and shared by every logger. Empty while timestamps are off.

local timestamp: function(): string

Returns

TypeDescription
string

the cached timestamp

warnfunction#

Logs at warn. Accepts string.format directives.

local warn: function<F is string>(fmt: F, ...: unpackof __NuppFormatArguments<F>): nil

Type parameters

NameDescription
F

Arguments

NameTypeDescription
fmtF
...unpackof __NuppFormatArguments<F>

Returns

TypeDescription
nil

Values#

formattervariable#

Reads the line format, or sets it and answers the one it replaced. Only a file-like target consults it.

local formatter: function(): Formatter & function(format: Formatter): Formatter

levelvariable#

Reads the threshold, or sets it and answers the one it replaced.

local level: function(): Level & function(level: Level): Level

sinkvariable#

Reads the destination, or sets it and answers the one it replaced.

local sink: function(): Target & function(target: Target): Target

timestampFormatvariable#

Reads the timestamp format, or sets it and answers the one it replaced. Setting it drops the cached value; an empty format turns timestamps off.

local timestampFormat: function(): string & function(format: string): string