1
import { useEnv } from '@directus/env';
2
import { REDACTED_TEXT, toArray } from '@directus/utils';
3
import type { Request, RequestHandler } from 'express';
4
import { merge } from 'lodash-es';
5
import { URL } from 'node:url';
6
import { pino, type Logger, type LoggerOptions } from 'pino';
7
import { pinoHttp, stdSerializers, type AutoLoggingOptions } from 'pino-http';
8
import { getConfigFromEnv } from './utils/get-config-from-env.js';
11
logger: Logger<never> | undefined;
12
} = { logger: undefined };
14
export const useLogger = () => {
19
_cache.logger = createLogger();
24
export const createLogger = () => {
27
const pinoOptions: LoggerOptions = {
28
level: (env['LOG_LEVEL'] as string) || 'info',
30
paths: ['req.headers.authorization', 'req.headers.cookie'],
31
censor: REDACTED_TEXT,
35
if (env['LOG_STYLE'] !== 'raw') {
36
pinoOptions.transport = {
37
target: 'pino-pretty',
39
ignore: 'hostname,pid',
45
const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
47
// Expose custom log levels into formatter function
48
if (loggerEnvConfig['levels']) {
49
const customLogLevels: { [key: string]: string } = {};
51
for (const el of toArray(loggerEnvConfig['levels'])) {
52
const key_val = el.split(':');
53
customLogLevels[key_val[0].trim()] = key_val[1].trim();
56
pinoOptions.formatters = {
57
level(label: string, number: any) {
59
severity: customLogLevels[label] || 'info',
65
delete loggerEnvConfig['levels'];
68
return pino(merge(pinoOptions, loggerEnvConfig));
71
export const createExpressLogger = () => {
74
const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', ['LOGGER_HTTP_LOGGER']);
75
const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
77
const httpLoggerOptions: LoggerOptions = {
78
level: (env['LOG_LEVEL'] as string) || 'info',
80
paths: ['req.headers.authorization', 'req.headers.cookie'],
81
censor: REDACTED_TEXT,
85
if (env['LOG_STYLE'] !== 'raw') {
86
httpLoggerOptions.transport = {
87
target: 'pino-http-print',
90
translateTime: 'SYS:HH:MM:ss',
93
ignore: 'hostname,pid',
100
if (env['LOG_STYLE'] === 'raw') {
101
httpLoggerOptions.redact = {
102
paths: ['req.headers.authorization', 'req.headers.cookie', 'res.headers'],
103
censor: (value, pathParts) => {
104
const path = pathParts.join('.');
106
if (path === 'res.headers') {
107
if ('set-cookie' in value) {
108
value['set-cookie'] = REDACTED_TEXT;
114
return REDACTED_TEXT;
119
// Expose custom log levels into formatter function
120
if (loggerEnvConfig['levels']) {
121
const customLogLevels: { [key: string]: string } = {};
123
for (const el of toArray(loggerEnvConfig['levels'])) {
124
const key_val = el.split(':');
125
customLogLevels[key_val[0].trim()] = key_val[1].trim();
128
httpLoggerOptions.formatters = {
129
level(label: string, number: any) {
131
severity: customLogLevels[label] || 'info',
137
delete loggerEnvConfig['levels'];
140
if (env['LOG_HTTP_IGNORE_PATHS']) {
141
const ignorePathsSet = new Set(env['LOG_HTTP_IGNORE_PATHS'] as string);
143
httpLoggerEnvConfig['autoLogging'] = {
145
if (!req.url) return false;
146
const { pathname } = new URL(req.url, 'http://example.com/');
147
return ignorePathsSet.has(pathname);
149
} as AutoLoggingOptions;
153
logger: pino(merge(httpLoggerOptions, loggerEnvConfig)),
154
...httpLoggerEnvConfig,
156
req(request: Request) {
157
const output = stdSerializers.req(request);
158
output.url = redactQuery(output.url);
162
}) as RequestHandler;
165
function redactQuery(originalPath: string) {
166
const url = new URL(originalPath, 'http://example.com/');
168
if (url.searchParams.has('access_token')) {
169
url.searchParams.set('access_token', REDACTED_TEXT);
172
return url.pathname + url.search;