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 = _G.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 moves it when given one:
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.level(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) answers 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)
endSwapping 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.sink(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 answers LuaFile?, and sink takes a target rather than a maybe, so the assert is what turns one into the other.
A file-like target renders through the formatter, which is replaceable on its own:
nupp.log.formatter(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 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.
nupp.log.timestampFormat("%H:%M:%S ") -- set, answers the previous format
nupp.log.timestampFormat("") -- offReading 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 answers nonsense rather than failing to resolve.
Named loggers#
named answers 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 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.
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 one line for a file-like destination. |
Level | type | The threshold, from silent to most verbose. |
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 one 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 | Reads or replaces the line formatter. |
forModule | function | The view a generated chunk binds once, so a site carries only severity and line. |
info | function | Logs at info. |
level | function | Reads or moves the threshold. |
levelName | function | The name for a sink's numeric level. |
named | function | A logger with a fixed name. |
sink | function | Reads or replaces the destination. |
timestamp | function | The stamp a line written now would carry. |
timestampFormat | function | Reads or replaces the timestamp format. |
warn | function | Logs at warn. |
Types#
Formattertype#
type log.Formatter = function(
level: log.Severity,
module: string,
line: integer,
message: string,
stamp: string
): stringRenders one 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.
Loggerrecord#
record log.Logger
readonly name: string
debug: function<F is string>(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
info: function<F is string>(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
warn: function<F is string>(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
error: function<F is string>(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
enabled: function(self: log.Logger, level: log.Level): boolean
endA 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.
Methods
debug#
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 |
info#
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 |
warn#
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 |
error#
error: function<F is string>(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nilLogs at error. Accepts string.format directives.
Arguments
| Name | Type | Description |
|---|---|---|
self | log.Logger | |
fmt | F | |
... | unpackof __NuppFormatArguments(F, nupp.Debug) |
Returns
| Type | Description |
|---|---|
nil |
enabled#
Whether this logger would emit at level.
Arguments
| Name | Type | Description |
|---|---|---|
self | log.Logger | |
level | log.Level |
Returns
| Type | Description |
|---|---|
boolean |
Fields
Severitytype#
type log.Severity = integerA level as a sink sees it: 1 error, 2 warn, 3 info, 4 debug.
Sinktype#
Receives one 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#
Reads or replaces the line formatter.
Arguments
| Name | Type | Description |
|---|---|---|
next | log.Formatter? | the formatter, or nil to read the current one |
Returns
| Type | Description |
|---|---|
log.Formatter? | the formatter this call replaced |
Raises
when next is not a function
log.forModulefunction#
The view a generated chunk binds once, so a site carries only severity and line.
Generated code calls this in a module's prologue. Nothing written by hand needs it: a severity call in statement position is already lowered to a test against the view's on array around a direct emit.
Arguments
| Name | Type | Description |
|---|---|---|
module | string | the module name its lines carry |
Returns
| Type | Description |
|---|---|
any | the view, whose |
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#
Reads or moves the threshold. Each level admits itself and everything above it.
Arguments
| Name | Type | Description |
|---|---|---|
level | log.Level? | the new threshold, or nil to read the current one |
Returns
| Type | Description |
|---|---|
log.Level | the threshold this call replaced |
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.namedfunction#
A logger with a fixed name. Repeating a name answers 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
when name is not a string
log.sinkfunction#
Reads or replaces the destination.
Arguments
| Name | Type | Description |
|---|---|---|
next | log.Target? | a sink function, a file, or nil to read the current destination |
Returns
| Type | Description |
|---|---|
log.Target | the destination this call replaced |
Raises
when next is neither a sink function nor a file
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(next: string?): stringReads or replaces 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
when next is not a string
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 |