Testing & Shutdown
Testing and shutdown behavior are important because some logger transports maintain internal runtime resources while the application is executing.
Buffered transports, async batch pipelines, metrics trackers, rolling file transports, and remote delivery systems may keep timers, streams, pending buffers, or background tasks alive internally.
Conceptually:
Runtime Execution
↓
Transport Resources
↓
Timers / Streams / BuffersIf those resources are not stopped correctly, they may continue running after tests complete or after the runtime itself begins shutting down.
Open handle warnings
In test environments, lingering logger resources commonly appear as open handle warnings.
A typical Jest warning looks like this:
Jest has detected open handles potentially keeping Jest from exiting.Conceptually:
Background Resource
↓
Test Completes
↓
Resource Still ActiveThis usually means a transport interval, metrics timer, open file stream, or background flush loop remained active after the test lifecycle finished.
The logger itself is often not the problem. The underlying transport lifecycle is.
Create loggers inside the test lifecycle
Logger instances should generally be created inside the test lifecycle rather than at module import time.
let logger: ReturnType<typeof createLogger>;
beforeEach(() => {
logger = createLogger({
level: 'info',
transports: []
});
});
afterEach(async () => {
await logger.shutdown?.();
});Conceptually:
Test Starts
↓
Create Logger
↓
Run Test
↓
Shutdown Logger
↓
Test EndsThis keeps resource ownership aligned with the lifecycle of the test itself.
Avoid eager transport initialization
Production transports should generally not start automatically inside modules shared by tests.
// Avoid this pattern in shared modules
export const logger = createLogger({
transports: [
createRotatingFileTransporter({
filename: './logs/runtime.log'
})
]
});Conceptually:
Module Import
↓
Transport Starts
↓
Background Resources ActiveIf the module is imported during test execution, transports may start timers or streams even when the test never emits a single runtime event.
This frequently leads to noisy test suites and unstable shutdown behavior.
Prefer lazy initialization
Lazy initialization is generally safer for reusable applications and shared packages.
let loggerInstance:
| ReturnType<typeof createLogger>
| undefined;
export function getLogger() {
if (loggerInstance) return loggerInstance;
loggerInstance = createLogger({
level: 'info',
transports:
process.env.NODE_ENV === 'test'
? []
: [
createRotatingFileTransporter({
filename: './logs/runtime.log'
})
]
});
return loggerInstance;
}
export async function shutdownLogger() {
await loggerInstance?.shutdown?.();
loggerInstance = undefined;
}Conceptually:
Application Requests Logger
↓
Lazy Initialization
↓
Controlled Runtime OwnershipThis prevents unnecessary background infrastructure from starting before the runtime actually needs it.
Explicit shutdown in tests
Tests should shut down logger resources explicitly whenever transports maintain internal lifecycle behavior.
afterEach(async () => {
await shutdownLogger();
});Conceptually:
Test Lifecycle Ends
↓
Flush Pending Entries
↓
Close ResourcesExplicit shutdown keeps test execution deterministic and prevents resource leakage across suites.
Mock transports for unit testing
Unit tests should generally prefer lightweight mock transports over real filesystem or remote infrastructure.
const transport = {
write: jest.fn().mockResolvedValue(undefined),
flush: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined)
};
const logger = createLogger({
transports: [transport],
colorize: false
});Conceptually:
Mock Transport
↓
Deterministic Runtime BehaviorMock transports avoid:
- filesystem writes
- network requests
- background timers
- environmental side effects
while still allowing transport behavior to be tested safely.
Timer-based transport testing
Timer-driven transports should generally expose explicit testing controls rather than relying on production behavior during automated tests.
const transport = new AsyncBatchTransporter({
batchSize: 10,
flushInterval: 1000,
sendBatch: async () => {},
startImmediately: false
});Conceptually:
Test Runtime
↓
Timers Disabled By DefaultThis keeps test execution predictable while still allowing interval behavior to be tested intentionally when needed.
If a test specifically needs interval execution, enable it deliberately and stop it afterward.
const transport = new AsyncBatchTransporter({
batchSize: 10,
flushInterval: 1000,
sendBatch: async () => {},
startImmediately: true,
enableTimerInTest: true
});
await transport.stop();Conceptually:
Explicit Timer Start
↓
Controlled Test Execution
↓
Explicit Timer StopTimer lifecycle should remain fully controlled during automated testing.
Graceful runtime shutdown
Shutdown behavior matters in production environments just as much as in tests.
Buffered or asynchronous transports may still contain pending runtime events when:
workers terminate
containers restart
deployments roll out
servers stop
runtime crashes occurApplications should therefore flush and close logger resources before exiting.
await logger.shutdown();Conceptually:
Pending Runtime Events
↓
Flush Pipeline
↓
Close Resources
↓
Safe Runtime ExitGraceful shutdown helps preserve telemetry continuity during runtime lifecycle transitions.
Signal handling
Applications should generally integrate logger shutdown into their own lifecycle management.
process.once('SIGTERM', async () => {
await logger.shutdown();
process.exit(0);
});Conceptually:
Termination Signal
↓
Logger Shutdown
↓
Runtime ExitThis allows transports to flush pending telemetry and close background resources safely before process termination.
Reusable package design
Reusable libraries should avoid registering global signal handlers automatically during import.
Conceptually:
Reusable Package
↓
No Automatic Process OwnershipSignal handling should remain opt-in so applications preserve control over their own lifecycle behavior.
export function registerLoggerShutdownHandlers(logger: ILogger) {
process.once('SIGINT', async () => {
await logger.shutdown?.();
process.exit(0);
});
process.once('SIGTERM', async () => {
await logger.shutdown?.();
process.exit(0);
});
}This keeps lifecycle orchestration predictable across different runtime environments.
Operational testing discipline
Testing and shutdown discipline ultimately protect runtime stability itself.
Conceptually:
Controlled Resource Lifecycle
↓
Predictable Runtime Behavior
↓
Reliable ObservabilityWithout controlled shutdown behavior, transports may leak resources, lose pending telemetry, or create unstable execution environments during both automated testing and production deployment.
Summary
Testing and shutdown behavior in Ambiten Logger are designed around controlled runtime resource management for transports that maintain timers, streams, buffers, or asynchronous delivery pipelines.
Lazy initialization prevents unnecessary background activity during tests, mock transports keep unit tests deterministic, explicit shutdown handling flushes pending runtime telemetry safely, timer lifecycle controls reduce open handle warnings, and graceful process integration ensures transports release resources correctly during both automated testing and production runtime termination.
