tpaga-logger
tpaga-logger
Structured logging SDK for Tpaga Node.js microservices. Emits wide-event JSON (canonical log line) to stdout for AWS CloudWatch Logs Insights, following loggingsucks.com.
Installation
pnpm add tpaga-loggerUsage
AWS Lambda
Use withLogger to wrap a Lambda handler. It automatically handles timing, correlationId extraction from headers, and emits the terminal log event on success or error.
import { createLogger, withLogger } from 'tpaga-logger';
const logger = createLogger({
service: 'my-service',
environment: process.env.STAGE ?? 'dev',
});
export const handler = withLogger(logger, 'createCharge', async (event, log) => {
log.with({ orderId: event.body.orderId, amount: event.body.amount });
const result = await chargeService.create(event.body);
return { statusCode: 200, body: JSON.stringify(result) };
});What withLogger does automatically:
- Extracts
correlationIdfromx-correlation-idorx-request-idheaders (generates a UUID if absent) - Starts a timer and calculates
durationMs - Emits
outcome: "success"withstatusCodeon completion - Emits
outcome: "error"with serialized error on failure, then re-throws
Express
Register two middlewares from the library — one before routes, one after. Both are zero-config.
import express from 'express';
import { createLogger, withTpagaExpressLogger, tpagaExpressErrorLogger } from 'tpaga-logger';
import { errorHandler } from './middleware/errorHandler';
const logger = createLogger({ service: 'url-signer-service', environment: process.env.NODE_ENV ?? 'dev' });
const app = express();
app.use(express.json());
app.use(withTpagaExpressLogger(logger)); // before routes: timing, correlationId, req.log
app.use('/api/v1/sign', signRouter);
app.use(tpagaExpressErrorLogger); // after routes: serialize + log any error
app.use(errorHandler); // after routes: map error → HTTP response (your code)Express error middleware must be registered after routes — this is an Express constraint.
What withTpagaExpressLogger does automatically:
- Extracts
correlationIdfromx-correlation-id/x-request-idheaders (generates UUID if absent) - Attaches a typed
WideEventBuildertoreq.logandres.log - Emits
outcome: "success"forstatusCode < 400,outcome: "error"otherwise - Calculates
durationMs— always matches the actual response time
What tpagaExpressErrorLogger does automatically:
- Serializes the error via
serializeError— no stack, structurederrorDetailsfor Zod-like errors - Adds
errorto the log context viareq.log.with({ error }) - Calls
next(err)to pass the error to yourerrorHandler
req.log / res.log in controllers:
req.log is available anywhere in the request lifecycle — no import needed:
export const signUrl = async (req: Request, res: Response, next: NextFunction) => {
try {
const input = signUrlSchema.parse(req.body);
req.log.with({ url: input.url, ttlSeconds: input.ttlSeconds });
res.status(200).json(generateSignedUrl(input));
} catch (err) {
req.log.with({ attemptedUrl: req.body?.url, stage: 'signing' }); // extra context on error
next(err);
}
};res.log works the same — useful in error handler middleware where req params are prefixed with _.
statusCode in the log always matches the HTTP response — it's read from res.statusCode after the response is sent.
serializeError output:
| Error type | type |
message |
errorDetails |
|---|---|---|---|
| ZodError | ZodError |
Validation failed |
array of Zod issues |
| AppError / Error | Error |
original message | — |
| Unknown | UnknownError |
String(err) |
— |
NestJS + Fastify v0.0.9+
Import from tpaga-logger/fastify. Register the plugin and the global interceptor — both are zero-config.
// app.module.ts
import { Module } from '@nestjs/common';
import { TpagaLoggerModule } from 'tpaga-logger/fastify';
@Module({
imports: [
TpagaLoggerModule.forRoot({
service: process.env.SERVICE_NAME ?? 'my-service',
environment: process.env.NODE_ENV ?? 'dev',
}),
],
})
export class AppModule {}// main.ts
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { withTpagaFastifyLogger, TpagaLoggerInterceptor, TPAGA_LOGGER } from 'tpaga-logger/fastify';
import type { Logger } from 'tpaga-logger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
const logger = app.get<Logger>(TPAGA_LOGGER);
await app.register(withTpagaFastifyLogger(logger)); // before routes: timing, correlationId, request.tpagaLog
app.useGlobalInterceptors(new TpagaLoggerInterceptor()); // catch errors before exception filters
await app.listen(3000, '0.0.0.0');
}
bootstrap();What withTpagaFastifyLogger does automatically:
- Extracts
correlationIdfromx-correlation-id/x-request-idheaders (generates UUID if absent) - Attaches a typed
WideEventBuildertorequest.tpagaLog - Emits
outcome: "success"forstatusCode < 400,outcome: "error"otherwise - Calculates
durationMs— always matches the actual response time
What TpagaLoggerInterceptor does automatically:
- Wraps every route handler and catches thrown exceptions before NestJS exception filters run
- Serializes the error via
serializeErrorand adds it to the log context viarequest.tpagaLog.with({ error }) - Re-throws so NestJS exception handling continues normally
Use
request.tpagaLog(notrequest.log) — Fastify already attaches its own pino logger torequest.log.
request.tpagaLog in controllers:
import { Controller, Get, Param, Req } from '@nestjs/common';
import type { FastifyRequest } from 'fastify';
@Controller('charges')
export class ChargesController {
@Get(':id')
async getCharge(@Req() req: FastifyRequest, @Param('id') id: string) {
req.tpagaLog.with({ chargeId: id });
const charge = await this.chargesService.findById(id);
req.tpagaLog.with({ merchantId: charge.merchantId });
return charge;
}
}API
tpaga-logger (Express + Lambda)
| Export | Description |
|---|---|
createLogger(config) |
Creates a logger instance for a service |
withLogger(logger, name, handler) |
Lambda handler wrapper |
withTpagaExpressLogger(logger) |
Express middleware (register with app.use) |
tpagaExpressErrorLogger |
Express error middleware (register after routes) |
getLog(res) |
Gets the WideEventBuilder from an Express response |
serializeError(err) |
Serializes an unknown error to a structured object |
LOG_LEVELS |
['info', 'warn', 'error', 'debug'] |
OUTCOMES |
['success', 'error', 'validation_failed', ...] |
tpaga-logger/fastify (NestJS + Fastify)
| Export | Description |
|---|---|
withTpagaFastifyLogger(logger) |
Fastify plugin — register with app.register() |
TpagaLoggerInterceptor |
NestJS interceptor — register with app.useGlobalInterceptors() |
TpagaLoggerModule |
NestJS dynamic module — import in AppModule |
TPAGA_LOGGER |
Injection token to retrieve the Logger instance via DI |
LoggerConfig
type LoggerConfig = {
service: string;
environment?: string;
redactKeys?: readonly string[]; // paths redacted with '[REDACTED]'
};WideEventBuilder (log)
type WideEventBuilder = {
with: (fields: Record<string, unknown>) => void; // add context fields
emit: (level: LogLevel, message: string, terminal: TerminalFields) => void; // send the log line
};
emitis called automatically bywithLoggerandwithTpagaExpressLogger. Call it manually only when usingstartOperationdirectly.
Local development (pretty logs)
pino-pretty is bundled in the library — no extra install needed in your service. Just start with LOG_PRETTY=true:
LOG_PRETTY=true node dist/index.js
# or for Lambda local testing:
LOG_PRETTY=true npx ts-node src/handler.tsOutput in the console:
[09:11:00.142] INFO: createCharge completed
service: "cash-in-manager"
correlationId: "abc-123"
orderId: "ORD-456"
amount: 50000
outcome: "success"
durationMs: 142
In production, leave LOG_PRETTY unset — raw JSON goes to stdout and CloudWatch Logs Insights queries it natively.
Development
pnpm install
pnpm typecheck
pnpm lint
pnpm test
pnpm buildDocs
- Log contract:
.agents/docs/SERF-5797_LOG_STRUCTURE_CONTRACT.md - Implementation plan:
.agents/docs/SERF-5796_TPAGA_LOGGER_IMPLEMENTATION_PLAN.md