logging.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. const { pino } = require('pino');
  2. const { pinoHttp, stdSerializers: pinoHttpSerializers } = require('pino-http');
  3. const uuid = require('uuid');
  4. /**
  5. * Generates the Request ID for logging and setting on responses
  6. * @param {http.IncomingMessage} req
  7. * @param {http.ServerResponse} [res]
  8. * @returns {import("pino-http").ReqId}
  9. */
  10. function generateRequestId(req, res) {
  11. if (req.id) {
  12. return req.id;
  13. }
  14. req.id = uuid.v4();
  15. // Allow for usage with WebSockets:
  16. if (res) {
  17. res.setHeader('X-Request-Id', req.id);
  18. }
  19. return req.id;
  20. }
  21. /**
  22. * Request log sanitizer to prevent logging access tokens in URLs
  23. * @param {http.IncomingMessage} req
  24. */
  25. function sanitizeRequestLog(req) {
  26. const log = pinoHttpSerializers.req(req);
  27. if (typeof log.url === 'string' && log.url.includes('access_token')) {
  28. // Doorkeeper uses SecureRandom.urlsafe_base64 per RFC 6749 / RFC 6750
  29. log.url = log.url.replace(/(access_token)=([a-zA-Z0-9\-_]+)/gi, '$1=[Redacted]');
  30. }
  31. return log;
  32. }
  33. const logger = pino({
  34. name: "streaming",
  35. // Reformat the log level to a string:
  36. formatters: {
  37. level: (label) => {
  38. return {
  39. level: label
  40. };
  41. },
  42. },
  43. redact: {
  44. paths: [
  45. 'req.headers["sec-websocket-key"]',
  46. // Note: we currently pass the AccessToken via the websocket subprotocol
  47. // field, an anti-pattern, but this ensures it doesn't end up in logs.
  48. 'req.headers["sec-websocket-protocol"]',
  49. 'req.headers.authorization',
  50. 'req.headers.cookie',
  51. 'req.query.access_token'
  52. ]
  53. }
  54. });
  55. const httpLogger = pinoHttp({
  56. logger,
  57. genReqId: generateRequestId,
  58. serializers: {
  59. req: sanitizeRequestLog
  60. }
  61. });
  62. /**
  63. * Attaches a logger to the request object received by http upgrade handlers
  64. * @param {http.IncomingMessage} request
  65. */
  66. function attachWebsocketHttpLogger(request) {
  67. generateRequestId(request);
  68. request.log = logger.child({
  69. req: sanitizeRequestLog(request),
  70. });
  71. }
  72. /**
  73. * Creates a logger instance for the Websocket connection to use.
  74. * @param {http.IncomingMessage} request
  75. * @param {import('./index.js').ResolvedAccount} resolvedAccount
  76. */
  77. function createWebsocketLogger(request, resolvedAccount) {
  78. // ensure the request.id is always present.
  79. generateRequestId(request);
  80. return logger.child({
  81. req: {
  82. id: request.id
  83. },
  84. account: {
  85. id: resolvedAccount.accountId ?? null
  86. }
  87. });
  88. }
  89. exports.logger = logger;
  90. exports.httpLogger = httpLogger;
  91. exports.attachWebsocketHttpLogger = attachWebsocketHttpLogger;
  92. exports.createWebsocketLogger = createWebsocketLogger;
  93. /**
  94. * Initializes the log level based on the environment
  95. * @param {Object<string, any>} env
  96. * @param {string} environment
  97. */
  98. exports.initializeLogLevel = function initializeLogLevel(env, environment) {
  99. if (env.LOG_LEVEL && Object.keys(logger.levels.values).includes(env.LOG_LEVEL)) {
  100. logger.level = env.LOG_LEVEL;
  101. } else if (environment === 'development') {
  102. logger.level = 'debug';
  103. } else {
  104. logger.level = 'info';
  105. }
  106. };