使用 Winston 日志记录的 AWS Lambda 丢失请求 ID

AWS Lambda using Winston logging loses Request ID

当使用 console.log 将日志行添加到 AWS CloudWatch 时,Lambda 请求 ID 将作为 described in the docs

添加到每一行

基于上述文档的简化示例

exports.handler = async function(event, context) {
  console.log("Hello");
  return context.logStreamName
};

会产生如下输出

START RequestId: c793869b-ee49-115b-a5b6-4fd21e8dedac Version: $LATEST

2019-06-07T19:11:20.562Z c793869b-ee49-115b-a5b6-4fd21e8dedac INFO Hello

END RequestId: c793869b-ee49-115b-a5b6-4fd21e8dedac

REPORT RequestId: c793869b-ee49-115b-a5b6-4fd21e8dedac Duration: 170.19 ms Billed Duration: 200 ms Memory Size: 128 MB Max Memory Used: 73 MB

这里关于这个问题的相关细节是请求 ID,c793869b-ee49-115b-a5b6-4fd21e8dedac 添加在带有“Hello”的行的时间戳之后。

AWS 文档指出

To output logs from your function code, you can use methods on the console object, or any logging library that writes to stdout or stderr.

The Node.js runtime logs the START, END, and REPORT lines for each invocation, and adds a timestamp, request ID, and log level to each entry logged by the function.

使用 Winston 作为记录器时,请求 ID 丢失。可能与格式化程序或传输一起发布。记录器的创建方式类似于

const logger = createLogger({
    level: 'debug',
    format: combine(
      timestamp(),
      printf(
        ({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`
      )
    ),
    transports: [new transports.Console()]
  });

我还尝试了 simple() 格式化程序而不是 printf(),但这对请求 ID 是否存在没有影响。同样完全删除格式仍然打印纯文本,即没有时间戳或请求 ID。

我还检查了 Winston Console transport 的源代码,它使用 console._stdout.write(如果存在)或 console.log 进行写入,这就是 AWS 文档所说的支持。

是否有某种方法可以配置 Winston 以将 AWS Lambda 请求 ID 作为消息的一部分?

P.S。我知道 AWS CloudWatch 有单独的 Winston Transports,但它们需要其他设置功能,我希望尽可能避免这些功能。由于请求 ID 很容易获得,因此它们看起来有点矫枉过正。

P.P.S。请求 ID 也可以从 Lambda Context 和用它初始化的自定义记录器对象中获取,但我也想避免这种情况,几乎出于相同的原因:为应该随时可用的东西做额外的工作。

问题在于 console._stdout.write() / process._stdout.write() 的使用,Winston built-in Console Transport 在存在时使用。

由于某些原因,写入 stdout 的行按原样转到 CloudWatch,并且 timestamp/request ID 不会像 console.log() 调用一样添加到日志行。

有一个 discussion on Github about making this a constructor option 可以在创建传输时选择,但由于与特定 IDE 及其处理标准输出日志的方式相关的问题而被关闭。 AWS Lambdas 的问题在讨论中仅作为旁注提及。

我的解决方案是为 Winston 制作自定义传输,它始终使用 console.log() 编写消息并留下时间戳和请求 ID 以供 AWS Lambda 节点运行时填写。

2020 年 5 月新增: 以下是我的解决方案的示例。不幸的是,我不记得这个实现的很多细节,但我几乎看了 Winston sources in Github 并采取了最低限度的实现并强制使用 console.log

'use strict';

const TransportStream = require('winston-transport');

class SimpleConsole extends TransportStream {
  constructor(options = {}) {
    super(options);
    this.name = options.name || 'simple-console';
  }

  log(info, callback) {
    setImmediate(() => this.emit('logged', info));

    const MESSAGE = Symbol.for('message');
    console.log(info[MESSAGE]);

    if (callback) {
      callback();
    }
  }
};

const logger = createLogger({
  level: 'debug',
  format: combine(
    printf(({ level, message }) => `${level.toUpperCase()}: ${message}`)
  ),
  transports: [new SimpleConsole()]
});

const debug = (...args) => logger.debug(args);
// ... And similar definition to other logging levels, info, warn, error etc

module.exports = {
  debug
  // Also export other logging levels..
};

另一种选择

正如@sanrodari 在评论中指出的那样,可以通过直接覆盖 built-in 控制台传输中的日志方法并强制使用 console.log.

来实现相同的目的
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console({
      log(info, callback) {
        setImmediate(() => this.emit('logged', info));

        if (this.stderrLevels[info[LEVEL]]) {
          console.error(info[MESSAGE]);

          if (callback) {
            callback();
          }
          return;
        }

        console.log(info[MESSAGE]);

        if (callback) {
          callback();
        }
      }
    })
  ]
});

full example for more details

根据AWS docs

To output logs from your function code, you can use methods on the console object, or any logging library that writes to stdout or stderr.

我 运行 在 lambda 中使用以下 Winston 设置进行快速测试:

const path = require('path');
const { createLogger, format, transports } = require('winston');
const { combine, errors, timestamp } = format;

const baseFormat = combine(
  timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
  errors({ stack: true }),
  format((info) => {
    info.level = info.level.toUpperCase();
    return info;
  })(),
);

const splunkFormat = combine(
  baseFormat,
  format.json(),
);

const prettyFormat = combine(
  baseFormat,
  format.prettyPrint(),
);

const createCustomLogger = (moduleName) => createLogger({
  level: process.env.LOG_LEVEL,
  format: process.env.PRETTY_LOGS ? prettyFormat : splunkFormat,
  defaultMeta: { module: path.basename(moduleName) },
  transports: [
    new transports.Console(),
  ],
});

module.exports = createCustomLogger;

在 CloudWatch 中,我没有获取我的请求 ID。我从自己的日志中获取时间戳,所以我不太关心它。没有得到请求 ID 是困扰我的问题

正如@kaskelloti 已经提到的,AWS 不会转换 console._stdout.write()console._stderr.write()

记录的消息

这是我修改后的解决方案,它考虑了 AWS 日志中的级别

const LEVEL = Symbol.for('level');
const MESSAGE = Symbol.for('message');

const logger = winston.createLogger({
    transports: [
        new winston.transports.Console({
            log(logPayload, callback) {
                setImmediate(() => this.emit('logged', logPayload));
                const message = logPayload[MESSAGE]

                switch (logPayload[LEVEL]) {
                    case "debug":
                        console.debug(message);
                        break
                    case "info":
                        console.info(message);
                        break
                    case "warn":
                        console.warn(message);
                        break
                    case "error":
                        console.error(message);
                        break
                    default:
                        //TODO: handle missing levels
                        break
                }

                if (callback) {
                    callback();
                }
            }
        })
    ],
})