@heady/logger
v1.0.2
Published
  
Keywords
Readme
Structured CloudWatch Logger
A production-ready TypeScript logger wrapper around Winston, designed specifically for AWS CloudWatch Logs and CloudWatch Insights.
It enforces structured JSON logging while maintaining human-readable messages, ensuring your logs are easy to debug locally and easy to query in production.
Features
- Structured JSON: automatically formats logs for CloudWatch (no manual
JSON.stringify). - Context Awareness: automates
[Class:Method]prefixes in messages. - Metadata Support: persistent metadata (like
requestId,userId) attached to every log. - Error Optimization: extracts error messages to the top level for easy sorting in CloudWatch Insights.
- Type Safety: built with TypeScript, ships with
.d.tsdeclarations.
Installation
npm install @heady/logger
# or
yarn add @heady/loggerUsage
1. Basic Usage
Initialize the logger with a context name (usually the class name).
import { LogService } from '@heady/logger';
class UserService {
private logger = new LogService('UserService');
getUser(id: string) {
this.logger.info(
'getUser', // Method name
'fetching user data', // Short message
'checking cache first', // Detailed description
{ id, cache: true } // Data object (no JSON.stringify needed)
);
}
}Output:
{
"timestamp": "2024-01-15T10:00:00.000Z",
"level": "info",
"message": "[UserService:getUser] fetching user data",
"context": "UserService",
"method": "getUser",
"description": "checking cache first",
"data": { "id": "123", "cache": true },
"service": "my-app"
}2. Using Metadata (Request Tracing)
Pass metadata (like requestId or userId) during initialization. It will be attached to every log line generated by that instance.
// Typically done in your controller or middleware
const logger = new LogService('OrderController', {
requestId: 'req-12345',
userId: 'user-987'
});
logger.info('createOrder', 'order received');
// Every log will automatically include "requestId" and "userId"3. Error Handling
Pass the raw Error object as the last argument. The logger extracts the stack trace and lifts the error message to the top level for better visibility in CloudWatch Insights.
try {
await db.connect();
} catch (err) {
logger.error('connect', 'db connection failed', 'retrying in 5s', err);
}Output:
{
"timestamp": "2024-01-15T10:00:00.000Z",
"level": "error",
"message": "[OrderController:connect] db connection failed",
"method": "connect",
"description": "retrying in 5s",
"errorMessage": "ECONNREFUSED 127.0.0.1:5432",
"error": {
"message": "ECONNREFUSED 127.0.0.1:5432",
"name": "Error",
"stack": "Error: ECONNREFUSED ...\n at ..."
},
"service": "my-app"
}Note: Non-
Errorvalues (plain strings or objects) can also be passed as the last argument. They will be serialized intoerrorMessagewithout a stack trace.
4. Debug Logging
Debug logs are hidden by default. They only appear when the LOG_LEVEL environment variable is set to debug.
logger.debug('calc', 'loop iteration', 'checking boundaries', { x: 10, y: 20 });5. Dynamic Metadata (setMetadata)
Use setMetadata to add or update metadata on an existing instance — useful when context (like userId) is only available after an async operation such as authentication.
const logger = new LogService('AuthService', { requestId: 'req-abc' });
// Before auth: userId is unknown
logger.info('authenticate', 'verifying token');
const user = await verifyToken(token);
// After auth: attach userId to all subsequent logs
logger.setMetadata('userId', user.id);
logger.info('authenticate', 'token verified', 'user authenticated');
// This log will include both "requestId" and "userId"Configuration
Control logger behavior using environment variables.
| Variable | Default | Description |
|----------|---------|-------------|
| LOG_LEVEL | info | Set to debug for verbose logs, or error to reduce noise. |
| SERVICE_NAME | my-app | Tags all logs with a service name for filtering in aggregated log streams. |
Example:
LOG_LEVEL=debug SERVICE_NAME=user-api npm run startAPI Reference
LogContext interface
interface LogContext {
requestId?: string;
userId?: string;
[key: string]: any; // any additional fields
}LogService class
Constructor
new LogService(context: string, defaultMeta?: LogContext)| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| context | string | Yes | Identifier for the class/module (appears in every log message). |
| defaultMeta | LogContext | No | Key-value metadata attached to all logs from this instance. |
Methods
| Method | Signature | Description |
|--------|-----------|-------------|
| info | (method, message, desc?, data?) => void | Standard informational log. |
| warn | (method, message, desc?, data?) => void | Warning — something unexpected but recoverable. |
| error | (method, message, desc?, error?) => void | Error — last param is an Error object or any value. |
| debug | (method, message, desc?, data?) => void | Verbose dev log, hidden unless LOG_LEVEL=debug. |
| setMetadata | (key: string, value: any) => void | Adds or updates a metadata field on this instance. |
Shared parameters for info / warn / error / debug:
| Parameter | Type | Description |
|-----------|------|-------------|
| method | string | The method/function name. Included in the log message prefix. |
| message | string | Short, human-readable summary (used in the message field). |
| desc | string? | Optional longer description stored in the description field. |
| data / error | any | Optional payload. For error(), pass the raw Error object here. |
Log output fields
{
"timestamp": "ISO 8601",
"level": "info | warn | error | debug",
"message": "[Context:method] short message",
"context": "context name from constructor",
"method": "method name",
"description": "optional longer description",
"data": {},
"service": "SERVICE_NAME env var",
"requestId": "if set in metadata",
"userId": "if set in metadata",
"errorMessage": "error.message (error logs only)",
"error": {
"message": "...",
"name": "...",
"stack": "..."
}
}Framework Integration
AWS Lambda
import { LogService } from '@heady/logger';
export const handler = async (event: any) => {
const logger = new LogService('LambdaHandler', {
requestId: event.requestContext?.requestId
});
logger.info('handler', 'processing event', null, { source: event.source });
try {
// ... business logic
} catch (err) {
logger.error('handler', 'unhandled error', 'event processing failed', err);
throw err;
}
};NestJS Service
// app-logger.service.ts
import { Injectable, Scope } from '@nestjs/common';
import { LogService } from '@heady/logger';
@Injectable({ scope: Scope.TRANSIENT })
export class AppLogger extends LogService {
constructor() {
super('AppLogger');
}
}
// user.service.ts
@Injectable()
export class UserService {
constructor(private readonly logger: AppLogger) {
this.logger.setMetadata('service', 'UserService');
}
async getUser(id: string) {
this.logger.info('getUser', 'fetching user', undefined, { id });
// ...
}
}CloudWatch Insights Queries
Since logs are structured JSON, you can run powerful queries in AWS CloudWatch Insights.
Find all errors in a specific class:
fields @timestamp, message, errorMessage, error.stack
| filter level = "error"
| filter context = "UserService"
| sort @timestamp descTrace a specific request end-to-end:
fields @timestamp, level, message, method
| filter requestId = "req-12345"
| sort @timestamp ascFilter by service name across multiple log groups:
fields @timestamp, message, level
| filter service = "user-api"
| filter level in ["warn", "error"]
| sort @timestamp descLicense
ISC
