logging.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { pino } from 'pino';
  2. import { pinoHttp, stdSerializers as pinoHttpSerializers } from 'pino-http';
  3. import * as uuid from '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. export 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. export 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. export 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. export 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. /**
  90. * Initializes the log level based on the environment
  91. * @param {Object<string, any>} env
  92. * @param {string} environment
  93. */
  94. export function initializeLogLevel(env, environment) {
  95. if (env.LOG_LEVEL && Object.keys(logger.levels.values).includes(env.LOG_LEVEL)) {
  96. logger.level = env.LOG_LEVEL;
  97. } else if (environment === 'development') {
  98. logger.level = 'debug';
  99. } else {
  100. logger.level = 'info';
  101. }
  102. }