# `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
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.
```text
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. 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`:
```nupp
nupp.log.debug("state=%?", state)
```
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:
```nupp
nupp.log.error("id %d", id)
```
```lua
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 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:
```nupp
nupp.log.level("debug") -- set, answers the previous one
nupp.log.level() -- read
```
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:
```nupp
local wanted = os.getenv("LOG_LEVEL")
if wanted == "debug" or wanted == "info" or wanted == "warn" then
nupp.log.level(wanted)
end
```
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:
```nupp
if nupp.log.enabled("debug") then
local report = summarize(world)
nupp.log.debug("world: %s", report)
end
```
## 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
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:
```nupp
local file = assert(io.open("game.log", "a"))
nupp.log.sink(file)
```
`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
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
nupp.log.timestampFormat("%H:%M:%S ") -- set, answers the previous format
nupp.log.timestampFormat("") -- off
```
::: deepdive 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
answers nonsense rather than failing to resolve.
:::
## Named loggers
`named` answers a logger carrying a fixed name, with a method per severity:
```nupp
local physics = nupp.log.named("physics")
physics:warn("step %d took %.2fms", step, elapsed)
```
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.
::: seealso
- [Diagnostics](../../../reference/diagnostics/index.html#diagnostic-index) for the
codes a mistyped format string reports
- [Standard library](../../../learn/runtime/data/standard-library/index.html) for how a facility
reaches a program without being linked into one that never uses it
:::
## Types
### `Formatter` _type_
```nupp
type log.Formatter = function(
level: log.Severity,
module: string,
line: integer,
message: string,
stamp: string
): string
```
Renders one line for a file-like destination. Only consulted when the target is
file-like; a sink function formats however it likes.
### `Level` _type_
```nupp
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.
### `Logger` _record_
```nupp
record log.Logger
readonly name: string
debug: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
info: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
warn: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
error: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
enabled: function(self: log.Logger, level: log.Level): boolean
end
```
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.
#### Methods
##### `debug`
```nupp
debug: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
```
Logs 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`
```nupp
info: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
```
Logs 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`
```nupp
warn: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
```
Logs 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`
```nupp
error: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
```
Logs 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`
```nupp
enabled: function(self: log.Logger, level: log.Level): boolean
```
Whether this logger would emit at `level`.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `log.Logger` | |
| `level` | `log.Level` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
#### Fields
##### `name`
```nupp
name: string
```
The name every line from this logger carries.
### `Severity` _type_
```nupp
type log.Severity = integer
```
A level as a sink sees it: 1 error, 2 warn, 3 info, 4 debug.
### `Sink` _type_
```nupp
type log.Sink = function(level: log.Severity, module: string, line: integer, message: string): nil
```
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.
### `Target` _type_
```nupp
type log.Target = log.Sink | LuaFile
```
Where lines go: a sink function, or anything file-like to write to.
## Functions
### `log.debug` _function_
```nupp
function log.debug(fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
```
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.enabled` _function_
```nupp
function log.enabled(level: log.Level): boolean
```
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.error` _function_
```nupp
function log.error(fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
```
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.formatter` _function_
```nupp
function log.formatter(next: log.Formatter?): log.Formatter?
```
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.forModule` _function_
```nupp
function log.forModule(module: string): any
```
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 `on` a site indexes and whose `emit` it calls |
### `log.info` _function_
```nupp
function log.info(fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
```
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.level` _function_
```nupp
function log.level(level: log.Level?): log.Level
```
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.levelName` _function_
```nupp
function log.levelName(severity: log.Severity): log.Level
```
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.named` _function_
```nupp
function log.named(name: string): log.Logger
```
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.
```nupp
const log = nupp.log.named("renderer")
log:info("loaded %d entities", count)
```
#### 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.sink` _function_
```nupp
function log.sink(next: log.Target?): log.Target
```
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.timestamp` _function_
```nupp
function log.timestamp(): string
```
The stamp a line written now would carry.
#### Returns
| Type | Description |
| --- | --- |
| `string` | the rendered timestamp, or "" when stamping is off |
### `log.timestampFormat` _function_
```nupp
function log.timestampFormat(next: string?): string
```
Reads or replaces the timestamp format. An empty format stops stamping.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `next` | `string?` | the `os.date` format, or nil to read the current one |
#### Returns
| Type | Description |
| --- | --- |
| `string` | the format this call replaced |
#### Raises
- when next is not a string
### `log.warn` _function_
```nupp
function log.warn(fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil
```
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` | |