logging.ts
Purpose
This module implements the application’s logging infrastructure. It installs global.log and global.makeLogger, writes log entries to per-level rotating files on the device filesystem, and — in debug builds — also persists every entry to a SQLite debug_logs table for in-app inspection via the Debug screen.
Its responsibilities include:
- Defining the four log levels and their numeric priority order
- Resolving and applying the active log level from an environment variable
- Writing log entries to per-level files in the Expo documents directory with byte-accurate rotation
- Keeping at most 5 rotated archive files per level
- Persisting all entries to SQLite in debug builds regardless of the console log level filter
- Routing each level to the appropriate
console.*method when the level passes the filter - Exposing
makeLoggerto create source-scoped logger objects - Installing globals so all modules can log without importing this file directly
- Providing
clearLogsto delete all log files
Invariants
Level Priority Order
Higher numeric values indicate higher verbosity. The filter passes a message when its level’s numeric value is less than or equal to the active level’s value:
| Level | Priority |
|---|---|
ERROR | 1 |
WARN | 2 |
DEBUG | 3 |
INFO | 4 |
At DEFAULT_LOG_LEVEL = "ERROR", only ERROR messages pass the filter.
Per-Level Write Queue
Each log level has its own Promise<void> write queue. File writes for the same level are serialized by chaining .then() calls.
Writes across different levels may proceed concurrently without contention.
Byte-Accurate Rotation
Before each append, the file size is checked against MAX_FILE_SIZE = 500 KB.
If the new entry would exceed the cap, the active file is renamed with a timestamp suffix and a new file is started. Rotated archives beyond MAX_ROTATIONS = 5 are deleted oldest-first.
Append-Only Writes
Log entries are written by seeking to the end of the file (handle.offset = handle.size) and calling handle.writeBytes().
This avoids the read-modify-write pattern that could corrupt lines if two writes race on different levels.
Debug Mode SQLite Persistence
When EXPO_PUBLIC_DEBUG === 'true', insertDebugLog is called for every log entry before the level filter is applied.
This means all four levels are always persisted to SQLite in debug builds, even if the console filter would suppress them.
Global Installation
The following assignments run at module load time:
global.logging ??= DEFAULT_LOG_LEVEL;
global.log = log;
global.makeLogger = makeLogger;
logging.ts must be imported before any module that calls global.log or global.makeLogger.
Exports
Level
const Level = { INFO: 4, DEBUG: 3, WARN: 2, ERROR: 1 } as const
LevelType
type LevelType = "INFO" | "DEBUG" | "WARN" | "ERROR"
DEFAULT_LOG_LEVEL
const DEFAULT_LOG_LEVEL: LevelType = "ERROR"
isLevelType(...)
function isLevelType(value: string): value is LevelType
Returns true if value is a valid LevelType string.
resolveLogLevel(...)
function resolveLogLevel(value: string | null | undefined): LevelType
Parses and returns a valid LevelType from the given string, defaulting to DEFAULT_LOG_LEVEL for invalid or absent values.
configureLogging(...)
function configureLogging(value: string | null | undefined): LevelType
Applies a log level to global.logging and returns the resolved level. Logs a warning to the console if the input is invalid.
log(...)
function log(
levels: LevelType[],
message: string,
source?: string,
...details: unknown[]
): void
Emits a log entry to all configured sinks for each level in levels.
makeLogger(...)
function makeLogger(source: string): {
debug, error, info, log, warn
}
Returns a logger object pre-bound to source with convenience methods for each level.
clearLogs()
async function clearLogs(): Promise<void>
Deletes the entire log directory and all its contents. Resets the directory-created flag.