Nest.js使用Winston时,部分日志未发送到Loki
我有一个Nest.js应用把日志发送到Loki。在Grafana中,我可以看到像 LOG [NestFactory] Starting Nest application.. 这样的日志等。但也有一些日志没有发送到Loki。
这是我的日志配置,分别用于控制台和Loki:
export const loggerConfig: winston.LoggerOptions = {
level: 'verbose',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
nestWinstonModuleUtilities.format.nestLike('NestApp', {
prettyPrint: true,
}),
),
}),
new LokiTransport({
host: process.env.LOKI_HOST!,
labels: { app: 'backend', env: process.env.NODE_ENV || 'development' },
json: true,
format: winston.format.json(),
replaceTimestamp: true,
onConnectionError: (err) => console.error(err),
}),
],
};
并且在 bootstrap() 里有这个:
const winstonInstance = createLogger(loggerConfig);
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
instance: winstonInstance,
}),
});
现在我有一个针对HTTP请求的拦截器,它在控制台和Loki上都能工作:
import { Logger } from '@nestjs/common';
........
@Injectable()
export class LoggerInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggerInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler) {
const req = context.switchToHttp().getRequest<AuthenticatedRequest>();
const user = req.user?.username ?? 'anonymous';
this.logger.log({
message: `[${req.method}] ${req.originalUrl}`,
user: user,
});
return next.handle();
}
}
但在这里,在另一个类里
import { Logger } from '@nestjs/common';
........
@Injectable()
export class TemCleanerJob {
private readonly logger = new Logger(TemCleanerJob.name);
private readonly tempDir = './uploads/temp';
private readonly maxAgeHours = 12;
constructor(private readonly s3Service: S3Service) {}
......
@Cron(CronExpression.EVERY_MINUTE)
idk() {
this.logger.log('log template');
this.logger.warn('warn log template');
this.logger.error('error log template');
throw new Error('asdfasd');
}
这些日志没有在Loki中显示,只在控制台输出。
我尝试在控制台 constructor(private readonly logger: Logger) {} 注入日志记录器,并把日志记录器添加到AppModule的 providers中
@Module({
providers: [Logger],
})
export class AppModule {}
但结果仍然一样。
解决方案
有两个独立的问题导致了这种情况。
1. Loki在 error 级别被记录时以4xx拒绝有效负载
当 NestJS Logger 调用 error() 时,nest-winston 会把上下文(或堆栈跟踪)放在一个 stack 字段中,作为一个数组,产生类似 {"stack": ["TemCleanerJob"]} 的效果。Loki会因为无法将该数组解析为有效的日志行值,而拒绝整个推送请求,返回4xx错误。你在任何地方都看不到它,因为 onConnectionError 只在连接级错误发生时触发,而不会在HTTP错误响应时触发。GitHub问题 https://github.com/JaniAnttonen/winston-loki/issues/188
解决方法是在到达Loki之前对 stack 字段进行清洗:
typescript
const cleanForLoki = winston.format((info) => {
if (Array.isArray(info.stack)) {
info.stack = info.stack.filter(Boolean).join('\n') || undefined;
if (!info.stack) delete info.stack;
}
return info;
});
2.默认分批处理在批次失败时会丢弃所有日志
使用 batching: true(默认设置),当Loki因上述原因拒绝一个批次时,批次中的所有日志都会被静默丢弃,而不仅仅是导致错误的那条。这就是为什么你也会丢失 log 和 warn。
修复方法:设置 batching: false。
最终配置
typescript
const cleanForLoki = winston.format((info) => {
if (Array.isArray(info.stack)) {
info.stack = info.stack.filter(Boolean).join('\n') || undefined;
if (!info.stack) delete info.stack;
}
return info;
});
export const loggerConfig: winston.LoggerOptions = {
level: 'verbose',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
nestWinstonModuleUtilities.format.nestLike('NestApp', {
prettyPrint: true,
}),
),
}),
new LokiTransport({
host: process.env.LOKI_HOST!,
labels: { app: 'backend', env: process.env.NODE_ENV || 'development' },
json: true,
format: winston.format.combine(
cleanForLoki(),
winston.format.json(),
),
replaceTimestamp: true,
batching: false,
onConnectionError: (err) => console.error(err),
}),
],
};
你之所以 LoggerInterceptor 能正常工作,是因为它只调用 this.logger.log(),从不产生 stack 数组,也从不导致批处理失败。