Skip to content

Commit

Permalink
Merge pull request #200 from boostcampwm-2024/dev-back
Browse files Browse the repository at this point in the history
[BE] Merge to main
  • Loading branch information
hyo-limilimee authored Nov 28, 2024
2 parents 974cbda + 37e43e5 commit 3a560d1
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 25 deletions.
11 changes: 11 additions & 0 deletions backend/console-server/src/common/cache/cache.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const CACHE_REFRESH_THRESHOLD_METADATA = 'CACHE_REFRESH_THRESHOLD';

export const FIFTEEN_SECONDS = 15 * 1000;
export const THIRTY_SECONDS = 30 * 1000;
export const ONE_MINUTE = 60 * 1000;
export const ONE_MINUTE_HALF = 3 * 30 * 1000;
export const THREE_MINUTES = 3 * 60 * 1000;
export const FIVE_MINUTES = 5 * 60 * 1000;
export const TEN_MINUTES = 10 * 60 * 1000;
export const HALF_HOUR = 30 * 60 * 1000;
export const ONE_HOUR = 60 * 60 * 1000;
23 changes: 23 additions & 0 deletions backend/console-server/src/common/cache/cache.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ExecutionContext } from '@nestjs/common';
import { SetMetadata } from '@nestjs/common';
import { CacheTTL } from '@nestjs/cache-manager';
import { CACHE_REFRESH_THRESHOLD_METADATA } from './cache.constant';

const calculateMillisecondsUntilMidnight = () => {
const now = new Date();
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
return midnight.getTime() - now.getTime();
};

export const CacheTTLUntilMidnight = () => {
return CacheTTL((_ctx: ExecutionContext) => calculateMillisecondsUntilMidnight());
};

type CacheRefreshThresholdFactory = (ctx: ExecutionContext) => Promise<number> | number;

export const CacheRefreshThreshold = (threshold: number | CacheRefreshThresholdFactory) =>
SetMetadata(CACHE_REFRESH_THRESHOLD_METADATA, threshold);

export const CacheRefreshAtMidnight = () => {
return CacheRefreshThreshold((_ctx: ExecutionContext) => calculateMillisecondsUntilMidnight());
};
111 changes: 111 additions & 0 deletions backend/console-server/src/common/cache/cache.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
Logger,
NestInterceptor,
Optional,
} from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER, CACHE_KEY_METADATA, CACHE_TTL_METADATA } from '@nestjs/cache-manager';
import { Reflector, HttpAdapterHost } from '@nestjs/core';
import { firstValueFrom } from 'rxjs';
import { CACHE_REFRESH_THRESHOLD_METADATA } from './cache.constant';
import { isNil } from '@nestjs/common/utils/shared.utils';

@Injectable()
export class CustomCacheInterceptor<T> implements NestInterceptor<T, T> {
@Optional()
@Inject()
protected readonly httpAdapterHost: HttpAdapterHost;

protected allowedMethods = ['GET'];

constructor(
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
private readonly reflector: Reflector,
) {}

async intercept(context: ExecutionContext, next: CallHandler<T>): Promise<Observable<T>> {
const cacheKey = this.trackBy(context);

const ttlValueOrFactory =
this.reflector.get(CACHE_TTL_METADATA, context.getHandler()) ??
this.reflector.get(CACHE_TTL_METADATA, context.getClass()) ??
null;

const refreshThresholdValueOrFactory =
this.reflector.get(CACHE_REFRESH_THRESHOLD_METADATA, context.getHandler()) ??
this.reflector.get(CACHE_REFRESH_THRESHOLD_METADATA, context.getClass()) ??
null;

if (!cacheKey) return next.handle();

try {
const ttl =
typeof ttlValueOrFactory === 'function'
? await ttlValueOrFactory(context)
: ttlValueOrFactory;

const refreshThreshold =
typeof refreshThresholdValueOrFactory === 'function'
? await refreshThresholdValueOrFactory(context)
: refreshThresholdValueOrFactory;

const args: [string, () => Promise<T>, number?, number?] = [
cacheKey,
() => firstValueFrom(next.handle()),
];
if (!isNil(ttl)) args.push(ttl);
if (!isNil(refreshThreshold)) args.push(refreshThreshold);

const cachedResponse = await this.cacheManager.wrap<T>(...args);

this.setHeadersWhenHttp(context, cachedResponse);

return of(cachedResponse);
} catch (err) {
Logger.error(
`CacheInterceptor Error: ${err.message}`,
err.stack,
'CustomCacheInterceptor',
);
return next.handle();
}
}

protected trackBy(context: ExecutionContext): string | undefined {
const httpAdapter = this.httpAdapterHost.httpAdapter;
const isHttpApp = httpAdapter && !!httpAdapter.getRequestMethod;
const cacheMetadata = this.reflector.get(CACHE_KEY_METADATA, context.getHandler());

if (!isHttpApp || cacheMetadata) {
return cacheMetadata;
}

const request = context.getArgByIndex(0);
if (!this.isRequestCacheable(context)) {
return undefined;
}
return httpAdapter.getRequestUrl(request);
}

protected isRequestCacheable(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
return this.allowedMethods.includes(req.method);
}

protected setHeadersWhenHttp(context: ExecutionContext, value: unknown): void {
if (!this.httpAdapterHost) {
return;
}
const { httpAdapter } = this.httpAdapterHost;
if (!httpAdapter) {
return;
}
const response = context.switchToHttp().getResponse();
httpAdapter.setHeader(response, 'X-Cache', !isNil(value) ? 'HIT' : 'MISS');
}
}
3 changes: 3 additions & 0 deletions backend/console-server/src/common/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './cache.constant';
export * from './cache.decorator';
export * from './cache.interceptor';
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import { GetTop5ElapsedTime } from './dto/get-top5-elapsed.time';
import { GetTop5ElapsedTimeDto } from './dto/get-top5-elapsed-time.dto';
import { GetPathElapsedTimeResponseDto } from './dto/get-path-elapsed-time-response.dto';
import { GetPathElapsedTimeRank } from './dto/get-path-elapsed-time.rank';
import { CacheInterceptor } from '@nestjs/cache-manager';
import { CustomCacheInterceptor, CacheRefreshThreshold } from '../../common/cache';
import { CacheTTL } from '@nestjs/cache-manager';
import { THREE_MINUTES, ONE_MINUTE_HALF } from '../../common/cache';

@Controller('log/elapsed-time')
@UseInterceptors(CacheInterceptor)
@UseInterceptors(CustomCacheInterceptor)
@CacheTTL(THREE_MINUTES)
@CacheRefreshThreshold(ONE_MINUTE_HALF)
export class ElapsedTimeController {
constructor(private readonly elapsedTimeService: ElapsedTimeService) {}

@Get('')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '기수 총 트래픽 평균 응답시간 API',
description: '요청받은 기수의 트래픽에 대한 평균 응답시간을 반환합니다.',
summary: '기수 총 트래픽 전체 기간 평균 응답시간',
description: '요청받은 기수 트래픽의 전체 기간 평균 응답시간을 반환합니다.',
})
@ApiResponse({
status: HttpStatus.OK,
Expand All @@ -32,7 +36,7 @@ export class ElapsedTimeController {
@Get('/top5')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '기수 내 응답 속도 TOP5',
summary: '기수 내 전체 기간 평균 응답 속도 TOP5',
description: '요청받은 기수의 응답 속도 랭킹 TOP 5를 반환합니다.',
})
@ApiResponse({
Expand All @@ -47,7 +51,7 @@ export class ElapsedTimeController {
@Get('/path-rank')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '개별 프로젝트의 경로별 응답 속도 순위',
summary: '개별 프로젝트의 전체 기간 경로별 응답 속도 순위',
description:
'개별 프로젝트의 경로별 응답 속도 중 가장 빠른/느린 3개를 반환합니다. 빠른 응답은 유효한(상태 코드 200번대)만을 포함합니다.',
})
Expand Down
10 changes: 10 additions & 0 deletions backend/console-server/src/log/rank/rank.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { GetSuccessRateRankResponseDto } from './dto/get-success-rate-rank-
import type { GetTrafficRankDto } from './dto/get-traffic-rank.dto';
import type { GetTrafficRankResponseDto } from './dto/get-traffic-rank-response.dto';
import type { GetElapsedTimeRankDto } from './dto/get-elapsed-time-rank.dto';
import { CACHE_MANAGER } from '@nestjs/cache-manager';

describe('RankController', () => {
let controller: RankController;
Expand All @@ -19,6 +20,11 @@ describe('RankController', () => {
getDAURank: jest.fn(),
getTrafficRank: jest.fn(),
};
const mockCacheManager = {
get: jest.fn(),
set: jest.fn(),
del: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
Expand All @@ -28,6 +34,10 @@ describe('RankController', () => {
provide: RankService,
useValue: mockRankService,
},
{
provide: CACHE_MANAGER,
useValue: mockCacheManager,
},
],
}).compile();

Expand Down
13 changes: 8 additions & 5 deletions backend/console-server/src/log/rank/rank.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RankService } from './rank.service';
import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common';
import { Controller, Get, HttpCode, HttpStatus, Query, UseInterceptors } from '@nestjs/common';
import { GetSuccessRateRankResponseDto } from './dto/get-success-rate-rank-response.dto';
import { GetSuccessRateRankDto } from './dto/get-success-rate-rank.dto';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
Expand All @@ -8,15 +8,18 @@ import { GetElapsedTimeRankDto } from './dto/get-elapsed-time-rank.dto';
import { GetDAURankDto } from './dto/get-dau-rank.dto';
import { GetDAURankResponseDto } from './dto/get-dau-rank-response.dto';
import { GetTrafficRankDto } from './dto/get-traffic-rank.dto';
import { CacheTTLUntilMidnight, CustomCacheInterceptor } from '../../common/cache';

@Controller('log/rank')
@UseInterceptors(CustomCacheInterceptor)
@CacheTTLUntilMidnight()
export class RankController {
constructor(private readonly rankService: RankService) {}

@Get('/success-rate')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '기수 내 응답 성공률 랭킹',
summary: '기수 내 어제 동안 응답 성공률 랭킹',
description: '요청받은 기수의 기수 내 응답 성공률 랭킹을 반환합니다.',
})
@ApiResponse({
Expand All @@ -31,7 +34,7 @@ export class RankController {
@Get('/elapsed-time')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '기수 내 응답 소요 시간 랭킹',
summary: '기수 내 어제 동안 응답 소요 시간 랭킹',
description: '요청 받은 기수의 어제 하루 동안 응답 소요 시간 랭킹을 반환합니다.',
})
@ApiResponse({
Expand All @@ -46,7 +49,7 @@ export class RankController {
@Get('/dau')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '기수 내 DAU 랭킹',
summary: '기수 내 어제 동안 DAU 랭킹',
description: '요청받은 기수의 기수 내 DAU 랭킹을 반환합니다.',
})
@ApiResponse({
Expand All @@ -61,7 +64,7 @@ export class RankController {
@Get('/traffic')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '기수 내 트래픽 랭킹',
summary: '기수 내 전체 데이터 트래픽 랭킹',
description: '요청받은 기수의 기수 내 트래픽 랭킹을 반환합니다.',
})
@ApiResponse({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import { GetSuccessRateResponseDto } from './dto/get-success-rate-response.dto';
import { GetSuccessRateDto } from './dto/get-success-rate.dto';
import { GetProjectSuccessRateResponseDto } from './dto/get-project-success-rate-response.dto';
import { GetProjectSuccessRateDto } from './dto/get-project-success-rate.dto';
import { CacheInterceptor } from '@nestjs/cache-manager';
import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';
import { CacheRefreshThreshold, ONE_MINUTE, THREE_MINUTES } from '../../common/cache';

@Controller('log/success-rate')
@UseInterceptors(CacheInterceptor)
@CacheTTL(THREE_MINUTES)
@CacheRefreshThreshold(ONE_MINUTE)
export class SuccessRateController {
constructor(private readonly successRateService: SuccessRateService) {}

@Get('')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '기수 내 응답 성공률',
description: '요청받은 기수의 기수 내 응답 성공률를 반환합니다.',
summary: '기수 내 전체 기간 응답 성공률',
description: '요청받은 기수의 기수 내 전체 기간 트래픽의 응답 성공률를 반환합니다.',
})
@ApiResponse({
status: HttpStatus.OK,
Expand All @@ -30,12 +33,12 @@ export class SuccessRateController {
@Get('/project')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '프로젝트 별 응답 성공률',
summary: '프로젝트 별 전체 기간 응답 성공률',
description: '요청받은 프로젝트의 응답 성공률을 반환합니다.',
})
@ApiResponse({
status: HttpStatus.OK,
description: '프로젝트 별 응답 성공률이 성공적으로 반환됨.',
description: '프로젝트 별 전체 기간의 응답 성공률이 성공적으로 반환됨.',
type: GetProjectSuccessRateResponseDto,
})
async getProjectSuccessRate(@Query() getProjectSuccessRateDto: GetProjectSuccessRateDto) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TrafficController } from './traffic.controller';
import { TrafficService } from './traffic.service';
import type { GetTrafficTop5ChartResponseDto } from './dto/get-traffic-top5-chart-response.dto';
import type { GetTrafficTop5ChartDto } from './dto/get-traffic-top5-chart.dto';
import { CACHE_MANAGER } from '@nestjs/cache-manager';

interface TrafficRankResponseType {
status: number;
Expand All @@ -24,7 +25,11 @@ describe('TrafficController 테스트', () => {
getTrafficDailyDifferenceByGeneration: jest.fn(),
getTrafficTop5Chart: jest.fn(),
};

const mockCacheManager = {
get: jest.fn(),
set: jest.fn(),
del: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TrafficController],
Expand All @@ -33,6 +38,10 @@ describe('TrafficController 테스트', () => {
provide: TrafficService,
useValue: mockTrafficService,
},
{
provide: CACHE_MANAGER,
useValue: mockCacheManager,
},
],
}).compile();

Expand Down
Loading

0 comments on commit 3a560d1

Please sign in to comment.