summaryrefslogtreecommitdiffstats
path: root/streaming/logging.js
blob: e1c552c22ed8f1ae4da91189ceaace8dbcb3133b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { pino } from 'pino';
import { pinoHttp, stdSerializers as pinoHttpSerializers } from 'pino-http';
import * as uuid from 'uuid';

/**
 * Generates the Request ID for logging and setting on responses
 * @param {http.IncomingMessage} req
 * @param {http.ServerResponse} [res]
 * @returns {import("pino-http").ReqId}
 */
function generateRequestId(req, res) {
  if (req.id) {
    return req.id;
  }

  req.id = uuid.v4();

  // Allow for usage with WebSockets:
  if (res) {
    res.setHeader('X-Request-Id', req.id);
  }

  return req.id;
}

/**
 * Request log sanitizer to prevent logging access tokens in URLs
 * @param {http.IncomingMessage} req
 */
function sanitizeRequestLog(req) {
  const log = pinoHttpSerializers.req(req);
  if (typeof log.url === 'string' && log.url.includes('access_token')) {
    // Doorkeeper uses SecureRandom.urlsafe_base64 per RFC 6749 / RFC 6750
    log.url = log.url.replace(/(access_token)=([a-zA-Z0-9\-_]+)/gi, '$1=[Redacted]');
  }
  return log;
}

export const logger = pino({
  name: "streaming",
  // Reformat the log level to a string:
  formatters: {
    level: (label) => {
      return {
        level: label
      };
    },
  },
  redact: {
    paths: [
      'req.headers["sec-websocket-key"]',
      // Note: we currently pass the AccessToken via the websocket subprotocol
      // field, an anti-pattern, but this ensures it doesn't end up in logs.
      'req.headers["sec-websocket-protocol"]',
      'req.headers.authorization',
      'req.headers.cookie',
      'req.query.access_token'
    ]
  }
});

export const httpLogger = pinoHttp({
  logger,
  genReqId: generateRequestId,
  serializers: {
    req: sanitizeRequestLog
  }
});

/**
 * Attaches a logger to the request object received by http upgrade handlers
 * @param {http.IncomingMessage} request
 */
export function attachWebsocketHttpLogger(request) {
  generateRequestId(request);

  request.log = logger.child({
    req: sanitizeRequestLog(request),
  });
}

/**
 * Creates a logger instance for the Websocket connection to use.
 * @param {http.IncomingMessage} request
 * @param {import('./index.js').ResolvedAccount} resolvedAccount
 */
export function createWebsocketLogger(request, resolvedAccount) {
  // ensure the request.id is always present.
  generateRequestId(request);

  return logger.child({
    req: {
      id: request.id
    },
    account: {
      id: resolvedAccount.accountId ?? null
    }
  });
}

/**
 * Initializes the log level based on the environment
 * @param {Object<string, any>} env
 * @param {string} environment
 */
export function initializeLogLevel(env, environment) {
  if (env.LOG_LEVEL && Object.keys(logger.levels.values).includes(env.LOG_LEVEL)) {
    logger.level = env.LOG_LEVEL;
  } else if (environment === 'development') {
    logger.level = 'debug';
  } else {
    logger.level = 'info';
  }
}