diff --git a/.env.example b/.env.example index 4a74a6b4..6d537708 100644 --- a/.env.example +++ b/.env.example @@ -46,4 +46,10 @@ DRAWIO_URL= DISABLE_TELEMETRY=false # Enable debug logging in production (default: false) -DEBUG_MODE=false \ No newline at end of file +DEBUG_MODE=false + +# Log database queries +DEBUG_DB=false + +# Log http requests +LOG_HTTP=false diff --git a/apps/server/package.json b/apps/server/package.json index 12d801a0..1bc18921 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -79,6 +79,7 @@ "mime-types": "^2.1.35", "nanoid": "3.3.11", "nestjs-kysely": "^1.2.0", + "nestjs-pino": "^4.5.0", "nodemailer": "^7.0.12", "openid-client": "^5.7.1", "otpauth": "^9.4.1", @@ -89,6 +90,8 @@ "pg": "^8.16.3", "pg-tsquery": "^8.4.2", "pgvector": "^0.2.1", + "pino-http": "^11.0.0", + "pino-pretty": "^13.1.3", "postmark": "^4.0.5", "react": "^18.3.1", "reflect-metadata": "^0.2.2", diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 56691444..8036b849 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -18,6 +18,7 @@ import { SecurityModule } from './integrations/security/security.module'; import { TelemetryModule } from './integrations/telemetry/telemetry.module'; import { RedisModule } from '@nestjs-labs/nestjs-ioredis'; import { RedisConfigService } from './integrations/redis/redis-config.service'; +import { LoggerModule } from './common/logger/logger.module'; const enterpriseModules = []; try { @@ -35,6 +36,7 @@ try { @Module({ imports: [ + LoggerModule, CoreModule, DatabaseModule, EnvironmentModule, diff --git a/apps/server/src/collaboration/server/collab-app.module.ts b/apps/server/src/collaboration/server/collab-app.module.ts index 08a2f688..eb6b57fa 100644 --- a/apps/server/src/collaboration/server/collab-app.module.ts +++ b/apps/server/src/collaboration/server/collab-app.module.ts @@ -8,9 +8,11 @@ import { QueueModule } from '../../integrations/queue/queue.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { HealthModule } from '../../integrations/health/health.module'; import { CollaborationController } from './collaboration.controller'; +import { LoggerModule } from '../../common/logger/logger.module'; @Module({ imports: [ + LoggerModule, DatabaseModule, EnvironmentModule, CollaborationModule, diff --git a/apps/server/src/collaboration/server/collab-main.ts b/apps/server/src/collaboration/server/collab-main.ts index d71da428..1a10167f 100644 --- a/apps/server/src/collaboration/server/collab-main.ts +++ b/apps/server/src/collaboration/server/collab-main.ts @@ -5,8 +5,8 @@ import { NestFastifyApplication, } from '@nestjs/platform-fastify'; import { TransformHttpResponseInterceptor } from '../../common/interceptors/http-response.interceptor'; -import { InternalLogFilter } from '../../common/logger/internal-log-filter'; import { Logger } from '@nestjs/common'; +import { Logger as PinoLogger } from 'nestjs-pino'; async function bootstrap() { const app = await NestFactory.create( @@ -17,10 +17,12 @@ async function bootstrap() { maxParamLength: 500, }), { - logger: new InternalLogFilter(), + bufferLogs: true, }, ); + app.useLogger(app.get(PinoLogger)); + app.setGlobalPrefix('api', { exclude: ['/'] }); app.enableCors(); diff --git a/apps/server/src/common/logger/logger.module.ts b/apps/server/src/common/logger/logger.module.ts new file mode 100644 index 00000000..327605a4 --- /dev/null +++ b/apps/server/src/common/logger/logger.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { LoggerModule as PinoLoggerModule } from 'nestjs-pino'; +import { createPinoConfig } from './pino.config'; + +@Module({ + imports: [PinoLoggerModule.forRoot(createPinoConfig())], + exports: [PinoLoggerModule], +}) +export class LoggerModule {} diff --git a/apps/server/src/common/logger/pino.config.ts b/apps/server/src/common/logger/pino.config.ts new file mode 100644 index 00000000..9d9a14f7 --- /dev/null +++ b/apps/server/src/common/logger/pino.config.ts @@ -0,0 +1,77 @@ +import { Params } from 'nestjs-pino'; +import { stdTimeFunctions } from 'pino'; + +const CONTEXTS_TO_IGNORE = [ + 'InstanceLoader', + 'RoutesResolver', + 'RouterExplorer', + 'WebSocketsController', +]; + +export function createPinoConfig(): Params { + const isProduction = process.env.NODE_ENV === 'production'; + const isDebugMode = process.env.DEBUG_MODE === 'true'; + const logHttp = process.env.LOG_HTTP === 'true'; + + const level = isProduction && !isDebugMode ? 'info' : 'debug'; + + return { + pinoHttp: { + level, + timestamp: stdTimeFunctions.isoTime, + transport: !isProduction + ? { + target: 'pino-pretty', + options: { + colorize: true, + singleLine: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', + }, + } + : undefined, + formatters: { + level: (label) => ({ level: label }), + log: (object: Record) => { + if (isProduction && !isDebugMode) { + const context = object['context'] as string | undefined; + if (context && CONTEXTS_TO_IGNORE.includes(context)) { + return { filtered: true }; + } + } + return object; + }, + }, + serializers: { + req: (req) => { + const forwardedFor = req.headers?.['x-forwarded-for']; + const ip = + req.headers?.['cf-connecting-ip'] || + (typeof forwardedFor === 'string' ? forwardedFor.split(',')[0]?.trim() : undefined) || + req.remoteAddress; + + return { + method: req.method, + url: req.url, + ip, + userAgent: req.headers?.['user-agent'], + }; + }, + res: (res) => ({ + statusCode: res.statusCode, + }), + }, + customLogLevel: (_req, res, err) => { + if (res.statusCode >= 500 || err) return 'error'; + if (res.statusCode >= 400) return 'warn'; + return 'info'; + }, + autoLogging: logHttp + ? { + ignore: (req) => + req.url === '/api/health' || req.url === '/api/health/live', + } + : false, + }, + }; +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 79340d6e..406921a0 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -5,9 +5,9 @@ import { NestFastifyApplication, } from '@nestjs/platform-fastify'; import { Logger, NotFoundException, ValidationPipe } from '@nestjs/common'; +import { Logger as PinoLogger } from 'nestjs-pino'; import { TransformHttpResponseInterceptor } from './common/interceptors/http-response.interceptor'; import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter'; -import { InternalLogFilter } from './common/logger/internal-log-filter'; import fastifyMultipart from '@fastify/multipart'; import fastifyCookie from '@fastify/cookie'; @@ -24,10 +24,12 @@ async function bootstrap() { }), { rawBody: true, - logger: new InternalLogFilter(), + bufferLogs: true, }, ); + app.useLogger(app.get(PinoLogger)); + app.setGlobalPrefix('api', { exclude: ['robots.txt', 'share/:shareId/p/:pageSlug'], }); @@ -99,9 +101,7 @@ async function bootstrap() { const port = process.env.PORT || 3000; await app.listen(port, '0.0.0.0', () => { - logger.log( - `Listening on http://127.0.0.1:${port} / ${process.env.APP_URL}`, - ); + logger.log(`Listening on http://127.0.0.1:${port} / ${process.env.APP_URL}`); }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 882fa54a..7c08b66f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -581,6 +581,9 @@ importers: nestjs-kysely: specifier: ^1.2.0 version: 1.2.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(kysely@0.28.2)(reflect-metadata@0.2.2) + nestjs-pino: + specifier: ^4.5.0 + version: 4.5.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) nodemailer: specifier: ^7.0.12 version: 7.0.12 @@ -611,6 +614,12 @@ importers: pgvector: specifier: ^0.2.1 version: 0.2.1 + pino-http: + specifier: ^11.0.0 + version: 11.0.0 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 postmark: specifier: ^4.0.5 version: 4.0.5 @@ -5615,6 +5624,9 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + columnify@1.6.0: resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==} engines: {node: '>=8.0.0'} @@ -5975,6 +5987,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -6483,6 +6498,9 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-copy@4.0.2: + resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} + fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -6832,6 +6850,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hexoid@1.0.0: resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} engines: {node: '>=8'} @@ -7366,6 +7387,10 @@ packages: react: optional: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-beautify@1.15.1: resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} engines: {node: '>=14'} @@ -8033,9 +8058,19 @@ packages: kysely: 0.x reflect-metadata: ^0.1.13 || ^0.2.2 + nestjs-pino@4.5.0: + resolution: {integrity: sha512-e54ChJMACSGF8gPYaHsuD07RW7l/OVoV6aI8Hqhpp0ZQ4WA8QY3eewL42JX7Z1U6rV7byNU7bGBV9l6d9V6PDQ==} + engines: {node: '>= 14'} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + pino: ^7.5.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + pino-http: ^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + next@14.2.10: resolution: {integrity: sha512-sDDExXnh33cY3RkS9JuFEKaS4HmlWmDKP1VJioucCG6z5KuA008DPsDZOzi8UfqEk3Ii+2NCQSJrfbEWtZZfww==} engines: {node: '>=18.17.0'} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -8476,6 +8511,16 @@ packages: pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-http@11.0.0: + resolution: {integrity: sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + pino-std-serializers@7.0.0: resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} @@ -8733,6 +8778,9 @@ packages: prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -9417,6 +9465,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + stripe@17.5.0: resolution: {integrity: sha512-kcyeAkDFjGsVl17FqnG7q/+xIjt0ZjOo9Dm+q8deAvs2Xe4iAHrhxyoP4etUVFc+/LZJANjIPVR+ZOnt9hr/Ug==} engines: {node: '>=12.*'} @@ -16277,6 +16329,8 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + colorette@2.0.20: {} + columnify@1.6.0: dependencies: strip-ansi: 6.0.1 @@ -16684,6 +16738,8 @@ snapshots: date-fns@4.1.0: {} + dateformat@4.6.3: {} + dayjs@1.11.13: {} dayjs@1.11.19: {} @@ -17320,6 +17376,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-copy@4.0.2: {} + fast-decode-uri-component@1.0.1: {} fast-deep-equal@2.0.1: {} @@ -17704,6 +17762,8 @@ snapshots: dependencies: function-bind: 1.1.2 + help-me@5.0.0: {} + hexoid@1.0.0: {} highlight.js@11.11.1: {} @@ -18472,6 +18532,8 @@ snapshots: '@types/react': 18.3.12 react: 18.3.1 + joycon@3.1.1: {} + js-beautify@1.15.1: dependencies: config-chain: 1.1.13 @@ -19229,6 +19291,13 @@ snapshots: kysely: 0.28.2 reflect-metadata: 0.2.2 + nestjs-pino@4.5.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): + dependencies: + '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + pino: 10.1.0 + pino-http: 11.0.0 + rxjs: 7.8.2 + next@14.2.10(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.51.0): dependencies: '@next/env': 14.2.10 @@ -19702,6 +19771,33 @@ snapshots: dependencies: split2: 4.2.0 + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-http@11.0.0: + dependencies: + get-caller-file: 2.0.5 + pino: 10.1.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.3 + secure-json-parse: 4.0.0 + sonic-boom: 4.0.1 + strip-json-comments: 5.0.3 + pino-std-serializers@7.0.0: {} pino@10.1.0: @@ -19993,6 +20089,11 @@ snapshots: prr@1.0.1: optional: true + pump@3.0.3: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -20783,6 +20884,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + stripe@17.5.0: dependencies: '@types/node': 22.19.1