@periodic/iridium
v1.0.0
Published
Production-grade, framework-agnostic structured logging library for Node.js with TypeScript support
Maintainers
Readme
🌟 Periodic Iridium
Production-grade, framework-agnostic structured logging library for Node.js with TypeScript support
Part of the Periodic series of Node.js packages by Uday Thakur.
💡 Why Iridium?
Iridium gets its name from the rare, dense metallic element known for its extreme durability and resistance to corrosion — just like how this library provides durable, reliable logging that won't fail under pressure.
In chemistry, iridium is one of the most corrosion-resistant metals known, found in the Earth's crust and meteorites. Similarly, @periodic/iridium was forged through real-world production experience to create logging infrastructure that's both resilient and elegant.
The name represents:
- Durability: Never throws, never fails, production-tested
- Clarity: Clean, structured logs with consistent formatting
- Precision: Type-safe with strict TypeScript support
- Rarity: A truly dependency-free, first-class logging core
Just as iridium is a fundamental building block in high-precision instruments, @periodic/iridium serves as the foundational logging layer for production-grade Node.js applications.
🎯 Why Choose Iridium?
Building robust applications requires reliable, structured logging, but most solutions come with significant challenges:
- Existing loggers (Winston, Pino, Bunyan) are heavy with many dependencies
- Framework-coupled solutions make library reuse difficult
- Global state in loggers causes problems in multi-tenant apps
- Configuration complexity leads to maintenance nightmares
- Wrapper libraries add layers without solving core issues
Periodic Iridium provides the perfect solution:
✅ Zero dependencies - Pure TypeScript logging core
✅ Framework-agnostic - Safe for use in both libraries and applications
✅ Five log levels - debug, info, warn, error, fatal
✅ Structured logging with first-class metadata support
✅ Child loggers with immutable context inheritance
✅ Custom transports - Console, Memory, and extensible
✅ Pretty & JSON formatters with TTY-aware colorization
✅ Sensitive data redaction built-in
✅ Type-safe with strict TypeScript from the ground up
✅ No global state - No side effects on import
✅ Production-ready - Never throws, graceful error handling
📦 Installation
npm install @periodic/iridiumOr with yarn:
yarn add @periodic/iridiumNo other dependencies required! Iridium is completely dependency-free.
🚀 Quick Start
import { createLogger, ConsoleTransport } from '@periodic/iridium';
const logger = createLogger({
level: 'info',
transports: [new ConsoleTransport()],
});
// Start logging
logger.info('Application started');
logger.warn('Low memory detected', { available: '512MB' });
logger.error('Database connection failed', { host: 'db.example.com' });
// With metadata
logger.info('User logged in', {
userId: 'user_123',
email: '[email protected]',
ipAddress: '192.168.1.1',
});Console Output:
[2024-02-13T10:30:00.000Z] INFO Application started
[2024-02-13T10:30:01.000Z] WARN Low memory detected {"available":"512MB"}
[2024-02-13T10:30:02.000Z] ERROR Database connection failed {"host":"db.example.com"}🧠 Core Concepts
The createLogger Function
createLoggeris the primary factory function- Returns a configured
Loggerinstance - Accepts flexible configuration options
- This is the main entry point for all applications
- No global state, safe for multi-tenant apps
Typical usage:
- Application code creates loggers with
createLogger() - Libraries export logger instances for consumers
- Tests create isolated loggers with MemoryTransport
const logger = createLogger({
level: 'info',
transports: [new ConsoleTransport()],
processors: [],
redact: { paths: ['password'] }
});The Logger Class
Loggeris the core logging class- Provides five logging methods: debug, info, warn, error, fatal
- Supports child logger creation with context
- Never throws - all errors routed to
onErrorhandler - Immutable context merging for safety
Design principle:
Applications create loggers, libraries use loggers, frameworks extend loggers.
// Logging
logger.info('User registered', { userId: '123' });
// Child logger with context
const requestLogger = logger.child({ requestId: 'req_abc' });
requestLogger.info('Processing request'); // Includes requestId
// Error handling
try {
await riskyOperation();
} catch (error) {
logger.error('Operation failed', { error });
}✨ Features
🎚️ Five Log Levels
Complete control over logging verbosity:
logger.debug('Detailed debugging info');
logger.info('Informational message');
logger.warn('Warning message');
logger.error('Error occurred');
logger.fatal('System failure');Level Hierarchy:
debug < info < warn < error < fatalSet minimum level to filter logs:
const logger = createLogger({
level: 'warn', // Only warn, error, fatal will be logged
transports: [new ConsoleTransport()],
});🎯 Structured Logging
First-class support for structured metadata:
logger.info('Payment processed', {
userId: 'user_123',
amount: 99.99,
currency: 'USD',
paymentMethod: 'card',
transactionId: 'txn_abc123',
});JSON Output:
{
"timestamp": "2024-02-13T10:30:00.000Z",
"level": "info",
"message": "Payment processed",
"meta": {
"userId": "user_123",
"amount": 99.99,
"currency": "USD",
"paymentMethod": "card",
"transactionId": "txn_abc123"
}
}🧒 Child Loggers
Create child loggers with inherited context:
// Parent logger
const logger = createLogger({
transports: [new ConsoleTransport()],
});
// Child logger for a specific request
const requestLogger = logger.child({
requestId: 'req_abc123',
userId: 'user_456',
method: 'POST',
path: '/api/users',
});
// All logs include the context
requestLogger.info('Request started');
requestLogger.info('Validating input');
requestLogger.info('Creating user');
requestLogger.info('Request completed');
// Output includes: requestId, userId, method, path in every logContext is immutable:
const parent = logger.child({ service: 'api' });
const child = parent.child({ requestId: '123' });
child.info('test'); // Context: { service: 'api', requestId: '123' }
parent.info('test'); // Context: { service: 'api' }🛡️ Error Serialization
Automatic, safe error handling:
try {
await connectToDatabase();
} catch (error) {
logger.error('Database connection failed', { error });
}Output:
{
"timestamp": "2024-02-13T10:30:00.000Z",
"level": "error",
"message": "Database connection failed",
"error": {
"name": "Error",
"message": "Connection refused",
"stack": "Error: Connection refused\n at ..."
}
}🎨 Formatters
Pretty Formatter (Human-Readable)
import { PrettyFormatter } from '@periodic/iridium';
const logger = createLogger({
transports: [
new ConsoleTransport({
formatter: new PrettyFormatter({ colorize: true }),
}),
],
});Output:
[2024-02-13T10:30:00.000Z] INFO User logged in {"userId":"123"}
[2024-02-13T10:30:01.000Z] WARN Low memory {"available":"512MB"}
[2024-02-13T10:30:02.000Z] ERROR Connection failed {"host":"db.example.com"}JSON Formatter (Machine-Readable)
import { JsonFormatter } from '@periodic/iridium';
const logger = createLogger({
transports: [
new ConsoleTransport({
formatter: new JsonFormatter(),
}),
],
});Output:
{"timestamp":"2024-02-13T10:30:00.000Z","level":"info","message":"User logged in","meta":{"userId":"123"}}🔒 Sensitive Data Redaction
Built-in support for redacting sensitive fields:
const logger = createLogger({
level: 'info',
transports: [new ConsoleTransport()],
redact: {
paths: ['password', 'apiKey', 'user.ssn', 'creditCard.number'],
censor: '[REDACTED]',
},
});
logger.info('User registered', {
username: 'john_doe',
password: 'super_secret_123', // Will be redacted
email: '[email protected]',
apiKey: 'sk_live_1234567890', // Will be redacted
user: {
name: 'John Doe',
ssn: '123-45-6789', // Will be redacted
},
});Output:
{
"username": "john_doe",
"password": "[REDACTED]",
"email": "[email protected]",
"apiKey": "[REDACTED]",
"user": {
"name": "John Doe",
"ssn": "[REDACTED]"
}
}🔌 Custom Processors
Transform log records before output:
import { addFieldsProcessor, addCorrelationIdProcessor } from '@periodic/iridium';
const logger = createLogger({
level: 'info',
transports: [new ConsoleTransport()],
processors: [
// Add static fields to every log
addFieldsProcessor({
environment: process.env.NODE_ENV,
version: process.env.APP_VERSION,
hostname: require('os').hostname(),
}),
// Add correlation ID
addCorrelationIdProcessor(() =>
Math.random().toString(36).substring(7)
),
],
});
logger.info('Application started');
// Output includes: environment, version, hostname, correlationIdCustom Processor:
import type { LogProcessor } from '@periodic/iridium';
const timestampProcessor: LogProcessor = (record) => ({
...record,
meta: {
...record.meta,
timestampMs: Date.now(),
},
});
const logger = createLogger({
processors: [timestampProcessor],
transports: [new ConsoleTransport()],
});📊 Multiple Transports
Send logs to multiple destinations:
import { ConsoleTransport, MemoryTransport } from '@periodic/iridium';
const memory = new MemoryTransport();
const logger = createLogger({
level: 'info',
transports: [
new ConsoleTransport(), // Output to console
memory, // Store in memory
],
});
logger.info('test');
// Later: check memory logs
console.log(`Logged ${memory.count()} records`);
console.log(memory.getRecords());📚 Common Patterns
1. Express.js Integration
import express from 'express';
import { createLogger, ConsoleTransport } from '@periodic/iridium';
const logger = createLogger({
level: process.env.LOG_LEVEL || 'info',
transports: [new ConsoleTransport()],
});
const app = express();
// Request logging middleware
app.use((req, res, next) => {
req.logger = logger.child({
requestId: req.id || Math.random().toString(36).substring(7),
method: req.method,
path: req.path,
userAgent: req.get('user-agent'),
});
req.logger.info('Request started');
next();
});
// Use request-specific logger
app.get('/api/users', async (req, res) => {
req.logger.info('Fetching users');
const users = await db.users.findAll();
res.json(users);
});
// Error handling
app.use((err, req, res, next) => {
req.logger.error('Request failed', { error: err });
res.status(500).json({ error: 'Internal Server Error' });
});2. Service Layer Logging
class UserService {
constructor(private logger: Logger) {}
async createUser(data: UserData) {
const log = this.logger.child({ operation: 'createUser' });
log.info('Creating user', { email: data.email });
try {
const user = await db.users.create(data);
log.info('User created successfully', { userId: user.id });
return user;
} catch (error) {
log.error('Failed to create user', { error });
throw error;
}
}
}
const userService = new UserService(logger);3. Background Jobs
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
const logger = createLogger({
level: 'info',
transports: [
new ConsoleTransport({
formatter: new JsonFormatter(), // JSON for log aggregation
}),
],
});
async function processJob(job: Job) {
const log = logger.child({
jobId: job.id,
jobType: job.type,
});
log.info('Job started');
try {
await job.process();
log.info('Job completed successfully');
} catch (error) {
log.error('Job failed', { error, attempts: job.attempts });
throw error;
}
}4. Testing with Memory Transport
import { createLogger, MemoryTransport } from '@periodic/iridium';
describe('UserService', () => {
let logger: Logger;
let memory: MemoryTransport;
let userService: UserService;
beforeEach(() => {
memory = new MemoryTransport();
logger = createLogger({
level: 'debug',
transports: [memory],
});
userService = new UserService(logger);
});
it('should log user creation', async () => {
await userService.createUser({ email: '[email protected]' });
const logs = memory.getRecordsByLevel('info');
expect(logs).toHaveLength(2);
expect(logs[0].message).toBe('Creating user');
expect(logs[1].message).toBe('User created successfully');
});
});5. Production Configuration
import winston from 'winston';
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
const isDevelopment = process.env.NODE_ENV === 'development';
const logger = createLogger({
level: process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'info'),
transports: [
new ConsoleTransport({
formatter: isDevelopment
? new PrettyFormatter({ colorize: true })
: new JsonFormatter(),
stderr: true, // error/fatal go to stderr
}),
],
processors: [
addFieldsProcessor({
service: 'api',
environment: process.env.NODE_ENV,
version: process.env.npm_package_version,
hostname: require('os').hostname(),
}),
],
redact: {
paths: [
'password',
'token',
'apiKey',
'secret',
'creditCard',
'ssn',
],
},
onError: (error) => {
// Log logger errors to external service
console.error('Logger error:', error);
},
});
export default logger;6. Graceful Shutdown
const logger = createLogger({
level: 'info',
transports: [new ConsoleTransport()],
});
async function shutdown(signal: string) {
logger.info('Shutdown initiated', { signal });
// Flush all async transports
await logger.flush();
logger.info('Shutdown complete');
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));🎛️ Configuration Options
Logger Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| level | LogLevel | 'info' | Minimum level to log |
| transports | Transport[] | [] | Output destinations |
| processors | LogProcessor[] | [] | Record transformers |
| redact | RedactionConfig | - | Sensitive data redaction |
| clock | ClockFunction | - | Custom timestamp function |
| onError | (error) => void | - | Error handler |
const logger = createLogger({
level: 'info',
transports: [new ConsoleTransport()],
processors: [addFieldsProcessor({ service: 'api' })],
redact: { paths: ['password'], censor: '[REDACTED]' },
clock: () => new Date().toISOString(),
onError: (error) => console.error('Logger error:', error),
});Console Transport Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| formatter | Formatter | PrettyFormatter | Output formatter |
| colorize | boolean | Auto-detect | Enable colors |
| stderr | boolean | true | Route error/fatal to stderr |
new ConsoleTransport({
formatter: new JsonFormatter(),
colorize: false,
stderr: true,
});Redaction Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| paths | string[] | - | Fields to redact (supports nested) |
| censor | string | '[REDACTED]' | Replacement value |
redact: {
paths: ['password', 'apiKey', 'user.ssn'],
censor: '***',
}📋 API Reference
createLogger(config?)
Create a new logger instance.
function createLogger(config?: LoggerConfig): LoggerLogger Class
Main logger interface.
class Logger {
debug(message: string, meta?: Record<string, unknown>): void
info(message: string, meta?: Record<string, unknown>): void
warn(message: string, meta?: Record<string, unknown>): void
error(message: string, meta?: Record<string, unknown>): void
fatal(message: string, meta?: Record<string, unknown>): void
child(context: LogContext): Logger
flush(): Promise<void>
}Transports
ConsoleTransport
new ConsoleTransport(options?: {
formatter?: Formatter;
colorize?: boolean;
stderr?: boolean;
})MemoryTransport
const memory = new MemoryTransport();
memory.log(record: LogRecord): void
memory.getRecords(): LogRecord[]
memory.getRecordsByLevel(level: string): LogRecord[]
memory.clear(): void
memory.count(): numberFormatters
PrettyFormatter
new PrettyFormatter(options?: {
colorize?: boolean;
})JsonFormatter
new JsonFormatter(options?: {
pretty?: boolean;
})Processors
addFieldsProcessor(fields: Record<string, unknown>): LogProcessor
addCorrelationIdProcessor(getId: () => string): LogProcessor
composeProcessors(...processors: LogProcessor[]): LogProcessor🧩 Architecture
@periodic/iridium/
├── src/
│ ├── core/ # Framework-agnostic core
│ │ ├── types.ts # TypeScript interfaces
│ │ ├── levels.ts # Log level utilities
│ │ ├── logger.ts # Main Logger class
│ │ ├── record.ts # Record building
│ │ └── processors.ts # Processor utilities
│ ├── transports/ # Transport implementations
│ │ ├── console.ts # Console transport
│ │ └── memory.ts # Memory transport
│ ├── formatters/ # Formatter implementations
│ │ ├── pretty.ts # Pretty formatter
│ │ └── json.ts # JSON formatter
│ ├── utils/ # Utilities
│ │ ├── serializeError.ts # Error serialization
│ │ └── redact.ts # Redaction
│ └── index.ts # Public APIDesign Philosophy:
- Core is pure TypeScript with no dependencies
- Transports are pluggable output destinations
- Formatters control output format
- Processors enable extensibility
- Easy to extend for custom transports and formatters
📈 Performance
Iridium is optimized for production workloads:
- Early level filtering - Disabled logs have near-zero overhead
- No unnecessary allocations - Minimal object creation
- Numeric comparisons - Fast level checks
- Deferred stringification - JSON.stringify only in transports
- Zero dependencies - No external overhead
Benchmark Results (Node.js 20):
Disabled logs (filtered): ~2,000,000 ops/sec
Active logs (memory): ~500,000 ops/sec
Active logs with metadata: ~450,000 ops/sec
Child logger: ~400,000 ops/sec
Multi-transport (2x): ~300,000 ops/sec
With processor: ~400,000 ops/sec
With redaction: ~350,000 ops/secRun benchmarks:
npm run benchmark🚫 Explicit Non-Goals
This package intentionally does not include:
❌ File transport (use external packages)
❌ HTTP transport (use external packages)
❌ Log rotation (use external tools)
❌ Log aggregation (use Elasticsearch, Datadog, etc.)
❌ Metrics collection (use Prometheus, StatsD, etc.)
❌ Tracing (use OpenTelemetry)
❌ Configuration files (configure in code)
❌ Automatic framework detection (explicit integration)
Focus on doing one thing well: structured logging core.
🤝 Related Packages
Part of the Periodic series by Uday Thakur:
- @periodic/obsidian - HTTP error handling
- @periodic/titanium - Rate limiting
- @periodic/osmium - Redis caching
Build complete, production-ready APIs with the Periodic series!
📖 Documentation
🧪 Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watch
# Run benchmarks
npm run benchmarkNote: All tests achieve >80% code coverage.
🛠️ Production Recommendations
Environment Variables
LOG_LEVEL=info
NODE_ENV=production
APP_VERSION=1.0.0Log Aggregation
Use JSON formatter with log aggregation services:
const logger = createLogger({
transports: [
new ConsoleTransport({
formatter: new JsonFormatter(),
}),
],
});
// Pipe to Elasticsearch, Datadog, CloudWatch, etc.Error Monitoring
Integrate with error tracking:
const logger = createLogger({
onError: (error) => {
Sentry.captureException(error);
},
});Performance Monitoring
const logger = createLogger({
processors: [
(record) => ({
...record,
meta: {
...record.meta,
duration: performance.now(),
},
}),
],
});🎨 TypeScript Support
Full TypeScript support with complete type safety:
import type {
Logger,
LogLevel,
LogRecord,
LogContext,
Transport,
Formatter,
LogProcessor,
RedactionConfig,
} from '@periodic/iridium';
const logger: Logger = createLogger({
level: 'info' as LogLevel,
transports: [new ConsoleTransport()],
});
const context: LogContext = { requestId: '123' };
const child: Logger = logger.child(context);📝 License
MIT © Uday Thakur
🙏 Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details on:
- Code of conduct
- Development setup
- Pull request process
- Coding standards
- Architecture principles
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
🌟 Show Your Support
Give a ⭐️ if this project helped you build better applications!
Built with ❤️ by Uday Thakur for production-grade Node.js applications
