@tony4ork/nestjs-logger
v0.0.2
Published
Company-standard logger for NestJS: pino, single-line JSON, OpenTelemetry field names
Readme
@tony4ork/nestjs-logger
The standard logger for all of the company's NestJS products. Built on pino, writes single-line JSON to stdout, with field names following the OpenTelemetry convention so SigNoz can automatically correlate logs with traces.
Developers don't need to learn anything new: you still write new Logger(OrderService.name) exactly like standard NestJS.
Log format
{
"level": "info",
"time": "2026-09-28T08:15:30.123Z",
"service.name": "product-x",
"service.version": "1.4.2",
"deployment.environment": "production",
"request_id": "a1b2c3d4-...",
"user.id": "u_123",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"trace_flags": "01",
"context": "OrderService",
"msg": "Order created",
"orderId": "ord_789"
}| Field | Source |
|---|---|
| service.name, service.version, deployment.environment | Fixed at startup, from options or the OTEL_* env vars |
| request_id, user.id, job.id | Automatic, from @tony4ork/nestjs-context |
| client.app_version, client.platform, client.bundle_id | Automatic, from the x-app-version, x-os, x-app-bundle-id headers (read by @tony4ork/nest-core) |
| trace_id, span_id, trace_flags | Automatic, from OpenTelemetry once tracing is enabled; absent otherwise |
| context | The class name passed to new Logger(...) |
| err | Present only when logging an error: type, message, stack |
The HTTP log (one line per request) uses OpenTelemetry attribute names: http.request.method, url.path, http.route, http.response.status_code, duration_ms, client.address, user_agent.original.
Installation
pnpm add @tony4ork/nestjs-logger @tony4ork/nestjs-context
pnpm add -D pino-pretty # for easy reading locally1. Declare the configuration in src/integrations/logger.integration.ts:
import { ConfigService } from '@nestjs/config';
import { LoggerModule } from '@tony4ork/nestjs-logger';
export const LoggerIntegration = LoggerModule.forRootAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
serviceName: cfg.get('OTEL_SERVICE_NAME', 'product-x'),
version: cfg.get('APP_VERSION'),
env: cfg.get('NODE_ENV'),
level: cfg.get('LOG_LEVEL', 'info'),
pretty: cfg.get('NODE_ENV') === 'local',
}),
});2. Import it in AppModule, after ContextModule:
imports: [ContextModule.forRoot(), LoggerIntegration, ...]3. Replace Nest's logger in main.ts:
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(app.get(AppLogger));
await app.listen(3000);bufferLogs: true holds onto startup logs until the logger is ready. app.listen() automatically flushes them. If you use NestFactory.createApplicationContext() (a worker with no HTTP), call app.flushLogs() after useLogger.
Usage
@Injectable()
export class OrderService {
private readonly logger = new Logger(OrderService.name);
async create(dto: CreateOrderDto) {
const order = await this.repo.save(dto);
this.logger.log('Order created', { orderId: order.id }); // style 1
this.logger.log({ msg: 'Order created', orderId: order.id }); // style 2, equivalent
return order;
}
async charge(orderId: string) {
try {
await this.payment.charge(orderId);
} catch (err) {
this.logger.error('Charge failed', err, { orderId }); // pass the Error to keep the stack
throw err;
}
}
}Log levels: error, warn, log (info), debug, verbose (trace), fatal.
Outside Nest DI (scripts, seeds, migrations):
import { createLogger } from '@tony4ork/nestjs-logger';
import { runWithContext } from '@tony4ork/nestjs-context';
const logger = createLogger({ serviceName: 'product-x-seed' }).forContext('Seed');
await runWithContext({}, async () => logger.log('Seeding...'));Configuration
| Option | Default | Note |
|---|---|---|
| serviceName | OTEL_SERVICE_NAME | Required in at least one of the two places |
| version | service.version in OTEL_RESOURCE_ATTRIBUTES, then APP_VERSION | |
| env | deployment.environment in OTEL_RESOURCE_ATTRIBUTES, then NODE_ENV | |
| level | LOG_LEVEL, then info | An invalid value falls back to info |
| pretty | false | Requires pino-pretty; falls back to JSON with a warning if missing. Customized via messageFormat/customPrettifiers to print in NestJS's default ConsoleLogger format + colors ([Nest] pid - time LEVEL [Context] message +Xms); fields outside the core set (request_id, duration_ms, orderId...) and err are still printed below by pino-pretty as usual |
| redactKeys | | Additional field names to redact |
| redactPaths | | pino redact paths, for redacting deeply nested fields, e.g. payload.card.number |
| base | | Fixed fields added to every log line |
| http.enabled | true | Disables the per-request log |
| http.ignorePaths | /health, /ready, /metrics, /favicon.ico | Strings (exact or prefix match) or a RegExp |
| http.slowThresholdMs | 0 (disabled) | Requests slower than the threshold are raised to warn |
It's best for the logger and OpenTelemetry to read the same set of env vars, so the service name always matches on SigNoz:
OTEL_SERVICE_NAME=product-x
OTEL_RESOURCE_ATTRIBUTES=service.version=1.4.2,deployment.environment=productionRedacting sensitive data
The following fields are always replaced with [REDACTED] at the root level and one level of nesting: password, newPassword, oldPassword, token, accessToken, refreshToken, idToken (and their snake_case forms), secret, clientSecret, apiKey, authorization, cookie, otp, pin, cardNumber, cvv, cvc.
Limitation to be aware of: exact name match (case-sensitive) and only one level deep. { user: { password } } is redacted, but { a: { b: { password } } } is not — use redactPaths for that case. The safest approach is still to avoid logging a whole request/entity object.
The HTTP log does not record the query string, headers, or body.
HTTP request log levels
5xx logs at error, 4xx logs at warn, a client disconnecting mid-request logs at warn with http.aborted: true, everything else logs at info.
Compatibility
NestJS 10 and 11, Node 18.18 or later. The test suite runs against both Nest versions.
Important note: if a project ends up with two copies of @nestjs/common in node_modules (usually from a version mismatch between the app and an SDK), app.useLogger() only applies to one copy — logs from the other copy print in Nest's default format instead of JSON. Every @tony4ork/* SDK must declare @nestjs/* as a peerDependency. Check with pnpm why @nestjs/common.
Dependencies and imports
@tony4ork/nestjs-context is a peerDependency: a project declares both packages. Import context functions directly from it instead of through this logger:
import { RequestContext, contextForJob, runWithContext } from '@tony4ork/nestjs-context';HTTP client error logging
Pino's default error serializer copies every property of the error. For axios errors, that pulls config (with the Authorization header and outgoing body) into the log. This logger strips config, request, response from axios errors, keeping only message, code, stack, and status.
