From 2fa4438487a937eaf98d71883ade8da191ec7fc7 Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Sun, 20 Apr 2025 23:47:30 +0900 Subject: [PATCH 1/6] add: docker-compose redis MQ --- .docker/redis/redis-docker-compose.dev.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .docker/redis/redis-docker-compose.dev.yml diff --git a/.docker/redis/redis-docker-compose.dev.yml b/.docker/redis/redis-docker-compose.dev.yml new file mode 100644 index 0000000..12080b2 --- /dev/null +++ b/.docker/redis/redis-docker-compose.dev.yml @@ -0,0 +1,12 @@ +version: '3.8' + +services: + redis-MQ: + image: redis:7.2-alpine + container_name: redis + ports: + - '6379:6379' + volumes: + - ./redis.conf:/usr/local/etc/redis/redis.conf + - ./redis-data:/data + command: ['redis-server', '/usr/local/etc/redis/redis.conf'] From 9fe4ad1cf131cd14564af9db90da8e7d48b12a5e Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Mon, 21 Apr 2025 01:37:56 +0900 Subject: [PATCH 2/6] Add below - logger (need to checks the logs is located in proper directory) - exception filter - logging interceptor - nestjs cache manager setting - temporarily implement cache interceptor, need to customize later - nestjs docker compose, need to require password - add redis cache for get course, lecture, session/info Todo - need to implement local cache synchronization --- .docker/redis/redis-mq-docker-compose.dev.yml | 12 ++ .gitignore | 4 +- apps/server/src/app.module.ts | 16 ++- apps/server/src/bootstrap/bootstrap.ts | 29 +---- .../interceptor/http.cache.interceptor.ts | 43 +++++++ .../src/modules/auth/auth.controller.ts | 7 +- .../src/modules/courses/courses.controller.ts | 13 ++- .../modules/lectures/lectures.controller.ts | 11 +- apps/server/src/settings.ts | 5 + deploy/scholar-sync/docker/Dockerfile.server | 2 + deploy/server/docker/Dockerfile.server | 2 + deploy/server/docker/docker-compose.dev.yml | 1 + deploy/server/docker/docker-compose.local.yml | 1 + deploy/server/docker/docker-compose.prod.yml | 1 + libs/common/src/enum/time.ts | 8 ++ libs/common/src/exception/exception.filter.ts | 44 +++++++ libs/common/src/logger/logger.ts | 101 ++++++++++++++++ libs/common/src/logger/logging.interceptor.ts | 34 ++++++ package.json | 3 + yarn.lock | 110 +++++++++++++++++- 20 files changed, 411 insertions(+), 36 deletions(-) create mode 100644 .docker/redis/redis-mq-docker-compose.dev.yml create mode 100644 apps/server/src/common/interceptor/http.cache.interceptor.ts create mode 100644 libs/common/src/enum/time.ts create mode 100644 libs/common/src/exception/exception.filter.ts create mode 100644 libs/common/src/logger/logger.ts create mode 100644 libs/common/src/logger/logging.interceptor.ts diff --git a/.docker/redis/redis-mq-docker-compose.dev.yml b/.docker/redis/redis-mq-docker-compose.dev.yml new file mode 100644 index 0000000..20dd8b9 --- /dev/null +++ b/.docker/redis/redis-mq-docker-compose.dev.yml @@ -0,0 +1,12 @@ +version: '3.8' + +services: + redis-MQ: + image: redis:7.2-alpine + container_name: redis-mq + ports: + - '6379:6379' + volumes: + - ./redis.conf:/usr/local/etc/redis/redis.conf + - ./redis-data:/data + command: ['redis-server', '/usr/local/etc/redis/redis.conf'] diff --git a/.gitignore b/.gitignore index f47b2c7..cfc0f48 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,6 @@ lerna-debug.log* apps/server/volumes volumes -.clinic \ No newline at end of file + +# redis +.docker/redis/redis-data \ No newline at end of file diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 4bb2f84..b268948 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -1,10 +1,14 @@ +import { createKeyv } from '@keyv/redis' +import { CacheModule } from '@nestjs/cache-manager' import { Module } from '@nestjs/common' -import { APP_GUARD } from '@nestjs/core' +import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core' import { JwtService } from '@nestjs/jwt' import { ClsPluginTransactional } from '@nestjs-cls/transactional' import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma' import { ClsModule } from 'nestjs-cls' +import { LoggingInterceptor } from '@otl/common/logger/logging.interceptor' + import { PrismaModule } from '@otl/prisma-client/prisma.module' import { PrismaService } from '@otl/prisma-client/prisma.service' @@ -65,6 +69,12 @@ import settings from './settings' }), ], }), + CacheModule.registerAsync({ + useFactory: async () => ({ + stores: [createKeyv(settings().getRedisConfig().url)], + }), + isGlobal: true, + }), ], controllers: [AppController], providers: [ @@ -77,6 +87,10 @@ import settings from './settings' }, inject: [AuthConfig], }, + { + provide: APP_INTERCEPTOR, + useClass: LoggingInterceptor, + }, JwtCookieGuard, MockAuthGuard, AppService, diff --git a/apps/server/src/bootstrap/bootstrap.ts b/apps/server/src/bootstrap/bootstrap.ts index c57b4fc..5a1f956 100644 --- a/apps/server/src/bootstrap/bootstrap.ts +++ b/apps/server/src/bootstrap/bootstrap.ts @@ -1,15 +1,16 @@ -import { ValidationPipe, VersioningType } from '@nestjs/common' +import { HttpException, ValidationPipe, VersioningType } from '@nestjs/common' import { NestFactory } from '@nestjs/core' import cookieParser from 'cookie-parser' import csrf from 'csurf' import { json } from 'express' import session from 'express-session' import fs from 'fs' -import morgan from 'morgan' import * as v8 from 'node:v8' import { join } from 'path' import * as swaggerUi from 'swagger-ui-express' +import { HttpExceptionFilter, UnexpectedExceptionFilter } from '@otl/common/exception/exception.filter' + import { AppModule } from '../app.module' import settings from '../settings' @@ -43,30 +44,7 @@ async function bootstrap() { ignoreMethods: ['GET', 'HEAD', 'OPTIONS', 'DELETE', 'PATCH', 'PUT', 'POST'], }), ) - // Logs requests - app.use( - morgan(':method :url OS/:req[client-os] Ver/:req[client-api-version]', { - // https://github.com/expressjs/morgan#immediate - immediate: true, - stream: { - write: (message) => { - console.info(message.trim()) - }, - }, - }), - ) - // Logs responses - // app.use( - // morgan(':method :url :status :res[content-length] :response-time ms', { - // stream: { - // write: (message) => { - // // console.log(formatMemoryUsage()) - // console.info(message.trim()); - // }, - // }, - // }), - // ); if (process.env.NODE_ENV !== 'prod') { const swaggerJsonPath = join(__dirname, '..', '..', 'docs', 'swagger.json') const swaggerDocument = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf-8')) @@ -83,6 +61,7 @@ async function bootstrap() { app.use('/api/sync', json({ limit: '50mb' })) app.use(json({ limit: '100kb' })) + app.useGlobalFilters(new UnexpectedExceptionFilter(), new HttpExceptionFilter()) console.log(v8.getHeapStatistics().heap_size_limit / 1024 / 1024) app.enableShutdownHooks() diff --git a/apps/server/src/common/interceptor/http.cache.interceptor.ts b/apps/server/src/common/interceptor/http.cache.interceptor.ts new file mode 100644 index 0000000..45ac071 --- /dev/null +++ b/apps/server/src/common/interceptor/http.cache.interceptor.ts @@ -0,0 +1,43 @@ +import { CacheInterceptor } from '@nestjs/cache-manager' +import { ExecutionContext, Injectable } from '@nestjs/common' + +/** + * @Todo + * 예시로 일단 인터넷에 있는거 긁어온거임. + * 적절히 구현 변형 필요 + */ + +const excludePaths = [ + // 캐시가 적용되지 않아야 할 path 목록 () + /(\/sample2\/)(.*)/i, +] + +@Injectable() +export class HttpCacheInterceptor extends CacheInterceptor { + trackBy(context: ExecutionContext): string | undefined { + const request = context.switchToHttp().getRequest() + const { query } = request + const { httpAdapter } = this.httpAdapterHost + + // Get Request가 아닌 request 처리 + const isGetRequest = httpAdapter.getRequestMethod(request) === 'GET' + if (!isGetRequest) { + return undefined + } + + // noCache=true query parameter 처리 + const noCache = query.noCache && query.noCache.toLowerCase() === 'true' + if (noCache) { + return undefined + } + + // 설정된 캐시 예외 path 처리 + const requestPath = httpAdapter.getRequestUrl(request).split('?')[0] + const exclude = excludePaths.find((path) => path.test(requestPath)) + if (exclude) { + return undefined + } + + return httpAdapter.getRequestUrl(request) + } +} diff --git a/apps/server/src/modules/auth/auth.controller.ts b/apps/server/src/modules/auth/auth.controller.ts index 56e3dce..aa7129d 100644 --- a/apps/server/src/modules/auth/auth.controller.ts +++ b/apps/server/src/modules/auth/auth.controller.ts @@ -1,5 +1,6 @@ +import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager' import { - Controller, Get, Query, Req, Res, Session, + Controller, Get, Query, Req, Res, Session, UseInterceptors, } from '@nestjs/common' import { GetUser } from '@otl/server-nest/common/decorators/get-user.decorator' import { Public } from '@otl/server-nest/common/decorators/skip-auth.decorator' @@ -7,6 +8,8 @@ import { IAuth, IUser } from '@otl/server-nest/common/interfaces' import settings from '@otl/server-nest/settings' import { session_userprofile } from '@prisma/client' +import { RedisTTL } from '@otl/common/enum/time' + import { ESSOUser } from '@otl/prisma-client/entities' import { UserService } from '../user/user.service' @@ -74,6 +77,8 @@ export class AuthController { response.redirect(next_url) } + @CacheTTL(RedisTTL.DAY) + @UseInterceptors(CacheInterceptor) @Get('info') async getUserProfile(@GetUser() user: session_userprofile): Promise { /* diff --git a/apps/server/src/modules/courses/courses.controller.ts b/apps/server/src/modules/courses/courses.controller.ts index 4809408..213dc30 100644 --- a/apps/server/src/modules/courses/courses.controller.ts +++ b/apps/server/src/modules/courses/courses.controller.ts @@ -1,5 +1,6 @@ +import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager' import { - BadRequestException, Controller, Get, Param, Post, Query, + BadRequestException, Controller, Get, Param, Post, Query, UseInterceptors, } from '@nestjs/common' import { GetUser } from '@otl/server-nest/common/decorators/get-user.decorator' import { Public } from '@otl/server-nest/common/decorators/skip-auth.decorator' @@ -7,6 +8,8 @@ import { ICourse, ILecture, IReview } from '@otl/server-nest/common/interfaces' import { CourseIdPipe } from '@otl/server-nest/common/pipe/courseId.pipe' import { session_userprofile } from '@prisma/client' +import { RedisTTL } from '@otl/common/enum/time' + import { CoursesService } from './courses.service' @Controller('api/courses') @@ -14,6 +17,8 @@ export class CourseController { constructor(private readonly coursesService: CoursesService) {} @Public() + @CacheTTL(RedisTTL.HOUR) + @UseInterceptors(CacheInterceptor) @Get() async getCourses( @Query() query: ICourse.Query, @@ -24,12 +29,16 @@ export class CourseController { } @Public() + @CacheTTL(RedisTTL.HOUR) + @UseInterceptors(CacheInterceptor) @Get('autocomplete') async getCourseAutocomplete(@Query() query: ICourse.AutocompleteQueryDto): Promise { return await this.coursesService.getCourseAutocomplete(query) } @Public() + @CacheTTL(RedisTTL.HOUR) + @UseInterceptors(CacheInterceptor) @Get(':id') async getCourseById( @Param('id', CourseIdPipe) id: number, @@ -40,6 +49,8 @@ export class CourseController { } @Public() + @CacheTTL(RedisTTL.HOUR) + @UseInterceptors(CacheInterceptor) @Get(':id/lectures') async getLecturesByCourseId( @Query() query: ICourse.LectureQueryDto, diff --git a/apps/server/src/modules/lectures/lectures.controller.ts b/apps/server/src/modules/lectures/lectures.controller.ts index 4cd88a3..07dd9e9 100644 --- a/apps/server/src/modules/lectures/lectures.controller.ts +++ b/apps/server/src/modules/lectures/lectures.controller.ts @@ -1,11 +1,14 @@ +import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager' import { - Controller, Get, Param, Query, + Controller, Get, Param, Query, UseInterceptors, } from '@nestjs/common' import { GetUser } from '@otl/server-nest/common/decorators/get-user.decorator' import { Public } from '@otl/server-nest/common/decorators/skip-auth.decorator' import { ILecture, IReview } from '@otl/server-nest/common/interfaces' import { session_userprofile } from '@prisma/client' +import { RedisTTL } from '@otl/common/enum/time' + import { LecturesService } from './lectures.service' @Controller('api/lectures') @@ -13,18 +16,24 @@ export class LecturesController { constructor(private readonly LectureService: LecturesService) {} @Public() + @CacheTTL(RedisTTL.HOUR) + @UseInterceptors(CacheInterceptor) @Get() async getLectures(@Query() query: ILecture.QueryDto): Promise { return await this.LectureService.getLectureByFilter(query) } @Public() + @CacheTTL(RedisTTL.HOUR) + @UseInterceptors(CacheInterceptor) @Get('autocomplete') async getLectureAutocomplete(@Query() query: ILecture.AutocompleteQueryDto): Promise { return await this.LectureService.getLectureAutocomplete(query) } @Public() + @CacheTTL(RedisTTL.HOUR) + @UseInterceptors(CacheInterceptor) @Get(':id') async getLectureById(@Param('id') id: number): Promise { return await this.LectureService.getLectureById(id) diff --git a/apps/server/src/settings.ts b/apps/server/src/settings.ts index 79170be..4ab5729 100644 --- a/apps/server/src/settings.ts +++ b/apps/server/src/settings.ts @@ -63,6 +63,10 @@ const getPrismaConfig = (): Prisma.PrismaClientOptions => ({ ], }) +const getRedisConfig = () => ({ + url: process.env.REDIS_URL, +}) + const getReplicatedPrismaConfig = (): Prisma.PrismaClientOptions => ({}) const getAWSConfig = () => ({}) @@ -106,6 +110,7 @@ export default () => ({ ormconfig: () => getPrismaConfig(), ormReplicatedConfig: () => getReplicatedPrismaConfig(), awsconfig: () => getAWSConfig(), + getRedisConfig: () => getRedisConfig(), getJwtConfig: () => getJwtConfig(), getSsoConfig: () => getSsoConfig(), getCorsConfig: () => getCorsConfig(), diff --git a/deploy/scholar-sync/docker/Dockerfile.server b/deploy/scholar-sync/docker/Dockerfile.server index 21e12c2..ea53e13 100644 --- a/deploy/scholar-sync/docker/Dockerfile.server +++ b/deploy/scholar-sync/docker/Dockerfile.server @@ -4,6 +4,8 @@ RUN mkdir -p /var/www/otlplus RUN mkdir -p /var/www/otlplus/libs RUN mkdir -p /var/www/otlplus/apps WORKDIR /var/www/otlplus +RUN mkdir -p /var/www/otlplus/apps/scholar-sync/logs + COPY package.json yarn.lock* package-lock.json* tsconfig.json tsconfig.build.json ./ COPY nest-cli.json ./ diff --git a/deploy/server/docker/Dockerfile.server b/deploy/server/docker/Dockerfile.server index 614005f..a2f4b87 100644 --- a/deploy/server/docker/Dockerfile.server +++ b/deploy/server/docker/Dockerfile.server @@ -4,6 +4,8 @@ RUN mkdir -p /var/www/otlplus-server RUN mkdir -p /var/www/otlplus-server/libs RUN mkdir -p /var/www/otlplus-server/apps WORKDIR /var/www/otlplus-server +RUN mkdir -p /var/www/otlplus-server/apps/server/logs + COPY package.json yarn.lock* package-lock.json* tsconfig.json tsconfig.build.json ./ COPY nest-cli.json ./ diff --git a/deploy/server/docker/docker-compose.dev.yml b/deploy/server/docker/docker-compose.dev.yml index 1f65495..f04e806 100644 --- a/deploy/server/docker/docker-compose.dev.yml +++ b/deploy/server/docker/docker-compose.dev.yml @@ -21,5 +21,6 @@ services: - '8000' volumes: - '/etc/timezone:/etc/timezone:ro' + - './apps/server/logs:/var/www/otlplus-server/apps/server/logs' working_dir: /var/www/otlplus-server command: pm2-runtime start ecosystem.config.js --only @otl/server-nest --node-args="max-old-space-size=40920" diff --git a/deploy/server/docker/docker-compose.local.yml b/deploy/server/docker/docker-compose.local.yml index fe780cc..04348bb 100644 --- a/deploy/server/docker/docker-compose.local.yml +++ b/deploy/server/docker/docker-compose.local.yml @@ -21,5 +21,6 @@ services: - '8000' volumes: - '/etc/timezone:/etc/timezone:ro' + - './apps/server/logs:/var/www/otlplus-server/apps/server/logs' working_dir: /var/www/otlplus-server command: pm2-runtime start ecosystem.config.js --only @otl/server-nest --node-args="max-old-space-size=40920" diff --git a/deploy/server/docker/docker-compose.prod.yml b/deploy/server/docker/docker-compose.prod.yml index c0004e4..be58f53 100644 --- a/deploy/server/docker/docker-compose.prod.yml +++ b/deploy/server/docker/docker-compose.prod.yml @@ -19,5 +19,6 @@ services: - '58000:8000' volumes: - '/etc/timezone:/etc/timezone:ro' + - './apps/server/logs:/var/www/otlplus-server/apps/server/logs' working_dir: /var/www/otlplus-server command: pm2-runtime start ecosystem.config.js --only @otl/server-nest --node-args="max-old-space-size=40920" diff --git a/libs/common/src/enum/time.ts b/libs/common/src/enum/time.ts new file mode 100644 index 0000000..77d6aeb --- /dev/null +++ b/libs/common/src/enum/time.ts @@ -0,0 +1,8 @@ +export const RedisTTL = { + DAY: 3600 * 24 * 1000, + WEEK: 3600 * 24 * 7 * 1000, + HOUR: 3600 * 1000, + HALF_HOUR: 1800 * 1000, + MIN: 60 * 1000, +} as const +export type TTL = (typeof RedisTTL)[keyof typeof RedisTTL] diff --git a/libs/common/src/exception/exception.filter.ts b/libs/common/src/exception/exception.filter.ts new file mode 100644 index 0000000..5b68c1d --- /dev/null +++ b/libs/common/src/exception/exception.filter.ts @@ -0,0 +1,44 @@ +import { + ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, +} from '@nestjs/common' + +import logger from '../logger/logger' + +@Catch() // BaseException을 상속한 exception에 대해서 실행됨. +export class UnexpectedExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const response = ctx.getResponse() + const request = ctx.getRequest() + + const resStatus = HttpStatus.INTERNAL_SERVER_ERROR + logger.error('Unexpected exception', exception) + logger.error(exception) + // + response.status(resStatus).json({ + // todo: exception의 response 형식 결정되면 변경해야함. + statusCode: resStatus, + timestamp: new Date().toISOString(), + path: request.url, + }) + } +} + +@Catch(HttpException) // BaseException을 상속한 exception에 대해서 실행됨. +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: T, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const response = ctx.getResponse() + const request = ctx.getRequest() + + const resStatus = exception.getStatus() + logger.error(exception.getResponse()) + response.status(resStatus).json({ + // todo: exception의 response 형식 결정되면 변경해야함. + message: exception.getResponse(), // test를 위한 코드 + statusCode: resStatus, + timestamp: new Date().toISOString(), + path: request.url, + }) + } +} diff --git a/libs/common/src/logger/logger.ts b/libs/common/src/logger/logger.ts new file mode 100644 index 0000000..ca7d9ff --- /dev/null +++ b/libs/common/src/logger/logger.ts @@ -0,0 +1,101 @@ +import path from 'path' +import winston, { createLogger, format, transports } from 'winston' +import DailyRotateFileTransport from 'winston-daily-rotate-file' + +// 실행 중인 앱의 루트 경로를 기준으로 logs 폴더 경로 설정 +const logDir = path.join(process.cwd(), 'logs') + +const baseFormat = format.combine( + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss(UTCZ)' }), + format.errors({ stack: true }), + format.splat(), + format.json(), +) + +const finalFormat = format.printf( + ({ + level, message, timestamp, stack, + }) => `${timestamp} [${level}]: ${message} ${level === 'error' && stack ? stack : ''}`, +) + +const uncolorizedFormat = format.combine(baseFormat, format.uncolorize(), finalFormat) +const colorizedFormat = format.combine(baseFormat, format.colorize({ all: true }), finalFormat) + +const { NODE_ENV } = process.env +const datePattern = 'YYYY-MM-DD-HH' +const maxSize = 5 * 1024 * 1024 + +// eslint-disable-next-line import/no-mutable-exports +let logger: winston.Logger +if (NODE_ENV === 'prod') { + logger = createLogger({ + level: 'info', + format: uncolorizedFormat, + defaultMeta: { service: 'otlplus' }, + transports: [ + new DailyRotateFileTransport({ + level: 'info', + filename: path.join(logDir, '%DATE%-combined.log'), + datePattern, + maxSize, + }), + new DailyRotateFileTransport({ + level: 'error', + filename: path.join(logDir, '%DATE%-error.log'), + datePattern, + maxSize, + }), + new transports.Console({ level: 'error' }), + ], + exceptionHandlers: [ + new DailyRotateFileTransport({ + filename: path.join(logDir, '%DATE%-unhandled.log'), + datePattern, + maxSize, + }), + new transports.Console(), + ], + }) +} +else if (NODE_ENV === 'dev') { + logger = createLogger({ + level: 'debug', + format: uncolorizedFormat, + defaultMeta: { service: 'otlplus' }, + transports: [ + new DailyRotateFileTransport({ + level: 'info', + filename: path.join(logDir, '%DATE%-combined.log'), + datePattern, + maxSize, + }), + new DailyRotateFileTransport({ + level: 'error', + filename: path.join(logDir, '%DATE%-error.log'), + datePattern, + maxSize, + }), + new transports.Console({ level: 'error' }), + new transports.Console({ level: 'debug', format: colorizedFormat }), + ], + exceptionHandlers: [ + new DailyRotateFileTransport({ + filename: path.join(logDir, '%DATE%-unhandled.log'), + datePattern, + maxSize, + }), + new transports.Console({ format: colorizedFormat }), + ], + }) +} +else { + logger = createLogger({ + level: 'debug', + format: colorizedFormat, + defaultMeta: { service: 'otlplus' }, + transports: [new transports.Console()], + exceptionHandlers: [new transports.Console()], + }) +} + +export default logger diff --git a/libs/common/src/logger/logging.interceptor.ts b/libs/common/src/logger/logging.interceptor.ts new file mode 100644 index 0000000..33feed8 --- /dev/null +++ b/libs/common/src/logger/logging.interceptor.ts @@ -0,0 +1,34 @@ +import { + CallHandler, ExecutionContext, Injectable, NestInterceptor, +} from '@nestjs/common' +import { Response } from 'express' +import { Observable, tap } from 'rxjs' + +import logger from '@otl/common/logger/logger' + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const now = Date.now() + + const request = context.switchToHttp().getRequest() + const response = context.switchToHttp().getResponse() + + const { method } = request + const url = request.originalUrl || request.url + const clientOs = request.headers['client-os'] || '-' + const apiVersion = request.headers['client-api-version'] || '-' + const userId = request?.user?.id ?? 'Anonymous' + + return next.handle().pipe( + tap(() => { + const delay = Date.now() - now + const { statusCode } = response + + const logMessage = `[User#${userId}] ${method} ${url} OS/${clientOs} Ver/${apiVersion} → ${statusCode} (${delay}ms)` + + logger.info(logMessage) + }), + ) + } +} diff --git a/package.json b/package.json index 2844e94..91bdace 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,10 @@ "typescript": "^5.7.2" }, "dependencies": { + "@keyv/redis": "^4.3.4", "@nestjs-cls/transactional": "^2.4.4", "@nestjs-cls/transactional-adapter-prisma": "^1.2.7", + "@nestjs/cache-manager": "^3.0.1", "@nestjs/cli": "^10.4.9", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", @@ -109,6 +111,7 @@ "@types/swagger-ui-express": "^4.1.8", "axios": "^1.7.9", "bcrypt": "^5.1.1", + "cache-manager": "^6.4.2", "canvas": "^3.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", diff --git a/yarn.lock b/yarn.lock index 85e38a5..e4baf93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -956,6 +956,21 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@keyv/redis@^4.3.4": + version "4.3.4" + resolved "https://registry.npmjs.org/@keyv/redis/-/redis-4.3.4.tgz#b6f93c9288f8f7dbcf11683fc285c15745ead782" + integrity sha512-PLWmawfq9McxEvtHa2Uj5WjI7g6qWtv2eOvXvXJ9tkwEV5vLkqA+pFeZ/0pz9xvP20NQiAkGm4521YJ0DhuFiw== + dependencies: + cluster-key-slot "^1.1.2" + redis "^4.7.0" + +"@keyv/serialize@^1.0.3": + version "1.0.3" + resolved "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz#e0fe3710e2a379cb0490cd41e5a5ffa2bab58bf6" + integrity sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g== + dependencies: + buffer "^6.0.3" + "@ljharb/through@^2.3.12": version "2.3.13" resolved "https://registry.yarnpkg.com/@ljharb/through/-/through-2.3.13.tgz#b7e4766e0b65aa82e529be945ab078de79874edc" @@ -998,6 +1013,11 @@ resolved "https://registry.yarnpkg.com/@nestjs-cls/transactional/-/transactional-2.4.5.tgz#5cf17a62e954fb771e67d5d0982f8b6541ad6953" integrity sha512-IECmG5SYIXMEOXOOD+UX51p7efjbEqpnKqMTkm2yh/gYAhrXFJDAmCGFmg59H/TJdtNPlI6Kt/vwojjQehrLIA== +"@nestjs/cache-manager@^3.0.1": + version "3.0.1" + resolved "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.1.tgz#893abd4fb8450d2fe6c6692bd71688c45b2d5128" + integrity sha512-4UxTnR0fsmKL5YDalU2eLFVnL+OBebWUpX+hEduKGncrVKH4PPNoiRn1kXyOCjmzb0UvWgqubpssNouc8e0MCw== + "@nestjs/cli@^10.4.9": version "10.4.9" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.4.9.tgz#ac3a23096a4725465360d8d60810f3e857f4a803" @@ -1252,6 +1272,40 @@ dependencies: "@prisma/debug" "6.2.1" +"@redis/bloom@1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" + integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== + +"@redis/client@1.6.0": + version "1.6.0" + resolved "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz#dcf4ae1319763db6fdddd6de7f0af68a352c30ea" + integrity sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg== + dependencies: + cluster-key-slot "1.1.2" + generic-pool "3.9.0" + yallist "4.0.0" + +"@redis/graph@1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz#8c10df2df7f7d02741866751764031a957a170ea" + integrity sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw== + +"@redis/json@1.0.7": + version "1.0.7" + resolved "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz#016257fcd933c4cbcb9c49cde8a0961375c6893b" + integrity sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ== + +"@redis/search@1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz#50976fd3f31168f585666f7922dde111c74567b8" + integrity sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw== + +"@redis/time-series@1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz#cba454c05ec201bd5547aaf55286d44682ac8eb5" + integrity sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g== + "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" @@ -2599,6 +2653,14 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + busboy@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -2611,6 +2673,13 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cache-manager@^6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/cache-manager/-/cache-manager-6.4.2.tgz#a2d3f8c52b0b19849b41ccfca7dde1b81d4ab1de" + integrity sha512-oT0d1cGWZAlqEGDPjOfhmldTS767jT6kBT3KIdn7MX5OevlRVYqJT+LxRv5WY4xW9heJtYxeRRXaoKlEW+Biew== + dependencies: + keyv "^5.3.2" + call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" @@ -2849,6 +2918,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +cluster-key-slot@1.1.2, cluster-key-slot@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -4410,6 +4484,11 @@ gauge@^3.0.0: strip-ansi "^6.0.1" wide-align "^1.1.2" +generic-pool@3.9.0: + version "3.9.0" + resolved "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" + integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -4751,7 +4830,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -5747,6 +5826,13 @@ keyv@^4.5.4: dependencies: json-buffer "3.0.1" +keyv@^5.3.2: + version "5.3.3" + resolved "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz#ec2d723fbd7b908de5ee7f56b769d46dbbeaf8ba" + integrity sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ== + dependencies: + "@keyv/serialize" "^1.0.3" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -7131,6 +7217,18 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redis@^4.7.0: + version "4.7.0" + resolved "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz#b401787514d25dd0cfc22406d767937ba3be55d6" + integrity sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ== + dependencies: + "@redis/bloom" "1.2.0" + "@redis/client" "1.6.0" + "@redis/graph" "1.1.1" + "@redis/json" "1.0.7" + "@redis/search" "1.2.0" + "@redis/time-series" "1.1.0" + reflect-metadata@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" @@ -8686,16 +8784,16 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@4.0.0, yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yaml@^2.2.1, yaml@^2.5.0: version "2.7.1" resolved "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6" From c78a055056fdd2dfbbf42aa8421099cd27036db3 Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Sat, 26 Apr 2025 12:59:39 +0900 Subject: [PATCH 3/6] add docker compose file of rabbitmq --- .docker/rabbitmq/docker-compose.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .docker/rabbitmq/docker-compose.yml diff --git a/.docker/rabbitmq/docker-compose.yml b/.docker/rabbitmq/docker-compose.yml new file mode 100644 index 0000000..4e2bc9e --- /dev/null +++ b/.docker/rabbitmq/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' + +services: + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq + ports: + - '5672:5672' # AMQP 프로토콜 (앱들이 사용하는 포트) + - '15672:15672' # 관리 콘솔 (웹 UI) 포트 + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS} + volumes: + - rabbitmq-data:/var/lib/rabbitmq + restart: unless-stopped + +volumes: + rabbitmq-data: From 7178df68d92070c0a83ed69528cdcd30a897fd0d Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Sat, 26 Apr 2025 13:03:40 +0900 Subject: [PATCH 4/6] add: read env file --- .docker/rabbitmq/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.docker/rabbitmq/docker-compose.yml b/.docker/rabbitmq/docker-compose.yml index 4e2bc9e..2a6ea4f 100644 --- a/.docker/rabbitmq/docker-compose.yml +++ b/.docker/rabbitmq/docker-compose.yml @@ -7,6 +7,7 @@ services: ports: - '5672:5672' # AMQP 프로토콜 (앱들이 사용하는 포트) - '15672:15672' # 관리 콘솔 (웹 UI) 포트 + env_file: '../../env/.env.dev' environment: RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS} From aa4782a5ab487cbc109d3ae65fb5b78117cbc25b Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Sat, 26 Apr 2025 13:23:33 +0900 Subject: [PATCH 5/6] add rabbit mq init sh --- .docker/rabbitmq/init-dev.sh | 1 + 1 file changed, 1 insertion(+) create mode 100644 .docker/rabbitmq/init-dev.sh diff --git a/.docker/rabbitmq/init-dev.sh b/.docker/rabbitmq/init-dev.sh new file mode 100644 index 0000000..bf41dcc --- /dev/null +++ b/.docker/rabbitmq/init-dev.sh @@ -0,0 +1 @@ +docker compose --env-file=../../env/.env.dev up -d \ No newline at end of file From 5eb97d300bc9643203d6b7adf60174862692b670 Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Sat, 26 Apr 2025 13:41:34 +0900 Subject: [PATCH 6/6] remove cache from session/info --- apps/server/src/modules/auth/auth.controller.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/server/src/modules/auth/auth.controller.ts b/apps/server/src/modules/auth/auth.controller.ts index aa7129d..56e3dce 100644 --- a/apps/server/src/modules/auth/auth.controller.ts +++ b/apps/server/src/modules/auth/auth.controller.ts @@ -1,6 +1,5 @@ -import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager' import { - Controller, Get, Query, Req, Res, Session, UseInterceptors, + Controller, Get, Query, Req, Res, Session, } from '@nestjs/common' import { GetUser } from '@otl/server-nest/common/decorators/get-user.decorator' import { Public } from '@otl/server-nest/common/decorators/skip-auth.decorator' @@ -8,8 +7,6 @@ import { IAuth, IUser } from '@otl/server-nest/common/interfaces' import settings from '@otl/server-nest/settings' import { session_userprofile } from '@prisma/client' -import { RedisTTL } from '@otl/common/enum/time' - import { ESSOUser } from '@otl/prisma-client/entities' import { UserService } from '../user/user.service' @@ -77,8 +74,6 @@ export class AuthController { response.redirect(next_url) } - @CacheTTL(RedisTTL.DAY) - @UseInterceptors(CacheInterceptor) @Get('info') async getUserProfile(@GetUser() user: session_userprofile): Promise { /*