UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

186 lines (185 loc) 6.75 kB
import { useEnv } from '@directus/env'; import { REDACTED_TEXT, toArray, toBoolean } from '@directus/utils'; import { merge } from 'lodash-es'; import { URL } from 'node:url'; import { pino } from 'pino'; import { pinoHttp, stdSerializers } from 'pino-http'; import { httpPrintFactory } from 'pino-http-print'; import { build as pinoPretty } from 'pino-pretty'; import { getConfigFromEnv } from '../utils/get-config-from-env.js'; import { LogsStream } from './logs-stream.js'; import { redactQuery } from './redact-query.js'; export const _cache = { logger: undefined, logsStream: undefined, httpLogsStream: undefined }; export const useLogger = () => { if (_cache.logger) { return _cache.logger; } _cache.logger = createLogger(); return _cache.logger; }; export const getLogsStream = (pretty) => { if (_cache.logsStream) { return _cache.logsStream; } _cache.logsStream = new LogsStream(pretty ? 'basic' : false); return _cache.logsStream; }; export const getHttpLogsStream = (pretty) => { if (_cache.httpLogsStream) { return _cache.httpLogsStream; } _cache.httpLogsStream = new LogsStream(pretty ? 'http' : false); return _cache.httpLogsStream; }; export const getLoggerLevelValue = (level) => { return pino.levels.values[level] || pino.levels.values['info']; }; export const createLogger = () => { const env = useEnv(); const pinoOptions = { level: env['LOG_LEVEL'] || 'info', redact: { paths: ['req.headers.authorization', 'req.headers.cookie'], censor: REDACTED_TEXT, }, }; const loggerEnvConfig = getConfigFromEnv('LOGGER_', { omitPrefix: 'LOGGER_HTTP' }); // Expose custom log levels into formatter function if (loggerEnvConfig['levels']) { const customLogLevels = {}; for (const el of toArray(loggerEnvConfig['levels'])) { const key_val = el.split(':'); customLogLevels[key_val[0].trim()] = key_val[1].trim(); } pinoOptions.formatters = { level(label, number) { return { severity: customLogLevels[label] || 'info', level: number, }; }, }; delete loggerEnvConfig['levels']; } const mergedOptions = merge(pinoOptions, loggerEnvConfig); const streams = []; // Console Logs if (env['LOG_STYLE'] !== 'raw') { streams.push({ level: mergedOptions.level, stream: pinoPretty({ ignore: 'hostname,pid', sync: true, }), }); } else { streams.push({ level: mergedOptions.level, stream: process.stdout }); } // WebSocket Logs if (toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) { const wsLevel = env['WEBSOCKETS_LOGS_LEVEL'] || 'info'; if (getLoggerLevelValue(wsLevel) < getLoggerLevelValue(mergedOptions.level)) { mergedOptions.level = wsLevel; } streams.push({ level: wsLevel, stream: getLogsStream(env['WEBSOCKETS_LOGS_STYLE'] !== 'raw'), }); } return pino(mergedOptions, pino.multistream(streams)); }; export const createExpressLogger = () => { const env = useEnv(); const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', { omitPrefix: 'LOGGER_HTTP_LOGGER' }); const loggerEnvConfig = getConfigFromEnv('LOGGER_', { omitPrefix: 'LOGGER_HTTP' }); const httpLoggerOptions = { level: env['LOG_LEVEL'] || 'info', redact: { paths: ['req.headers.authorization', 'req.headers.cookie'], censor: REDACTED_TEXT, }, }; if (env['LOG_STYLE'] === 'raw' || toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) { httpLoggerOptions.redact = { paths: ['req.headers.authorization', 'req.headers.cookie', 'res.headers', 'req.query.access_token'], censor: (value, pathParts) => { const path = pathParts.join('.'); if (path === 'res.headers') { if ('set-cookie' in value) { value['set-cookie'] = REDACTED_TEXT; } return value; } return REDACTED_TEXT; }, }; } // Expose custom log levels into formatter function if (loggerEnvConfig['levels']) { const customLogLevels = {}; for (const el of toArray(loggerEnvConfig['levels'])) { const key_val = el.split(':'); customLogLevels[key_val[0].trim()] = key_val[1].trim(); } httpLoggerOptions.formatters = { level(label, number) { return { severity: customLogLevels[label] || 'info', level: number, }; }, }; delete loggerEnvConfig['levels']; } if (env['LOG_HTTP_IGNORE_PATHS']) { const ignorePathsSet = new Set(env['LOG_HTTP_IGNORE_PATHS']); httpLoggerEnvConfig['autoLogging'] = { ignore: (req) => { if (!req.url) return false; const { pathname } = new URL(req.url, 'http://example.com/'); return ignorePathsSet.has(pathname); }, }; } const mergedHttpOptions = merge(httpLoggerOptions, loggerEnvConfig); const streams = []; if (env['LOG_STYLE'] !== 'raw') { const pinoHttpPretty = httpPrintFactory({ all: true, translateTime: 'SYS:HH:MM:ss', relativeUrl: true, prettyOptions: { ignore: 'hostname,pid', sync: true, }, }); streams.push({ level: mergedHttpOptions.level, stream: pinoHttpPretty(process.stdout) }); } else { streams.push({ level: mergedHttpOptions.level, stream: process.stdout }); } // WebSocket Logs if (toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) { const wsLevel = env['WEBSOCKETS_LOGS_LEVEL'] || 'info'; if (getLoggerLevelValue(wsLevel) < getLoggerLevelValue(mergedHttpOptions.level)) { mergedHttpOptions.level = wsLevel; } streams.push({ level: wsLevel, stream: getHttpLogsStream(env['WEBSOCKETS_LOGS_STYLE'] !== 'raw'), }); } return pinoHttp({ logger: pino(mergedHttpOptions, pino.multistream(streams)), ...httpLoggerEnvConfig, serializers: { req(request) { const output = stdSerializers.req(request); output.url = redactQuery(output.url); return output; }, }, }); };