@pawells/logger-transport-file
v4.1.0
Published
File transport for @pawells/logger — persists structured log entries to rolling log files
Maintainers
Readme
@pawells/logger-transport-file
File transport plugin for @pawells/logger. Writes structured log entries to disk with automatic size-based and daily rotation, configurable archive retention, and an injectable fs module for deterministic unit testing.
Description
@pawells/logger-transport-file extends the @pawells/logger event bus with a file-backed transport. It appends formatted log entries to a specified file, rotates the file when it exceeds a configurable byte threshold or on a daily schedule, retains a bounded number of archives, and optionally enforces a total disk budget for archived files.
Key characteristics:
- Auto-registers on construction — unlike other built-in transports,
FileTransportcallsthis.Register(...)inside its constructor. No separateRegister()call is required or should be made. - Async initialization — directory creation and file-open happen in a background promise (
initPromise). Construction is always synchronous and never throws from I/O. - Dependency-injectable
fs— pass a mockFsModuleTypeto the constructor to avoid touching the real filesystem in unit tests. - Single-writer assumption — one
FileTransportinstance per file path. Multiple uncoordinated writers will race on rotation. - JSON output by default — uses
JSONLogFormatterunless a different formatter is supplied.
Requirements
- Node.js >= 22.0.0
@pawells/logger>=4.0.0 (peer dependency)
Installation
Install both the core package and this transport together:
npm install @pawells/logger @pawells/logger-transport-file
# or
yarn add @pawells/logger @pawells/logger-transport-fileQuick Start
import { Logger, ConsoleTransport, LogLevelFilter, LogLevels } from '@pawells/logger';
import { FileTransport } from '@pawells/logger-transport-file';
// ConsoleTransport requires an explicit Register() call
const consoleTransport = new ConsoleTransport({
filters: [LogLevelFilter(LogLevels.INFO)],
});
consoleTransport.Register();
// FileTransport auto-registers in its constructor — do NOT call Register() again
const fileTransport = new FileTransport({
filePath: '/var/log/my-app/app.log', // must be absolute
rotation: {
enabled: true,
maxFileSize: 10 * 1024 * 1024, // 10 MB
maxArchives: 5,
},
});
const logger = new Logger('api');
logger.info('Server started', { port: 3000 });
logger.warn('High memory usage', { memoryPercent: 85 });
logger.error('Request failed', new Error('Connection refused'));
// Graceful shutdown — flush pending writes and close the file handle
process.on('SIGTERM', async () => {
await fileTransport.close();
process.exit(0);
});
process.on('SIGINT', async () => {
await fileTransport.close();
process.exit(0);
});Note: Creating a
FileTransportis sufficient to start receiving log entries. Registration is automatic. Other transports (ConsoleTransport,StreamTransport,MemoryTransport, and all custom transports) require an explicitRegister()call.
API Reference
FileTransport
class FileTransport extends LogTransport<IFileTransportOptions>Writes formatted log entries to a file on disk. Extends LogTransport from @pawells/logger.
Auto-registration
FileTransport calls this.Register('file-transport:<filePath>') inside its constructor, using the file path as part of the registration name. This allows multiple FileTransport instances to coexist, each writing to a different file. Do not call Register() on a FileTransport — it is already registered. Always call close() in teardown to unregister and flush.
Constructor
constructor(options: IFileTransportOptions, fsModule?: FsModuleType)| Parameter | Type | Required | Description |
| ---------- | ---------------------- | -------- | -------------------------------------------------------------------------------- |
| options | IFileTransportOptions | Yes | Transport configuration. See IFileTransportOptions below. |
| fsModule | FsModuleType | No | Optional fs/promises-compatible module for dependency injection in tests. |
Validates ALL options synchronously via AssertFileTransportOptions and throws before any I/O occurs:
TypeError—filePathis not absolute, or contains path traversal sequences (../or./); invalidformatter(does not implementLogFormatter); non-functiononInitErrororonRotationError.RangeError—rotation.maxFileSizeis below 1024 or not an integer;rotation.maxArchivesis outside 0–100 or not an integer;rotation.maxTotalArchiveSizeis negative or not an integer.
Directory creation and file open are deferred to a background promise (initPromise) and do not throw from the constructor.
Properties
| Property | Type | Description |
| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| initPromise | Promise<void> | Resolves when async initialization (directory creation and file open) completes. Await this in tests before asserting writes. In production, errors surface automatically. |
Methods
async close(): Promise<void>Flushes all pending writes, closes the file handle, and unregisters the transport from LogManager. Safe to call multiple times (idempotent). Does not throw — stream-close errors are written to process.stderr. Call this during application shutdown to ensure all buffered entries are written to disk.
async Flush(): Promise<void>Waits for initialization and all pending writes to complete without closing the file or unregistering the transport. Useful when you need to guarantee log entries are persisted without tearing down the transport.
OnPosted(entry: TLogEntry): Promise<void>Called automatically by LogManager for each log entry that passes the transport's filter chain. Do not call this directly.
IFileTransportOptions
interface IFileTransportOptions extends ILogTransportOptions {
filePath: string;
formatter?: LogFormatter;
rotation?: IFileRotationOptions;
filters?: LogEntryPredicate[];
onInitError?: (error: FileRotationError) => void | Promise<void>;
onRotationError?: (error: FileRotationError) => void | Promise<void>;
}| Field | Type | Required | Default | Description |
| --------------- | ------------------------------------------------- | -------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| filePath | string | Yes | — | Absolute path to the log file. Relative paths throw TypeError at construction. Parent directory is created automatically with mode 0o700. |
| formatter | LogFormatter | No | JSONLogFormatter | Formatter used to convert each TLogEntry to a string before writing. |
| rotation | IFileRotationOptions | No | See below | File rotation configuration. Omit to use defaults (10 MB threshold, 5 archives, enabled). |
| filters | LogEntryPredicate[] | No | undefined | Inherited from ILogTransportOptions. All predicates must return true for an entry to be written. |
| onInitError | (error: FileRotationError) => void \| Promise<void> | No | undefined | Callback invoked when directory creation or file open fails. If omitted, errors are written to process.stderr only. |
| onRotationError | (error: FileRotationError) => void \| Promise<void> | No | undefined | Callback invoked when file rotation or archive cleanup fails. If omitted, errors are written to process.stderr only. |
IFileRotationOptions
interface IFileRotationOptions {
enabled?: boolean;
maxFileSize?: number;
maxArchives?: number;
gzipArchives?: boolean;
dailyRotation?: boolean;
maxTotalArchiveSize?: number;
}| Field | Type | Default | Description |
| -------------------- | --------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| enabled | boolean | true | Whether automatic rotation is active. Set to false to disable all rotation. |
| maxFileSize | number | 10_485_760 (10 MB) | File size in bytes at which size-based rotation is triggered. Must be at least 1024 bytes. |
| maxArchives | number | 5 | Maximum number of archived files to retain. When exceeded, the oldest archive is deleted. Set to 0 to truncate instead of archiving. Maximum value is 100. |
| gzipArchives | boolean | false | When true, rotated archive files are compressed with gzip and saved with a .gz extension. |
| dailyRotation | boolean | false | When true, the log file is rotated at midnight local time regardless of size, organizing logs by calendar date. |
| maxTotalArchiveSize| number | 0 (unlimited) | Maximum total disk space in bytes that all archived files may consume. When exceeded, the oldest archives are deleted until the total is within budget. 0 disables the limit. |
FileRotationError
class FileRotationError extends BaseError<{ code: string; cause?: Error }>Error class used when log rotation fails. FileTransport catches this error internally and routes it to process.stderr and/or the onRotationError callback — applications do not receive it as an uncaught throw. The error code is always 'FILE_ROTATION_ERROR'.
To observe rotation errors programmatically, provide an onRotationError callback in IFileTransportOptions:
const transport = new FileTransport({
filePath: '/var/log/app.log',
onRotationError: (error) => {
metrics.increment('log.rotation.error');
},
});Assertion and Validation Functions
These functions validate options objects. They are useful for validating configuration at application startup or in custom transport wrappers.
AssertFileTransportOptions
function AssertFileTransportOptions(options: unknown): asserts options is IFileTransportOptionsThrows TypeError if options is not a valid IFileTransportOptions object. Validates that filePath is a non-empty absolute string without traversal components, that formatter (if provided) implements the LogFormatter interface, that rotation (if provided) passes rotation validation, and that the error callbacks (if provided) are functions.
ValidateFileTransportOptions
function ValidateFileTransportOptions(options: unknown): booleanReturns true if options passes AssertFileTransportOptions validation, false otherwise. Non-throwing alternative.
AssertFileRotationOptions
function AssertFileRotationOptions(options: unknown): asserts options is IFileRotationOptionsThrows TypeError or RangeError if options is not a valid IFileRotationOptions object. Validates enabled (boolean), maxFileSize (finite integer >= 1024), maxArchives (integer 0–100), gzipArchives (boolean), dailyRotation (boolean), and maxTotalArchiveSize (non-negative integer).
ValidateFileRotationOptions
function ValidateFileRotationOptions(options: unknown): booleanReturns true if options passes AssertFileRotationOptions validation, false otherwise.
FsModuleType
type FsModuleType = {
mkdir: typeof fsPromises.mkdir;
open: typeof fsPromises.open;
stat: typeof fsPromises.stat;
rename: typeof fsPromises.rename;
realpath: typeof fsPromises.realpath;
readdir: typeof fsPromises.readdir;
unlink: typeof fsPromises.unlink;
}Shape of the fs/promises-compatible object accepted by the FileTransport constructor. Provide an object implementing this interface to avoid real filesystem access in unit tests. Marked @internal in source — the type is exported to support testing only and may change in minor releases.
Testing
FileTransport is designed for deterministic unit testing via the fsModule constructor parameter and initPromise property.
Three rules for testing FileTransport:
- Inject a mock
fsModule— avoids real filesystem access and makes tests portable. - Await
transport.initPromise— initialization is async; asserting before it resolves produces flaky tests. - Call
close()inafterEach— becauseFileTransportauto-registers, every test that constructs one must explicitly close it on teardown to prevent cross-test leakage.
import { describe, it, expect, afterEach } from 'vitest';
import { Logger } from '@pawells/logger';
import { FileTransport } from '@pawells/logger-transport-file';
describe('FileTransport', () => {
let transport: FileTransport;
afterEach(async () => {
// FileTransport auto-registers — always close in afterEach to unregister
await transport.close();
});
it('writes a log entry to the mock file system', async () => {
const written: string[] = [];
const mockFs = {
mkdir: async () => undefined,
open: async () => ({
write: async (data: string) => { written.push(data); },
close: async () => undefined,
}),
stat: async () => ({ size: 0 }),
rename: async () => undefined,
realpath: async (p: string) => p,
readdir: async () => [],
unlink: async () => undefined,
};
transport = new FileTransport(
{ filePath: '/tmp/test/app.log' },
mockFs as never,
);
// Await initialization before making assertions
await transport.initPromise;
const logger = new Logger('test');
logger.info('hello from test');
// Allow the write promise to settle
await new Promise<void>((resolve) => setTimeout(resolve, 10));
expect(written.length).toBeGreaterThan(0);
expect(written[0]).toContain('hello from test');
});
});NX commands for this package (run from the repository root):
yarn nx run @pawells/logger-transport-file:typecheck
yarn nx run @pawells/logger-transport-file:lint -- --fix
yarn nx run @pawells/logger-transport-file:test -- --coverage
yarn nx run @pawells/logger-transport-file:buildLicense
MIT — see LICENSE for details.
