# 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 ```