nupp.log
nupp.log is leveled logging whose disabled path is the one it is designed around. A severity call in statement position is lowered rather than called, so a filtered line evaluates none of its arguments.
nupp.log.error("cannot open %s: %s", path, reason)
nupp.log.warn("retrying in %dms", delay)
nupp.log.info("loaded %d entities", count)
nupp.log.debug("state=%s", state)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 numberBecause %s accepts anything, nothing needs tostring and a nil argument prints as nil rather than raising. Lines go to standard error until a host says otherwise.
%? is Nupp's debug directive. It requires nupp.Debug, calls the value's debug() method, and formats the returned string as %s:
At lowered call sites that method call is inside the level guard, so filtered logs do not render the value. Named loggers perform the same check in their already-lazy writer.
Severity calls are intrinsics#
A severity call in statement position whose format is a literal is lowered rather than called:
const __nuppModule = require("nupp.log").forModule("amb"); -- once, in the prologue
if __nuppModule.on[1] then __nuppModule.emit(1,47,string.format("id %d",id)) endThree properties follow from that shape, and together they are the reason this is a compiler intrinsic rather than a library.
Filtered calls evaluate 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.
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.
Filtered call cost#
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.
Forms that stay calls#
Every other form keeps an ordinary call meaning exactly the same thing, only slower. The module shows as ?, and the arguments are evaluated:
| Form | Lowered | Reason |
|---|---|---|
nupp.log.info("id %d", id) |
yes | |
nupp.log.info(format, id) |
no | the 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 | not the compiler-provided nupp |
logger:info("id %d", id) |
no | a named logger, not the path |
Levels#
level reads the threshold and setLevel moves it:
The five levels are "off", "error", "warn", "info" and "debug", each admitting itself and everything above it, so "warn" emits warnings and errors. The default is "warn".
The parameter is that literal union, so a string from outside the program has to be narrowed to one of the five before it can be passed. os.getenv("LOG_LEVEL") or "warn" is string, and string is not one of them:
local wanted = os.getenv("LOG_LEVEL")
if wanted == "debug" or wanted == "info" or wanted == "warn" then
nupp.log.setLevel(wanted)
endA 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) returns whether a level would emit. Use it to guard preparation spanning more than one call, which no single lowered site can elide:
if nupp.log.enabled("debug") then
local report = summarize(world)
nupp.log.debug("world: %s", report)
endModule levels#
setModuleLevel overrides the threshold for one exact module name. Modules without an override inherit the global threshold, including after it changes:
The override reaches existing and future module views. moduleLevel reads the effective threshold, and passing "inherit" removes the override:
nupp.log.moduleLevel("game.physics") -- "debug"
nupp.log.setModuleLevel("game.physics", "inherit") -- restore the global thresholdNames are exact: configuring game.physics does not also configure game.physics.collision. A named logger with the same name uses the same threshold.
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:
nupp.log.setSink(function(level: integer, module: string, line: integer, message: string): nil
sdl.logMessage(CATEGORY, PRIORITY[level], ("%s:%d %s"):format(module, line, message))
end)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:
io.open returns LuaFile?, and setSink takes a target rather than a maybe, so the assert is what turns one into the other. The target is taken: the log owns it from then on, and sink() reads which one is in force.
A file-like target renders through the formatter, which is replaceable on its own:
nupp.log.setFormatter(function(level: integer, module: string, line: integer, message: string, stamp: string): string
return ("%s[%s] %s"):format(stamp, nupp.log.levelName(level), message)
end)Both setters return the value they replaced, so a host can restore what it found, and setFormatter(nil) returns to the built-in rendering.
Timestamps#
nupp.log.timestamp() returns 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.
nupp.log.setTimestampFormat("%H:%M:%S ") -- returns the previous format
nupp.log.setTimestampFormat("") -- off
nupp.log.timestampFormat() -- ""Reading the second through the FFI
os.time is NYI in LuaJIT and stitches the trace it stands on, so the second is read through the FFI instead. os.time is still called once at startup to validate the symbol, because some Windows CRTs inline time to _time64 or give it a 32-bit time_t, and a symbol that resolves but is the wrong width returns nonsense rather than failing to resolve.
Named loggers#
named returns a logger carrying a fixed name, with a method per severity:
Use one for a subsystem that does not correspond to a module, and for a call site the intrinsic cannot reach. Repeating a name returns 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.
Cost#
The installer lands only in modules that reach nupp.log, like every other compiler-provided facility. A module that never logs carries nothing.
Module contents
Types
| Type | Kind | Description |
|---|---|---|
Formatter | type | Renders a line for a file-like destination. |
Level | type | The threshold, from silent to most verbose. |
LevelOverride | type | A module threshold or the instruction to resume inheriting the global one. |
Logger | record | A logger carrying a fixed name, for subsystems and for call sites the intrinsic cannot rewrite. |
Severity | type | A level as a sink sees it: 1 error, 2 warn, 3 info, 4 debug. |
Sink | type | Receives an emitted line, already formatted. |
Target | type | Where lines go: a sink function, or anything file-like to write to. |
Functions
| Function | Kind | Description |
|---|---|---|
debug | function | Logs at debug. |
enabled | function | Whether a level is currently admitted. |
error | function | Logs at error. |
formatter | function | The line formatter in force. |
info | function | Logs at info. |
level | function | The global threshold. |
levelName | function | The name for a sink's numeric level. |
moduleLevel | function | The effective threshold for a specific module or named logger: its override, or the global threshold when it has none. |
named | function | A logger with a fixed name. |
setFormatter | function | Replaces the line formatter. |
setLevel | function | Moves the global threshold. |
setModuleLevel | function | Overrides the threshold for a specific module or named logger. |
setSink | function | Replaces the destination. |
setTimestampFormat | function | Replaces the timestamp format. |
sink | function | The destination in force. |
timestamp | function | The stamp a line written now would carry. |
timestampFormat | function | The timestamp format in force. |
warn | function | Logs at warn. |
Types#
Formattertype#
type log.Formatter = function(
level: log.Severity,
module: string,
line: integer,
message: string,
stamp: string
): stringRenders a line for a file-like destination. Only consulted when the target is file-like; a sink function formats however it likes.
Leveltype#
type log.Level = "off" | "error" | "warn" | "info" | "debug"The threshold, from silent to most verbose. Each level admits itself and everything above it, so "warn" emits warnings and errors.
LevelOverridetype#
type log.LevelOverride = log.Level | "inherit"A module threshold or the instruction to resume inheriting the global one.
Loggerrecord#
record log.Logger ...A logger carrying a fixed name, for subsystems and for call sites the intrinsic cannot rewrite. Changing the level or target restamps every logger, so a filtered call reaches an empty function rather than a test.
Members
| Name | Kind | Description |
|---|---|---|
name | field | The name every line from this logger carries. |
debug | method | Logs at debug. |
info | method | Logs at info. |
warn | method | Logs at warn. |
error | method | Logs at error. |
enabled | method | Whether this logger would emit at level. |
debugmethod#
debug: function<F is string>(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nilLogs at debug. Accepts string.format directives.
Arguments
| Name | Type | Description |
|---|---|---|
self | log.Logger | |
fmt | F | |
... | unpackof __NuppFormatArguments(F, nupp.Debug) |
Returns
| Type | Description |
|---|---|
nil |
infomethod#
info: function<F is string>(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nilLogs at info. Accepts string.format directives.
Arguments
| Name | Type | Description |
|---|---|---|
self | log.Logger | |
fmt | F | |
... | unpackof __NuppFormatArguments(F, nupp.Debug) |
Returns
| Type | Description |
|---|---|
nil |
warnmethod#
warn: function<F is string>(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nilLogs at warn. Accepts string.format directives.
Arguments
| Name | Type | Description |
|---|---|---|
self | log.Logger | |
fmt | F | |
... | unpackof __NuppFormatArguments(F, nupp.Debug) |
Returns
| Type | Description |
|---|---|
nil |
Severitytype#
type log.Severity = integerA level as a sink sees it: 1 error, 2 warn, 3 info, 4 debug.
Sinktype#
Receives an emitted line, already formatted.
Replacing this replaces the back end, so a host logging through its own facility pays for nothing it discards. No timestamp is passed, because a sink that wants one asks.
Targettype#
Where lines go: a sink function, or anything file-like to write to.
Functions#
log.debugfunction#
Logs at debug. Accepts string.format directives.
Type parameters
| Name | Description |
|---|---|
F |
Arguments
| Name | Type | Description |
|---|---|---|
fmt | F | |
... | unpackof __NuppFormatArguments(F, nupp.Debug) |
Returns
| Type | Description |
|---|---|
nil |
log.enabledfunction#
Whether a level is currently admitted.
Arguments
| Name | Type | Description |
|---|---|---|
level | log.Level | the level to test |
Returns
| Type | Description |
|---|---|
boolean | whether a call at that level would emit |
log.errorfunction#
Logs at error. Accepts string.format directives.
Type parameters
| Name | Description |
|---|---|
F |
Arguments
| Name | Type | Description |
|---|---|---|
fmt | F | |
... | unpackof __NuppFormatArguments(F, nupp.Debug) |
Returns
| Type | Description |
|---|---|
nil |
log.formatterfunction#
The line formatter in force.
Returns
| Type | Description |
|---|---|
log.Formatter? | the formatter, or nil when lines use the built-in rendering |
log.infofunction#
Logs at info. Accepts string.format directives.
Type parameters
| Name | Description |
|---|---|
F |
Arguments
| Name | Type | Description |
|---|---|---|
fmt | F | |
... | unpackof __NuppFormatArguments(F, nupp.Debug) |
Returns
| Type | Description |
|---|---|
nil |
log.levelfunction#
The global threshold.
Returns
| Type | Description |
|---|---|
log.Level | the level in force for every module without an override |
log.levelNamefunction#
The name for a sink's numeric level.
Arguments
| Name | Type | Description |
|---|---|---|
severity | log.Severity | the numeric level a sink received |
Returns
| Type | Description |
|---|---|
log.Level | the level's name |
log.moduleLevelfunction#
function log.moduleLevel(name: string): log.LevelThe effective threshold for a specific module or named logger: its override, or the global threshold when it has none.
Arguments
| Name | Type | Description |
|---|---|---|
name | string | the exact module or logger name |
Returns
| Type | Description |
|---|---|
log.Level | the level in force for that name |
Raises
| Type | Condition |
|---|---|
string | when name is not a string |
log.namedfunction#
A logger with a fixed name. Repeating a name returns the same logger.
Reach for one where the intrinsic cannot rewrite the call: a format built at run time, a call in expression position, a subsystem that wants its own name on every line. The methods are restamped whenever the level or the target moves, so a filtered call reaches an empty function rather than a test.
Arguments
| Name | Type | Description |
|---|---|---|
name | string | the name its lines carry |
Returns
| Type | Description |
|---|---|
log.Logger | the logger |
Raises
| Type | Condition |
|---|---|
string | when name is not a string |
log.setFormatterfunction#
function log.setFormatter(next: log.Formatter?): log.Formatter?Replaces the line formatter.
Arguments
| Name | Type | Description |
|---|---|---|
next | log.Formatter? | the formatter, or nil to return to the built-in rendering |
Returns
| Type | Description |
|---|---|
log.Formatter? | the formatter this call replaced |
Raises
| Type | Condition |
|---|---|
string | when next is neither a function nor nil |
log.setLevelfunction#
Moves the global threshold. Each level admits itself and everything above it.
Arguments
| Name | Type | Description |
|---|---|---|
level | log.Level | the new threshold |
Returns
| Type | Description |
|---|---|
log.Level | the threshold this call replaced |
Raises
| Type | Condition |
|---|---|
string | when level is unknown |
log.setModuleLevelfunction#
function log.setModuleLevel(name: string, level: log.LevelOverride): log.LevelOverrides the threshold for a specific module or named logger.
A name without an override inherits the global threshold. Passing "inherit" removes an override. Existing and future generated module views observe the change while retaining the call-site guard that avoids evaluating filtered arguments.
Arguments
| Name | Type | Description |
|---|---|---|
name | string | the exact module or logger name |
level | log.LevelOverride | the new threshold, or |
Returns
| Type | Description |
|---|---|
log.Level | the effective threshold this call replaced |
Raises
| Type | Condition |
|---|---|
string | when name is not a string or level is unknown |
log.setSinkfunction#
Replaces the destination. The log owns the target from then on.
Arguments
| Name | Type | Description |
|---|---|---|
takes next | log.Target | a sink function, or a file to write rendered lines to |
Returns
| Type | Description |
|---|---|
log.Target | the destination this call replaced |
Raises
| Type | Condition |
|---|---|
string | when next is neither a sink function nor a file |
log.setTimestampFormatfunction#
function log.setTimestampFormat(next: string): stringReplaces the timestamp format. An empty format stops stamping.
Arguments
| Name | Type | Description |
|---|---|---|
next | string | the |
Returns
| Type | Description |
|---|---|
string | the format this call replaced |
Raises
| Type | Condition |
|---|---|
string | when next is not a string |
log.sinkfunction#
The destination in force.
Returns
| Type | Description |
|---|---|
log.Target | the sink function or file lines go to |
log.timestampfunction#
function log.timestamp(): stringThe stamp a line written now would carry.
Returns
| Type | Description |
|---|---|
string | the rendered timestamp, or "" when stamping is off |
log.timestampFormatfunction#
function log.timestampFormat(): stringThe timestamp format in force.
Returns
| Type | Description |
|---|---|
string | the |
log.warnfunction#
Logs at warn. Accepts string.format directives.
Type parameters
| Name | Description |
|---|---|
F |
Arguments
| Name | Type | Description |
|---|---|---|
fmt | F | |
... | unpackof __NuppFormatArguments(F, nupp.Debug) |
Returns
| Type | Description |
|---|---|
nil |