# Module: `nupp.log`
# `nupp.log`
Leveled logging whose disabled path is the path it is designed around.
```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.
```
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:
```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 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
```nupp
nupp.log.level("debug") -- set, answers the previous one
nupp.log.level() -- read
nupp.log.level(os.getenv("LOG_LEVEL") or "warn")
```
`"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:
```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 = io.open("game.log", "a")
nupp.log.sink(file)
```
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
```
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
```nupp
local physics = nupp.log.named("physics")
physics:warn("step %d took %.2fms", step, elapsed)
```
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.
The severity operations are compiler intrinsics. A call in statement position
whose format is a literal compiles to a level test around a direct emit, so a
filtered call evaluates none of its arguments. Every other spelling stays an
ordinary call meaning the same thing, only slower: a value rather than a call,
a computed format, a named argument, or a `nupp` some local has taken.
## Types
### `Formatter` _type_
Renders one line for a file-like destination. Only consulted when the
target is file-like; a sink function formats however it likes.
```nupp
type Formatter = function(
level: Severity,
module: string,
line: integer,
message: string,
stamp: string
): string
```
### `Level` _type_
The threshold, from silent to most verbose. Each level admits itself and
everything above it, so "warn" emits warnings and errors.
```nupp
type Level = "off" | "error" | "warn" | "info" | "debug"
```
### `Logger` _record_
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.
```nupp
record Logger
readonly name: string
debug: function(self: Logger, fmt: F, ...: unpackof __NuppFormatArguments): nil
info: function(self: Logger, fmt: F, ...: unpackof __NuppFormatArguments): nil
warn: function(self: Logger, fmt: F, ...: unpackof __NuppFormatArguments): nil
error: function(self: Logger, fmt: F, ...: unpackof __NuppFormatArguments): nil
enabled: function(self: Logger, level: Level): boolean
end
```
#### Methods
##### `debug`
Logs at debug. Accepts `string.format` directives.
```nupp
debug: function(self: Logger, fmt: F, ...: unpackof __NuppFormatArguments): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Logger` | |
| `fmt` | `F` | |
| `...` | `unpackof \_\_NuppFormatArguments\` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `info`
Logs at info. Accepts `string.format` directives.
```nupp
info: function(self: Logger, fmt: F, ...: unpackof __NuppFormatArguments): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Logger` | |
| `fmt` | `F` | |
| `...` | `unpackof \_\_NuppFormatArguments\` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `warn`
Logs at warn. Accepts `string.format` directives.
```nupp
warn: function(self: Logger, fmt: F, ...: unpackof __NuppFormatArguments): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Logger` | |
| `fmt` | `F` | |
| `...` | `unpackof \_\_NuppFormatArguments\` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `error`
Logs at error. Accepts `string.format` directives.
```nupp
error: function(self: Logger, fmt: F, ...: unpackof __NuppFormatArguments): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Logger` | |
| `fmt` | `F` | |
| `...` | `unpackof \_\_NuppFormatArguments\` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `enabled`
Whether this logger would emit at `level`.
```nupp
enabled: function(self: Logger, level: Level): boolean
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Logger` | |
| `level` | `Level` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
#### Fields
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | The name every line from this logger carries. |
### `Severity` _type_
A level as a sink sees it: 1 error, 2 warn, 3 info, 4 debug.
```nupp
type Severity = integer
```
### `Sink` _type_
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.
```nupp
type Sink = function(level: Severity, module: string, line: integer, message: string): nil
```
### `Target` _type_
Where lines go: a sink function, or anything file-like to write to.
```nupp
type Target = Sink | LuaFile
```
## Functions
### `debug` _function_
Logs at debug. Accepts `string.format` directives.
```nupp
local debug: function(fmt: F, ...: unpackof __NuppFormatArguments): nil
```
#### Type parameters
| Name | Description |
| --- | --- |
| `F` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `fmt` | `F` | |
| `...` | `unpackof \_\_NuppFormatArguments\` | |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `enabled` _function_
Whether a level would emit. Guards preparation spanning more than one
call, which no single rewritten site can elide.
```nupp
local enabled: function(level: Level): boolean
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `level` | `Level` | the level to ask about |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | whether a call at that level would reach the sink |
### `error` _function_
Logs at error. Accepts `string.format` directives.
```nupp
local error: function(fmt: F, ...: unpackof __NuppFormatArguments): nil
```
#### Type parameters
| Name | Description |
| --- | --- |
| `F` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `fmt` | `F` | |
| `...` | `unpackof \_\_NuppFormatArguments\` | |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `info` _function_
Logs at info. Accepts `string.format` directives.
```nupp
local info: function(fmt: F, ...: unpackof __NuppFormatArguments): nil
```
#### Type parameters
| Name | Description |
| --- | --- |
| `F` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `fmt` | `F` | |
| `...` | `unpackof \_\_NuppFormatArguments\` | |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `levelName` _function_
The name for a sink's numeric level.
```nupp
local levelName: function(level: Severity): Level
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `level` | `Severity` | the numeric level a sink received |
#### Returns
| Type | Description |
| --- | --- |
| `Level` | the level's name |
### `named` _function_
A logger with a fixed name. Repeating a name answers the same logger.
```nupp
local named: function(name: string): Logger
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | the name its lines carry |
#### Returns
| Type | Description |
| --- | --- |
| `Logger` | the logger |
### `timestamp` _function_
The current time formatted, recomputed at most once a second and shared
by every logger. Empty while timestamps are off.
```nupp
local timestamp: function(): string
```
#### Returns
| Type | Description |
| --- | --- |
| `string` | the cached timestamp |
### `warn` _function_
Logs at warn. Accepts `string.format` directives.
```nupp
local warn: function(fmt: F, ...: unpackof __NuppFormatArguments): nil
```
#### Type parameters
| Name | Description |
| --- | --- |
| `F` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `fmt` | `F` | |
| `...` | `unpackof \_\_NuppFormatArguments\` | |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
## Values
### `formatter` _variable_
Reads the line format, or sets it and answers the one it replaced. Only
a file-like target consults it.
```nupp
local formatter: function(): Formatter & function(format: Formatter): Formatter
```
### `level` _variable_
Reads the threshold, or sets it and answers the one it replaced.
```nupp
local level: function(): Level & function(level: Level): Level
```
### `sink` _variable_
Reads the destination, or sets it and answers the one it replaced.
```nupp
local sink: function(): Target & function(target: Target): Target
```
### `timestampFormat` _variable_
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.
```nupp
local timestampFormat: function(): string & function(format: string): string
```