From fdb2e42b2935132d4688b634121ffffc5b2ef8de Mon Sep 17 00:00:00 2001 From: daedalus <44623501+ComfortablyCoding@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:31:42 -0500 Subject: [PATCH] Add ability to ignore specific paths from the HTTP logger (#20368) * add changeset * Add the ability to ignore specific paths from HTTP logger * fix linting * Cast env, clean-up, add test * Use dedicated env config name * Add docs * Clean-up --------- Co-authored-by: Pascal Jufer --- .changeset/sixty-toys-nail.md | 5 +++ api/src/env.ts | 3 ++ api/src/logger.test.ts | 60 +++++++++++++++++++++++++++++- api/src/logger.ts | 21 ++++++++--- docs/self-hosted/config-options.md | 1 + 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 .changeset/sixty-toys-nail.md diff --git a/.changeset/sixty-toys-nail.md b/.changeset/sixty-toys-nail.md new file mode 100644 index 0000000000..3bef3adab4 --- /dev/null +++ b/.changeset/sixty-toys-nail.md @@ -0,0 +1,5 @@ +--- +'@directus/api': minor +--- + +Added the ability to ignore specific paths from HTTP logger diff --git a/api/src/env.ts b/api/src/env.ts index 28eb66b740..a1d608f061 100644 --- a/api/src/env.ts +++ b/api/src/env.ts @@ -26,6 +26,7 @@ const allowedEnvironmentVars = [ 'PUBLIC_URL', 'LOG_LEVEL', 'LOG_STYLE', + 'LOG_HTTP_IGNORE_PATHS', 'MAX_PAYLOAD_SIZE', 'ROOT_REDIRECT', 'SERVE_APP', @@ -370,6 +371,8 @@ const typeMap: Record = { MAX_BATCH_MUTATION: 'number', SERVER_SHUTDOWN_TIMEOUT: 'number', + + LOG_HTTP_IGNORE_PATHS: 'array', }; let env: Record = { diff --git a/api/src/logger.test.ts b/api/src/logger.test.ts index 7f0c47597c..7a609ecfcf 100644 --- a/api/src/logger.test.ts +++ b/api/src/logger.test.ts @@ -1,6 +1,9 @@ import { REDACTED_TEXT } from '@directus/utils'; +import http from 'node:http'; +import type { AddressInfo } from 'node:net'; import { Writable } from 'node:stream'; import { pino } from 'pino'; +import { pinoHttp, type HttpLogger } from 'pino-http'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { doMockEnv } from './__utils__/mock-env.js'; @@ -15,7 +18,7 @@ const MOCK_ENV = { LOG_STYLE: 'raw', }; -doMockEnv({ env: MOCK_ENV }); +const { setEnv } = doMockEnv({ env: MOCK_ENV }); const { httpLoggerOptions } = await import('./logger.js'); @@ -145,3 +148,58 @@ describe('res.headers', () => { }); }); }); + +describe('ignored paths', () => { + afterEach(() => { + vi.resetModules(); + }); + + const doRequest = (logger: HttpLogger) => + new Promise((resolve) => { + const server = http.createServer((req, res) => { + logger(req, res); + res.end(); + }); + + server.listen(0, '127.0.0.1', () => { + const address = server.address() as AddressInfo; + const path = '/server/ping'; + + http.get('http://' + address.address + ':' + address.port + path, () => { + server.close(resolve); + }); + }); + }); + + test('should log request with no ignored paths specified', async () => { + const { httpLoggerEnvConfig } = await import('./logger.js'); + + const logger = pinoHttp({ + logger: pino(httpLoggerOptions, stream), + ...httpLoggerEnvConfig, + }); + + await doRequest(logger); + + expect(logOutput.mock.calls[0][0]).toMatchObject({ + req: { + url: '/server/ping', + }, + }); + }); + + test('should not log request when it matches ignored path', async () => { + setEnv({ LOG_HTTP_IGNORE_PATHS: '/server/ping' }); + + const { httpLoggerEnvConfig } = await import('./logger.js'); + + const logger = pinoHttp({ + logger: pino(httpLoggerOptions, stream), + ...httpLoggerEnvConfig, + }); + + await doRequest(logger); + + expect(logOutput).not.toHaveBeenCalled(); + }); +}); diff --git a/api/src/logger.ts b/api/src/logger.ts index 856d04d123..0f3c2df14b 100644 --- a/api/src/logger.ts +++ b/api/src/logger.ts @@ -1,10 +1,9 @@ import { REDACTED_TEXT, toArray } from '@directus/utils'; import type { Request, RequestHandler } from 'express'; import { merge } from 'lodash-es'; -import type { LoggerOptions } from 'pino'; -import { pino } from 'pino'; -import { pinoHttp, stdSerializers } from 'pino-http'; -import { URL } from 'url'; +import { URL } from 'node:url'; +import { pino, type LoggerOptions } from 'pino'; +import { pinoHttp, stdSerializers, type AutoLoggingOptions } from 'pino-http'; import env from './env.js'; import { getConfigFromEnv } from './utils/get-config-from-env.js'; @@ -100,7 +99,19 @@ if (loggerEnvConfig['levels']) { const logger = pino(merge(pinoOptions, loggerEnvConfig)); -const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', ['LOGGER_HTTP_LOGGER']); +export const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', ['LOGGER_HTTP_LOGGER']); + +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); + }, + } as AutoLoggingOptions; +} export const expressLogger = pinoHttp({ logger: pino(merge(httpLoggerOptions, loggerEnvConfig)), diff --git a/docs/self-hosted/config-options.md b/docs/self-hosted/config-options.md index 2ec1a7c081..8a7aba1e8d 100644 --- a/docs/self-hosted/config-options.md +++ b/docs/self-hosted/config-options.md @@ -235,6 +235,7 @@ prefixing the value with `{type}:`. The following types are available: | `PUBLIC_URL`[1] | URL where your API can be reached on the web. | `/` | | `LOG_LEVEL` | What level of detail to log. One of `fatal`, `error`, `warn`, `info`, `debug`, `trace` or `silent`. | `info` | | `LOG_STYLE` | Render the logs human readable (pretty) or as JSON. One of `pretty`, `raw`. | `pretty` | +| `LOG_HTTP_IGNORE_PATHS` | List of HTTP request paths which should not appear in the log, for example `/server/ping`. | -- | | `MAX_PAYLOAD_SIZE` | Controls the maximum request body size. Accepts number of bytes, or human readable string. | `1mb` | | `ROOT_REDIRECT` | Redirect the root of the application `/` to a specific route. Accepts a relative path, absolute URL, or `false` to disable. | `./admin` | | `SERVE_APP` | Whether or not to serve the Admin application | `true` |