serverless-event-logger
v1.1.2
Published
π Lightweight, zero-dependency structured JSON logger for AWS Lambda. Optimized for observability with automatic context extraction from serverless events.
Maintainers
Readme
serverless-event-logger
Lightweight, zero-dependency structured JSON logger for AWS Lambda. Optimized for observability with automatic context extraction from serverless-event-orchestrator events.
Version: 1.1.1
Features
- Zero dependencies - Minimal bundle size (< 5KB minified)
- Structured JSON output - Perfect for CloudWatch Insights queries
- Automatic context extraction - Captures requestId, userId, tenantId, segment from NormalizedEvent
- Multi-tenant support - Extracts
tenantIdandtenantTypefor tenant-aware logging - Log level filtering - debug, info, warn, error, silent
- Sensitive data redaction - Automatically redact passwords, tokens, etc.
- Timer utility - Measure operation duration with
startTimer() - Child loggers - Add module-specific context (repositories, services)
- Error serialization -
logError()method for convenient error logging - TypeScript first - Full type definitions included
Installation
For MLHolding Lambdas (Local Workspace)
# From your lambda directory
npm install ../serverless-event-loggerOr add to package.json:
{
"dependencies": {
"serverless-event-logger": "file:../serverless-event-logger"
}
}From npm (if published)
npm install serverless-event-loggerLambda Integration Guide
This guide shows the recommended architecture for integrating the logger in MLHolding Lambdas.
1. Create Logger Instance (Module Level)
Create a centralized logger instance for your Lambda:
// src/infrastructure/logging/logger.ts
import { createLogger } from 'serverless-event-logger';
export const logger = createLogger({
service: process.env.LAMBDA_NAME || 'ml-properties-lambda',
redactPaths: ['password', 'token', 'authorization', 'apiKey', 'accessToken', 'refreshToken'],
});2. Use in Handler (with Context)
The handler extracts context from NormalizedEvent and passes the enriched logger to use cases:
// src/application/handlers/property/createProperty.handler.ts
import { NormalizedEvent, HttpResponse, httpResponse } from 'serverless-event-orchestrator';
import { logger } from '../../../infrastructure/logging/logger';
import { CreatePropertyUseCase } from '../../use-cases/property/CreatePropertyUseCase';
import { container } from '../../../infrastructure/di/container';
export async function createPropertyHandler(event: NormalizedEvent): Promise<HttpResponse> {
// Create logger with full request context (requestId, userId, tenantId, path, etc.)
const log = logger.withContext(event);
log.info('Create property request received');
try {
const useCase = container.resolve(CreatePropertyUseCase);
const result = await useCase.execute(event, log);
log.info('Property created successfully', { propertyId: result.id });
return httpResponse.created(result);
} catch (error) {
log.logError('Failed to create property', error);
throw error;
}
}3. Use in Use Cases
Use cases receive the logger and can create child loggers for sub-operations:
// src/application/use-cases/property/CreatePropertyUseCase.ts
import { injectable, inject } from 'tsyringe';
import { NormalizedEvent } from 'serverless-event-orchestrator';
import { ILogger } from 'serverless-event-logger';
import { IPropertiesRepository } from '../../../domain/repositories/IPropertiesRepository';
@injectable()
export class CreatePropertyUseCase {
constructor(
@inject('IPropertiesRepository') private readonly propertiesRepo: IPropertiesRepository,
) {}
async execute(event: NormalizedEvent, log: ILogger): Promise<Property> {
const dto = event.payload.body as CreatePropertyDTO;
const { userId, tenantId } = event.context.tenantInfo || {};
log.debug('Validating property data', { title: dto.title });
// Timer for measuring repository operation
const timer = log.startTimer('save-property');
const property = await this.propertiesRepo.create({
...dto,
userId,
tenantId,
});
timer.done({ propertyId: property.id });
return property;
}
}4. Use in Repositories (Child Logger)
Repositories use child loggers to add module context:
// src/infrastructure/repositories/DynamoPropertiesRepository.ts
import { injectable } from 'tsyringe';
import { logger } from '../logging/logger';
import { IPropertiesRepository } from '../../domain/repositories/IPropertiesRepository';
@injectable()
export class DynamoPropertiesRepository implements IPropertiesRepository {
private readonly log = logger.child({ module: 'DynamoPropertiesRepository' });
async create(data: CreatePropertyData): Promise<Property> {
const timer = this.log.startTimer('dynamodb-put');
try {
const property = new PropertyModel(data);
await property.save();
timer.done({ propertyId: property.id, tenantId: data.tenantId });
return property;
} catch (error) {
this.log.logError('Failed to save property to DynamoDB', error, {
tenantId: data.tenantId
});
throw error;
}
}
async findByTenant(tenantId: string, options?: QueryOptions): Promise<Property[]> {
this.log.debug('Querying properties by tenant', { tenantId, options });
const timer = this.log.startTimer('dynamodb-query');
const results = await PropertyModel.query('tenantId').eq(tenantId).exec();
timer.done({ count: results.length, tenantId });
return results;
}
}API Reference
createLogger(config)
Creates a new logger instance.
interface LoggerConfig {
service: string; // REQUIRED: Name of your lambda/service
defaultLevel?: LogLevel; // Default: 'info'
silent?: boolean; // Default: false (set true for tests)
redactPaths?: string[]; // Fields to redact (e.g., ['password', 'token'])
timestampFormat?: 'iso' | 'epoch'; // Default: 'iso'
pretty?: boolean; // Default: false (formatted output for local dev)
}
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';Logging Methods
logger.debug(message: string, data?: object): void // Only if LOG_LEVEL=debug
logger.info(message: string, data?: object): void
logger.warn(message: string, data?: object): void
logger.error(message: string, data?: object): voidlogger.logError(message, error, data?)
Convenience method that serializes errors automatically:
try {
await riskyOperation();
} catch (error) {
log.logError('Operation failed', error, { operationId: '123' });
// Output includes: error (message), name, stack, plus your data
}logger.withContext(event)
Creates a new logger with context extracted from a NormalizedEvent:
const log = logger.withContext(event);
// Extracts: requestId, userId, tenantId, tenantType, segment, path, method, userAgentExtracted fields from NormalizedEvent:
| Field | Source |
|-------|--------|
| requestId | event.context.requestId (or auto-generated) |
| userId | event.context.identity.userId |
| tenantId | event.context.tenantInfo.tenantId |
| tenantType | event.context.tenantInfo.tenantType |
| segment | event.context.segment |
| path | event.payload.path |
| method | event.payload.httpMethod |
| userAgent | event.payload.headers['user-agent'] |
logger.child(fields)
Creates a child logger with additional fixed fields:
const repoLog = logger.child({ module: 'DynamoPropertiesRepository' });
repoLog.info('Query executed', { table: 'Properties' });
// Output includes: module: 'DynamoPropertiesRepository'logger.startTimer(label)
Measures operation duration:
const timer = log.startTimer('external-api-call');
await callExternalAPI();
timer.done({ statusCode: 200, endpoint: '/search' });
// Output: {"message":"external-api-call completed","duration":234,"statusCode":200,...}Configuration
Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| LOG_LEVEL | Minimum log level (debug, info, warn, error, silent) | info |
| LOG_SILENT | Disable all logs (true/false) | false |
| LOG_PRETTY | Formatted JSON output for local dev | false |
Recommended template.yaml config:
Globals:
Function:
Environment:
Variables:
LOG_LEVEL: info
# LOG_PRETTY: true # Enable only for local debuggingSensitive Data Redaction
const logger = createLogger({
service: 'ml-auth-lambda',
redactPaths: ['password', 'token', 'authorization', 'apiKey', 'accessToken', 'refreshToken', 'secret'],
});
logger.info('Login attempt', {
username: 'john',
password: 'secret123',
headers: { authorization: 'Bearer xyz' }
});
// Output: {..., "password": "[REDACTED]", "headers": { "authorization": "[REDACTED]" }}Log Output Structure
Base Fields (always present)
{
"timestamp": "2024-01-15T10:30:00.000Z",
"level": "info",
"message": "Property created",
"service": "ml-properties-lambda"
}Context Fields (with withContext)
{
"timestamp": "2024-01-15T10:30:00.000Z",
"level": "info",
"message": "Property created",
"service": "ml-properties-lambda",
"requestId": "req_m2abc123_xyz789",
"userId": "user_abc123",
"tenantId": "org_century21",
"tenantType": "ORG",
"segment": "private",
"path": "/properties",
"method": "POST"
}Error Output (with logError)
{
"timestamp": "2024-01-15T10:30:00.000Z",
"level": "error",
"message": "Failed to save property",
"service": "ml-properties-lambda",
"requestId": "req_m2abc123_xyz789",
"tenantId": "org_century21",
"error": "ConditionalCheckFailedException",
"name": "Error",
"stack": "Error: ConditionalCheckFailedException\n at ..."
}CloudWatch Insights Queries
Find errors by service
fields @timestamp, message, path, method, error, userId, tenantId
| filter level = "error"
| filter service = "ml-properties-lambda"
| sort @timestamp desc
| limit 100Errors by tenant
fields @timestamp, message, error, path, userId
| filter level = "error"
| filter tenantId = "org_century21"
| sort @timestamp desc
| limit 50Activity for a specific tenant
fields @timestamp, level, message, path, method, userId
| filter tenantId = "org_century21"
| sort @timestamp desc
| limit 100Latency analysis by endpoint
fields @timestamp, path, method, duration, tenantId
| filter message like /completed/
| stats avg(duration) as avg_ms, max(duration) as max_ms, count() as requests by path, method
| sort avg_ms descSlow operations (> 1 second)
fields @timestamp, message, duration, path, tenantId, userId
| filter duration > 1000
| sort duration desc
| limit 50User activity trace
fields @timestamp, level, message, path, method, tenantId
| filter userId = "user_abc123"
| sort @timestamp desc
| limit 50Request trace by requestId
fields @timestamp, level, message, module, duration
| filter requestId = "req_m2abc123_xyz789"
| sort @timestamp ascTenant usage statistics
fields tenantId
| filter level = "info"
| stats count() as requests by tenantId
| sort requests desc
| limit 20Project Structure (Recommended)
ml-properties-lambda/
βββ src/
β βββ application/
β β βββ handlers/
β β β βββ property/
β β β βββ createProperty.handler.ts # Uses logger.withContext(event)
β β βββ use-cases/
β β βββ property/
β β βββ CreatePropertyUseCase.ts # Receives ILogger as parameter
β β
β βββ infrastructure/
β β βββ logging/
β β β βββ logger.ts # createLogger({ service: '...' })
β β β
β β βββ repositories/
β β βββ DynamoPropertiesRepository.ts # Uses logger.child({ module: '...' })
β β
β βββ domain/
β βββ repositories/
β βββ IPropertiesRepository.tsTypeScript Support
Full TypeScript definitions included:
import {
createLogger,
Logger,
LoggerConfig,
LogLevel,
Timer,
ILogger, // Interface for dependency injection
NormalizedEventLike,
ContextFields,
serializeError, // Utility for manual error serialization
} from 'serverless-event-logger';Testing
Disable logging in tests with silent: true:
// In your test setup or individual tests
import { createLogger } from 'serverless-event-logger';
const logger = createLogger({
service: 'test',
silent: true, // No console output during tests
});Or via environment variable:
LOG_SILENT=true npm testBest Practices
- One logger per Lambda - Create at module level in
src/infrastructure/logging/logger.ts - Use
withContextin handlers - Captures requestId, userId, tenantId automatically - Pass logger to use cases - Use
ILoggerinterface for testability - Use
childfor repositories/services - Adds module identification - Use
logErrorfor exceptions - Automatically serializes Error objects - Use
startTimerfor I/O operations - DynamoDB, external APIs, file operations - Redact sensitive data - Configure
redactPathsfor passwords, tokens, keys - Set LOG_LEVEL per environment -
debugfor dev,infofor production
Compatibility
- Node.js: >= 18.0.0
- TypeScript: >= 5.0
- serverless-event-orchestrator: ^1.2.0 (optional peer dependency)
License
MIT Β© MLHolding
