@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
186 lines (185 loc) • 6.75 kB
JavaScript
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;
},
},
});
};