logging.ts

Configures the global logging system with per-level file rotation and SQLite debug persistence

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 makeLogger to create source-scoped logger objects
  • Installing globals so all modules can log without importing this file directly
  • Providing clearLogs to 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:

LevelPriority
ERROR1
WARN2
DEBUG3
INFO4

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.