Logging
Glasswork provides structured logging with automatic request ID correlation, optimized for AWS Lambda and CloudWatch Logs Insights.
Quick Start
For a complete observability setup including logging, exception tracking, and request correlation, see the Observability Guide.
Default Behavior
When you don't configure logging, Glasswork:
- In Lambda: Uses plain text logging (no ANSI colors) for cleaner CloudWatch output
- In development: Uses colored console logging for readability
Pino Integration (Recommended)
For production Lambda deployments, use Pino with the built-in configuration:
npm install pinopnpm add pinoyarn add pinoimport pino from 'pino';
import { bootstrap } from 'glasswork/core';
import { lambdaPinoConfig } from 'glasswork/observability';
const { app } = await bootstrap(AppModule, {
logger: {
pino: pino(lambdaPinoConfig),
},
});What You Get
Automatic HTTP request logging:
{
"level": "info",
"requestId": "abc-123-def",
"method": "POST",
"path": "/api/users",
"status": 201,
"duration": 45,
"msg": "HTTP Request"
}Automatic request context via AsyncLocalStorage - use getRequestId() anywhere:
import { getRequestId } from 'glasswork/observability';
export class UserService {
async create(data: CreateUserDto) {
const requestId = getRequestId(); // Works automatically!
console.log(JSON.stringify({ requestId, msg: 'Creating user' }));
}
}Context-Aware Logger
For cleaner service code, create a context-aware logger:
import pino from 'pino';
import { createContextAwarePinoLogger, lambdaPinoConfig } from 'glasswork/observability';
const basePino = pino(lambdaPinoConfig);
export const logger = createContextAwarePinoLogger({
pino: basePino,
service: 'user-service',
});
// In your service
logger.info('Creating user', { email: data.email });
// Output: {"requestId":"abc-123","service":"user-service","email":"...","msg":"Creating user"}Logger Service with Dependency Injection (Recommended)
For cleaner service code, create a LoggerService that can be injected into your services:
// src/modules/common/logger.service.ts
import pino from 'pino';
import { type Logger } from 'glasswork/core';
import { createContextAwarePinoLogger, lambdaPinoConfig } from 'glasswork/observability';
const basePino = pino(lambdaPinoConfig);
export class LoggerService {
private readonly loggers = new Map<string, Logger>();
/** Get a context-aware logger for a service */
for(serviceName: string): Logger {
let logger = this.loggers.get(serviceName);
if (!logger) {
logger = createContextAwarePinoLogger({
pino: basePino,
service: serviceName,
});
this.loggers.set(serviceName, logger);
}
return logger;
}
/** Get the base pino logger (for bootstrap config) */
get pino() {
return basePino;
}
}
export const loggerService = new LoggerService();Register it in your module:
// src/modules/common/common.module.ts
import { defineModule } from 'glasswork/core';
import { loggerService } from './logger.service';
export const CommonModule = defineModule({
name: 'common',
providers: [
{ provide: 'loggerService', useValue: loggerService },
// ... other providers
],
});Use in bootstrap:
// src/server.ts
import { loggerService } from './modules/common/logger.service';
const { app } = await bootstrap(AppModule, {
logger: { pino: loggerService.pino },
});Now inject it into your services:
// src/modules/user/user.service.ts
import { type Logger } from 'glasswork/core';
import type { LoggerService } from '../common/logger.service';
export class UserService {
private readonly logger: Logger;
constructor({ loggerService }: { loggerService: LoggerService }) {
this.logger = loggerService.for('UserService');
}
async create(data: CreateUserDto) {
this.logger.info('Creating user', { email: data.email });
// Output: {"requestId":"abc-123","service":"UserService","email":"...","msg":"Creating user"}
}
}Benefits:
- Just 2 lines of boilerplate per service
requestIdautomatically included via AsyncLocalStorage- Service name automatically tagged
- Shared pino instance across all services
- Full TypeScript support
Built-in Logger in Route Handlers
When using createRoutes, a context-aware logger is automatically available in the handler context:
import { createRoutes } from 'glasswork/http';
export const userRoutes = createRoutes<{ userService: UserService }>(
(router, { userService }, route) => {
router.post('/users', ...route({
tags: ['Users'],
body: CreateUserDto,
responses: { 201: UserDto },
handler: async ({ body, logger }) => {
// Logger automatically includes requestId and uses first tag as service name
logger.info('Creating user', { email: body.email });
// Output: {"requestId":"abc-123","service":"Users","email":"...","msg":"Creating user"}
return userService.create(body);
}
}));
}
);The route handler logger:
- Uses the route's first tag (e.g.,
'Users') as the service name - Falls back to
operationIdor'Route'if no tags defined - Requires
logger.pinoconfigured in bootstrap options
User and Custom Context
import { setRequestUser, setRequestContextValue } from 'glasswork/observability';
// In auth middleware - included in all subsequent logs
setRequestUser(user.id);
// Add any custom context
setRequestContextValue('tenantId', tenant.id);CloudWatch Logs Insights
Query your structured logs:
-- Find all logs for a request
fields @timestamp, service, msg
| filter requestId = "abc-123-def"
| sort @timestamp asc
-- Slow requests
fields requestId, method, path, duration
| filter msg = "HTTP Request" and duration > 1000
| sort duration descSee CloudWatch Insights Queries for more examples.
Built-in Logger
For development or simple cases, use the built-in logger:
import { createLogger } from 'glasswork/core';
const logger = createLogger('UserService');
logger.info('Creating user', { email });
// Output: [UserService] Creating user { email: '...' }Testing
Automatic Silent Logging in Tests
When NODE_ENV=test, route handlers automatically use a silent logger if no pino logger is configured. This means you don't need to configure anything - logs are automatically silenced in tests.
// In your test setup
process.env.NODE_ENV = 'test';
// Routes created directly (without bootstrap) will use silent logger
const router = new Hono();
rangerRoutes(router, { rangerService, authService });Explicit Silent Logging with Bootstrap
If you use bootstrap in tests, configure a silent pino logger:
import pino from 'pino';
const { app } = await bootstrap(AppModule, {
logger: { pino: pino({ level: 'silent' }) },
});Note: When NODE_ENV=test and no pino logger is configured, route handlers automatically use a silent logger, so explicit configuration is optional.
Advanced Patterns
Getting Full Request Context
Access all context data, not just the request ID:
import { getRequestContext } from 'glasswork/observability';
export function getFullRequestContext() {
const ctx = getRequestContext();
return {
requestId: ctx?.requestId,
userId: ctx?.userId,
method: ctx?.method,
path: ctx?.path,
...ctx?.custom,
};
}Custom Context Logger
If you need more control than createContextAwarePinoLogger:
import pino from 'pino';
import { getRequestContext } from 'glasswork/observability';
const basePino = pino({ level: 'info' });
export const logger = {
info(msg: string, extra?: object) {
const ctx = getRequestContext();
basePino.info({
requestId: ctx?.requestId,
userId: ctx?.userId,
service: 'my-service',
...ctx?.custom,
...extra,
}, msg);
},
error(msg: string, extra?: object) {
const ctx = getRequestContext();
basePino.error({
requestId: ctx?.requestId,
userId: ctx?.userId,
service: 'my-service',
...ctx?.custom,
...extra,
}, msg);
},
};Performance Notes
AsyncLocalStorage has minimal overhead (~1-2% in typical workloads). For best performance:
// ✅ Fast - structured objects
logger.info('User created', { userId, email });
// ❌ Slower - string interpolation
logger.info(`User ${userId} created with email ${email}`);Removing Sensitive Data from Logs
Use logger.sanitizers to remove or replace sensitive values before Glasswork writes logs or reports exceptions. For example, this removes an email field from log metadata:
import pino from 'pino';
import { bootstrap } from 'glasswork/core';
const { app } = await bootstrap(AppModule, {
logger: {
pino: pino({ level: 'info' }),
sanitizers: {
metadata: (fields) => {
const safe = { ...fields };
delete safe.email;
return safe;
},
},
},
});You can supply any of these functions:
| Function | Receives | Returns |
|---|---|---|
path | A URL path | A path with sensitive parts removed or masked |
message | A log or exception message | A safe message |
error | An Error | An error safe to log or report |
metadata | An object of log or exception fields | An object with safe fields |
The functions apply to HTTP logs, route logs, startup diagnostics, and exception tracking. Each bootstrap uses its own settings. You decide which values to remove; Glasswork does not add application-specific rules. Without sanitizers, logging continues to use its defaults.
Using the Same Settings in Your Services
For application services, create a logger with createContextAwarePinoLogger({ pino, sanitizers }). It also sanitizes errors and fields added through logger.child(). If you already have a Glasswork Logger, you can supply it through logger.instance in the bootstrap options.
Services you create yourself need that logger too. Pass it through the logger option when configuring email, SES webhooks, rate limiting, or exception trackers:
EmailConfig.loggercreateSESWebhookHandler({ logger })createRateLimitMiddleware({ logger, ...options })createCloudWatchTracker({ logger })orcreateConsoleTracker({ logger })
