Logging estruturado JSON para APIs Node.js (NestJS, Express, Fastify) integrado com Datadog Agent no EKS.
- Como funciona
- Instalação
- Regra crítica: dd-trace antes do Pino
- NestJS
- Express
- Fastify
- Logar dentro de services e handlers
- Modos de correlação com traces
- Configuração completa
- Env vars no Kubernetes
- Checklist de validação
- Troubleshooting
- Referência da API
API (NestJS / Express / Fastify)
└── createPinoLogger → stdout JSON (uma linha por registro)
├── message — texto do log
├── level — info | warn | error | debug | trace | fatal
├── time — ISO 8601
├── service — DD_SERVICE → SERVICE_NAME → 'app'
├── env — DD_ENV → NODE_ENV → 'development'
├── version — DD_VERSION (omitido quando não definido)
└── dd.trace_id / dd.span_id (quando correlationMode: 'datadog')
Datadog Agent (containerCollectAll: true)
└── coleta stdout → Log Explorer + correlação com APM
Exemplo de saída JSON em produção:
{
"level": "info",
"time": "2025-04-15T14:32:01.123Z",
"service": "partner-service",
"env": "production",
"version": "1.4.2",
"message": "User created successfully",
"userId": "usr_abc123",
"dd": {
"trace_id": "8765432198765432",
"span_id": "1234567812345678",
"service": "partner-service",
"env": "production",
"version": "1.4.2"
}
}pnpm add @starbemtech/star-node-stack-helper pino pino-httpPara correlação com APM Datadog:
pnpm add dd-tracePara desenvolvimento local com formatação colorida (opcional):
pnpm add -D pino-prettyPara que dd.trace_id e dd.span_id apareçam nos logs, o dd-tracedeve ser inicializado antes de qualquer import de pino — inclusive o import desta lib.
Crie um arquivo dedicado e importe-o como primeira linha do entrypoint:
// src/tracing.tsimportddTracefrom'dd-trace'ddTrace.init({// DD_SERVICE, DD_ENV, DD_VERSION lidos das env vars automaticamentelogInjection: false,// a lib cuida da injeção via mixin (correlationMode: 'datadog')runtimeMetrics: true,})// src/main.ts ou src/server.tsimport'./tracing'// ← SEMPRE A PRIMEIRA LINHA// a partir daqui: qualquer import de pino ou da libimport{createPinoLogger}from'@starbemtech/star-node-stack-helper'Se não usar dd-trace, simplesmente não crie o tracing.ts e omita o import.
// src/main.tsimport'./tracing'// ← PRIMEIRO, antes de tudoimport{NestFactory}from'@nestjs/core'import{createPinoLogger,createNestPinoLoggerService,}from'@starbemtech/star-node-stack-helper'import{AppModule}from'./app.module'asyncfunctionbootstrap(){constrootLogger=createPinoLogger({correlationMode: 'datadog',// injeta dd.trace_id / dd.span_id em cada log// serviceName: 'my-api' // opcional — padrão: DD_SERVICE ou SERVICE_NAME// logLevel: 'info' // opcional — padrão: 'info'})constlogger=createNestPinoLoggerService(rootLogger,'Bootstrap')constapp=awaitNestFactory.create(AppModule,{ logger })awaitapp.listen(3000,()=>{logger.log('API running on port 3000','Bootstrap')})}bootstrap()Todos os logs internos do NestJS (bootstrap, rotas, módulos) passam pelo Pino e saem como JSON estruturado.
Para registrar requests/responses com o mesmo formato JSON (incluindo dd.trace_id):
// src/app.module.tsimport{Module}from'@nestjs/common'import{APP_INTERCEPTOR,APP_FILTER,Reflector}from'@nestjs/core'import{createPinoLogger,createPinoLogWriter,LogInterceptor,LogExceptionFilter,}from'@starbemtech/star-node-stack-helper'constrootLogger=createPinoLogger({correlationMode: 'datadog'})
@Module({providers: [{provide: APP_INTERCEPTOR,useFactory: (reflector: Reflector)=>newLogInterceptor(reflector,{logWriter: createPinoLogWriter(rootLogger,'HTTP'),skipResponseLogging: true,// evita duplicação com acesso HTTP}),inject: [Reflector],},{provide: APP_FILTER,useValue: newLogExceptionFilter({logWriter: createPinoLogWriter(rootLogger,'ExceptionFilter'),}),},],})exportclassAppModule{}Quando
logWriteré omitido, oLogInterceptore oLogExceptionFiltercontinuam funcionando com oLoggernativo do NestJS (retrocompatível).
// src/users/users.service.tsimport{Injectable}from'@nestjs/common'import{createPinoLogger,createPinoLogWriter,LogWriter,}from'@starbemtech/star-node-stack-helper'// Opção A: injetar via DI (recomendado)
@Injectable()exportclassUsersService{constructor(
@Inject('LOG_WRITER')privatereadonlylog: LogWriter,){}asynccreateUser(data: CreateUserDto){this.log.info('Creating user',{email: data.email})try{constuser=awaitthis.db.create(data)this.log.info('User created',{userId: user.id})returnuser}catch(error){this.log.error('Failed to create user',{email: data.email,error: error.message,})throwerror}}}Provendo o LogWriter via DI no AppModule:
constrootLogger=createPinoLogger({correlationMode: 'datadog'})
@Module({providers: [{provide: 'LOG_WRITER',useValue: createPinoLogWriter(rootLogger),},],})exportclassAppModule{}// src/server.tsimport'./tracing'// ← PRIMEIROimportexpressfrom'express'import{createPinoLogger,createHttpLogger,pinoLogContext,}from'@starbemtech/star-node-stack-helper'constapp=express()constlogger=createPinoLogger({correlationMode: 'datadog',})// Middleware de HTTP access log// Silencia automaticamente: /healthz, /health, /ready, /live, /ping, /metricsapp.use(createHttpLogger(logger))// Usar em handlersapp.get('/users',async(req,res)=>{logger.info({userId: req.user?.id},'Fetching users')try{constusers=awaitgetUsers()logger.info({count: users.length},'Users fetched')res.json(users)}catch(error){logger.error(pinoLogContext.error(error,{operation: 'getUsers'}),'Failed to fetch users',)res.status(500).json({error: 'Internal server error'})}})app.listen(3000,()=>{logger.info('Server started on port 3000')})// Adicionar rotas ao silêncio (além das built-in)app.use(createHttpLogger(logger,{customSilentRoutes: ['/internal/ready','/probe'],}),)O matching é por pathname, então /connect/healthz também é silenciado quando /healthz está na lista.
// src/server.tsimport'./tracing'// ← PRIMEIROimportFastifyfrom'fastify'import{createPinoLogger,buildFastifyLoggerOptions,}from'@starbemtech/star-node-stack-helper'constlogger=createPinoLogger({correlationMode: 'datadog',})// Fastify usa o logger Pino diretamente — cada request recebe um child loggerconstapp=Fastify(buildFastifyLoggerOptions(logger))app.get('/users',async(request,reply)=>{// request.log é um child do rootLogger, já com req.id boundrequest.log.info({userId: '123'},'Fetching users')constusers=awaitgetUsers()returnusers})app.listen({port: 3000},()=>{logger.info('Server started on port 3000')})Se preferir controlar o access log via hook:
import{buildFastifyLoggerOptionsQuiet}from'@starbemtech/star-node-stack-helper'constapp=Fastify(buildFastifyLoggerOptionsQuiet(logger))app.addHook('onResponse',(request,reply,done)=>{// acesso custom aquidone()})A instância retornada por createPinoLogger é um pino.Logger padrão.
// Logar com dados estruturadoslogger.info({userId: 'u123',action: 'purchase'},'Purchase completed')logger.warn({orderId: 'o456',reason: 'low_stock'},'Stock alert')logger.error({error: err.message,stack: err.stack},'Payment failed')logger.debug({query: sql, params },'DB query executed')// Child logger por contexto (herda todos os campos base + dd.*)constuserLogger=logger.child({module: 'UsersService'})userLogger.info({userId: '123'},'Processing user')import{pinoLogContext}from'@starbemtech/star-node-stack-helper'// Contexto de request (Express)logger.info(pinoLogContext.request(req,{userId: req.user.id}),'Request received')// Contexto de errologger.error(pinoLogContext.error(error,{operation: 'createUser'}),'Operation failed')// Contexto de performanceconststart=Date.now()awaitdoSomething()logger.info(pinoLogContext.performance('processPayment',Date.now()-start),'Done')// Contexto de proxy / chamada externalogger.info(pinoLogContext.proxy('https://api.external.com','ExternalService'),'Proxy request')O campo correlationMode controla como dd.trace_id e dd.span_id são injetados:
| Valor | Comportamento | Peer dep necessária |
|---|---|---|
'datadog' | Injeta dd.trace_id / dd.span_id via mixin na chamada do span ativo do dd-trace | dd-trace |
'otel' | Injeta trace_id / span_id do span ativo do @opentelemetry/api | @opentelemetry/api |
'none' | Sem injeção pela lib. Depende de DD_LOGS_INJECTION=true no ddTrace.init() | — |
Quando a peer dep não está instalada ou não há span ativo, a lib retorna {} silenciosamente — nenhum erro é lançado.
// Datadog APMconstlogger=createPinoLogger({correlationMode: 'datadog'})// OpenTelemetryconstlogger=createPinoLogger({correlationMode: 'otel'})// Sem correlação (logs puros)constlogger=createPinoLogger({correlationMode: 'none'})// ou simplesmente omitir o campo (padrão é 'none')constlogger=createPinoLogger({})import{createPinoLogger,PinoLoggerConfig}from'@starbemtech/star-node-stack-helper'constlogger=createPinoLogger({// Identidade do serviçoserviceName: 'partner-service',// padrão: DD_SERVICE → SERVICE_NAME → 'app'environment: 'production',// padrão: DD_ENV → NODE_ENV → 'development'// Nível mínimo de loglogLevel: 'info',// padrão: 'info'// Correlação de tracescorrelationMode: 'datadog',// padrão: 'none'// Rotas silenciadas no HTTP access log (além das built-in)customSilentRoutes: ['/probe','/internal/ready'],// Campos extras a redactar em staging/production (além dos padrão)customRedactPaths: ['req.body.cpf','req.body.card_number'],// Formatters customizados (raramente necessário)customFormatters: {level: (label)=>({level: label,severity: label.toUpperCase()}),},// Serializers customizados (raramente necessário)customSerializers: {req: (req: any)=>({method: req.method,url: req.url}),},})Os seguintes campos são removidos automaticamente do JSON de saída:
req.headers.authorizationreq.headers.cookiereq.body.passwordreq.body.tokenreq.body.secretres.headers["set-cookie"]
Adicionar nas env vars do deployment (.k8s/deploy-{env}.tpl.yml):
env:
- name: DD_SERVICEvalue: 'partner-service'# define service em todos os logs
- name: DD_ENVvalue: 'production'# define env em todos os logs
- name: DD_VERSIONvalue: '${VERSION}'# define version em todos os logs
- name: NODE_ENVvalue: 'production'# usado como fallback para environmentE no ConfigMap (.k8s/config.tpl.yml):
data:
DD_SERVICE: partner-serviceLOG_LEVEL: infoQuando DD_SERVICE, DD_ENV e DD_VERSION estão definidos, o createPinoLogger não precisa de nenhuma configuração explícita:
// Suficiente quando as env vars estão corretas no podconstlogger=createPinoLogger({correlationMode: 'datadog'})Após o deploy, verificar no Datadog Log Explorer:
- Logs aparecem em formato JSON (primeiro caractere
{) - Campo
messagepresente (nãomsg) - Campo
servicebate com a tagtags.datadoghq.com/servicedo pod - Campo
envbate com o ambiente do cluster - Campo
versionpresente (requerDD_VERSIONno pod) - Em requests com span ativo,
dd.trace_idestá presente no JSON - No APM → Trace → aba "Logs", os logs do request aparecem correlacionados
- Rotas de health check não aparecem no Log Explorer
- Campos sensíveis (
authorization,password,token) aparecem como[REDACTED]
- Confirmar que
import './tracing'é a primeira linha do entrypoint - Confirmar que
correlationMode: 'datadog'está emcreatePinoLogger - Confirmar que
dd-traceestá instalado:pnpm ls dd-trace - O campo só aparece quando há um span ativo — apenas durante requests HTTP instrumentados
- Em
development/local, a lib usapino-prettyautomaticamente se disponível - Em
staging/production, a saída é sempre JSON compacto - Garantir que
NODE_ENV=production(ouDD_ENV=production) está definido no pod
Verificar se a app usa prefixo global (ex.: /connect/healthz). O match é por endsWith, então /healthz como sufixo é capturado. Se o path for completamente diferente, adicionar via customSilentRoutes.
Quando se usa createHttpLogger (Express) ou Fastify com logger built-in, e também LogInterceptor, os requests podem ser logados duas vezes. Usar skipRequestLogging: true ou skipResponseLogging: true no LogInterceptor para evitar duplicação.
Instalar como devDependency no projeto consumidor:
pnpm add -D pino-prettyA lib detecta automaticamente via require.resolve.
Cria um pino.Logger configurado para Datadog.
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
serviceName | string | DD_SERVICE | SERVICE_NAME | 'app' | Nome do serviço em todos os logs |
environment | LogEnvironment | DD_ENV | NODE_ENV | 'development' | Ambiente de deploy |
logLevel | LogLevel | 'info' | Nível mínimo de log |
correlationMode | CorrelationMode | 'none' | Modo de injeção de trace/span IDs |
customSilentRoutes | string[] | [] | Rotas extras a silenciar no HTTP access log |
customRedactPaths | string[] | [] | Campos extras a redactar |
customFormatters | object | — | Formatters customizados de level e log |
customSerializers | object | — | Serializers customizados de req, res, err |
Retorna: pino.Logger
Cria middleware Express de HTTP access log baseado em pino-http.
| Parâmetro | Tipo | Descrição |
|---|---|---|
logger | pino.Logger | Logger root criado por createPinoLogger |
config.customSilentRoutes | string[] | Rotas extras a silenciar |
Comportamento por status code:
- 2xx / 3xx em rotas silenciadas →
silent(não logado) - 4xx em rotas silenciadas →
warn - 4xx em rotas normais →
warn - 5xx →
error - Erro de request →
error - 2xx / 3xx em rotas normais →
info
Cria um LoggerService do NestJS que delega para Pino. Usado no NestFactory.create.
| Parâmetro | Tipo | Descrição |
|---|---|---|
rootLogger | pino.Logger | Logger root criado por createPinoLogger |
defaultContext | string | Contexto padrão quando não informado na chamada |
Retorna: LoggerService
Mapeamento de métodos NestJS → Pino:
| NestJS | Pino |
|---|---|
log() | info() |
error() | error() |
warn() | warn() |
debug() | debug() |
verbose() | trace() |
fatal() | fatal() |
Cria um LogWriter para usar em interceptors, guards e filters.
| Parâmetro | Tipo | Descrição |
|---|---|---|
rootLogger | pino.Logger | Logger root criado por createPinoLogger |
context | string | Contexto bound no child logger |
Retorna: LogWriter
Retorna o objeto de opções para passar ao construtor do Fastify.
constapp=Fastify(buildFastifyLoggerOptions(logger))// equivalente a: Fastify({ logger })Retorna opções para Fastify com request logging desabilitado.
constapp=Fastify(buildFastifyLoggerOptionsQuiet(logger))// equivalente a: Fastify({ logger, disableRequestLogging: true })Helpers para construir objetos de contexto estruturados.
pinoLogContext.request(req,additionalData?)// contexto de request ExpresspinoLogContext.error(error,context?)// contexto de erropinoLogContext.performance(operation,duration,metadata?)// contexto de performancepinoLogContext.proxy(target,service,metadata?)// contexto de chamada externaInterface genérica de escrita estruturada. Implementada por createPinoLogWriter e pelo wrapper de fallback do Nest Logger.
interfaceLogWriter{info: (message: string,context?: Record<string,unknown>)=>voidwarn: (message: string,context?: Record<string,unknown>)=>voiderror: (message: string,context?: Record<string,unknown>)=>voiddebug: (message: string,context?: Record<string,unknown>)=>voidtrace: (message: string,context?: Record<string,unknown>)=>voidfatal: (message: string,context?: Record<string,unknown>)=>void}importtype{PinoLoggerConfig,// config de createPinoLoggerLogLevel,// 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'CorrelationMode,// 'datadog' | 'otel' | 'none'LogEnvironment,// 'development' | 'staging' | 'production' | 'local' | 'test'LogWriter,// interface genérica de log estruturadoLogInterceptorOptions,// opções do LogInterceptor NestJSLogGuardOptions,// opções do LogGuard NestJSLogExceptionFilterOptions,// opções do LogExceptionFilter NestJS}from'@starbemtech/star-node-stack-helper'