Logging
Type: Reference / Specification
Path: logging/
1. Purpose
The logging module provides structured, append-only log persistence for Nile applications. It writes NDJSON log entries to disk via pino, supports optional time-based file chunking, and exposes a query API for reading logs back with filters.
1.1 Responsibilities
- Log creation — Write structured NDJSON records to disk (production/test) or stdout (dev/agentic mode)
- File chunking — Optionally split log files into time-based chunks (monthly, daily, weekly) organized in per-app directories
- Log retrieval — Query logs with filters (appName, log_id, level, date range) with smart chunk selection to avoid scanning irrelevant files
- Factory API —
createLogger(appName, config?)returns a bound logger with.info(),.warn(),.error()methods
1.2 Non-Goals
- Size-based rotation — The module does not implement log rotation by file size. Only time-based chunking is supported.
- Log shipping — No built-in support for sending logs to external services (e.g., Datadog, Elasticsearch). Consumers can build this on top of the query API.
- Diagnostics logging — Internal nile diagnostics (engine, REST, server boot messages) use
createDiagnosticsLogfromutils/diagnostics-log.ts, not this module. See section 7.
2. Architecture
3. Public API
3.1 createLogger(appName, config?)
Path: logging/create-log.ts
Factory that returns a logger bound to a specific app name. Optionally accepts chunking config.
Returns: { info, warn, error } — each method takes a LogInput (same as Log minus appName).
3.2 createLog(log, config?)
Path: logging/logger.ts
Lower-level function that writes a single log entry. Used internally by createLogger.
Behavior by MODE:
prod/NODE_ENV=test— Writes NDJSON to the resolved log file path. Test mode usesappendFileSyncfor deterministic reads; prod uses pino async transport.agentic— Returns the log record as a JSON string (no file I/O).- Any other value — Prints to
console.logand returns"dev-mode, see your dev console!".
Returns: The generated log_id (nanoid, 6 chars), or JSON string in agentic mode.
Throws: If log.appName is missing.
3.3 getLogs(filters?, config?)
Path: logging/logger.ts
Reads and filters log entries from disk. Supports both flat files and chunked directories.
Filters (LogFilter):
appName— Filter by app name (required for chunked mode to locate the directory)log_id— Filter by specific log IDlevel— Filter by"info","warn", or"error"from/to— Date range filter (inclusive)
Smart chunk selection: When chunking is enabled and date filters are provided, getLogs computes the date range of each chunk file and skips files that fall entirely outside the requested range. This avoids reading and parsing irrelevant files.
Returns: Log[] — array of matching log entries.
3.4 resolveLogPath(appName, config?)
Computes the file path for a given app and chunking config. Exported for testing and advanced use.
Creates the app subdirectory if it doesn't exist.
3.5 formatChunkName(date, chunking)
Formats a date into the chunk filename (without extension). Exported for testing and reuse.
4. Key Types
4.1 Log
The level field is used both in the TypeScript interface and in the serialized NDJSON records. Previously this was type in the interface and level in the JSON — this mismatch has been normalized.
4.2 LoggerConfig
"none"(default) — Single flat file per app:logs/{appName}.log"monthly"—logs/{appName}/YYYY-MM.log"daily"—logs/{appName}/YYYY-MM-DD.log"weekly"—logs/{appName}/YYYY-WNN.log(ISO 8601 week number)
4.3 LogFilter
5. File Layout
5.1 Flat Mode (default)
5.2 Chunked Mode
Each file contains NDJSON records identical in format to flat mode. The only difference is where they are written.
6. Internal Helpers
These functions are not exported but are critical to getLogs performance:
resolveLogFiles(filters, chunking)— Determines which files to read. For flat mode, returns the single file. For chunked mode, scans the app directory and filters by date relevance.isChunkRelevant(filename, chunking, filters)— Checks if a chunk file's date range overlaps with the filter'sfrom/torange.getChunkDateRange(chunkName, chunking)— Parses a chunk filename into start/end date boundaries.readAndParseLogFiles(files)— Reads NDJSON from multiple files into a single array, skipping malformed lines.applyLogFilters(logs, filters)— AppliesappName,log_id,level, and time range filters.getISOWeekNumber(date)— ISO 8601 week number calculation.getDateFromISOWeek(year, week)— Returns the Monday of a given ISO week.
7. Diagnostics Logging (Nile Internals)
Nile's internal modules (server, engine, REST) use a separate diagnostics logging system that is distinct from this module. The createDiagnosticsLog utility in utils/diagnostics-log.ts provides centralized diagnostic output:
Behavior:
- When
diagnosticsisfalse(or absent), returns a no-op function - When
diagnosticsistrue, checksresources.loggerfirst, falls back toconsole.log - The prefix (e.g.
"Engine") is automatically prepended as[Engine]
This replaces the previous pattern where server.ts, rest.ts, and engine.ts each defined their own inline log() closures.
8. handleError — Userland Error Utility
Path: utils/handle-error.ts
A utility for application developers that combines error logging and error return in a single call. Infers atFunction from the caller stack when not provided.
8.1 Behavior
- Logger resolution: Uses explicit
loggerparam if provided, otherwise callsgetContext()and usesctx.resources.logger. If neither is available, throws. - atFunction inference: Parses
new Error().stackto extract the caller function name. Falls back to"unknown"if parsing fails. Override via theatFunctionparam. - Logging: Calls
logger.error({ atFunction, message, data })— receives alog_idback - Return: Returns
Err("[log_id] message")— error ID first, then user-facing message
8.2 Interface
8.3 Constraints
- Logger required — Throws if no explicit logger and no
resources.loggeron the context - Stack parsing — Relies on
Error().stackwhich may behave differently across runtimes. OverrideatFunctionwhen the inferred name is unhelpful (e.g., arrow functions, callbacks)
8.4 Failure Modes
- No logger available — Throws
"handleError: No logger available. Provide a logger param or set resources.logger on server config."
8. Constraints
- MODE required —
createLogthrows ifprocess.env.MODEis not set (lazy-evaluated on first log call, not at import time) - appName required —
createLogthrows iflog.appNameis missing - Chunked getLogs requires appName — Without
appName, chunked mode returns an empty array (no directory to scan) - No concurrent write safety — In test mode, uses
appendFileSync. In production, pino handles buffering. No file-level locking is implemented. - Pino transport per call — In production,
createLogcreates a new pino transport for each log entry. Callers writing many logs should usecreateLoggerand consider caching.
9. Failure Modes
- Missing MODE — Throws
"Missing MODE environment variable"on firstcreateLogcall - Missing appName — Throws immediately with the stringified log object for debugging
- Malformed log lines —
getLogssilently skips lines that failJSON.parse(NDJSON tolerance) - Missing log directory — Created automatically on first write (
mkdirSyncwith{ recursive: true }) - Unparseable chunk filenames —
isChunkRelevantreturnstrue(includes the file to be safe rather than silently dropping data)