Nest.js使用Winston时,部分日志未发送到Loki

前端开发 2026-07-10

我有一个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因上述原因拒绝一个批次时,批次中的所有日志都会被静默丢弃,而不仅仅是导致错误的那条。这就是为什么你也会丢失 logwarn

修复方法:设置 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 数组,也从不导致批处理失败。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章