From 8eaefd049a7525ca4c74eaf802a32d1663ed9278 Mon Sep 17 00:00:00 2001 From: sjy2335 Date: Thu, 21 Nov 2024 15:43:23 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=9D=91=EB=8B=B5=20=EC=86=8D?= =?UTF-8?q?=EB=8F=84=20TOP5=20API=20=EC=9A=94=EC=B2=AD,=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5,=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20DT?= =?UTF-8?q?O?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/dto/get-speed-rank-response.dto.ts | 28 +++++++++++++++++++ .../src/log/dto/get-speed-rank.dto.ts | 14 ++++++++++ .../src/log/metric/speed-rank.metric.ts | 11 ++++++++ 3 files changed, 53 insertions(+) create mode 100644 backend/console-server/src/log/dto/get-speed-rank-response.dto.ts create mode 100644 backend/console-server/src/log/dto/get-speed-rank.dto.ts create mode 100644 backend/console-server/src/log/metric/speed-rank.metric.ts diff --git a/backend/console-server/src/log/dto/get-speed-rank-response.dto.ts b/backend/console-server/src/log/dto/get-speed-rank-response.dto.ts new file mode 100644 index 00000000..16fcb46f --- /dev/null +++ b/backend/console-server/src/log/dto/get-speed-rank-response.dto.ts @@ -0,0 +1,28 @@ +import { Exclude, Expose, Type } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ProjectSpeedData { + @ApiProperty({ + example: 'watchducks', + description: '해당 프로젝트명', + }) + @Expose() + projectName: string; + + @ApiProperty({ + example: 123.45, + description: '평균 응답 소요 시간 (ms).', + }) + @Expose() + @Type(() => Number) + avgResponseTime: number; +} + +export class GetSpeedRankResponseDto { + @ApiProperty({ + type: [ProjectSpeedData], + description: '프로젝트별 응답 속도 배열', + }) + @Type(() => ProjectSpeedData) + projectSpeedRank: ProjectSpeedData[]; +} diff --git a/backend/console-server/src/log/dto/get-speed-rank.dto.ts b/backend/console-server/src/log/dto/get-speed-rank.dto.ts new file mode 100644 index 00000000..411d1b56 --- /dev/null +++ b/backend/console-server/src/log/dto/get-speed-rank.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNumber } from 'class-validator'; + +export class GetSpeedRankDto { + @IsNumber() + @Type(() => Number) + @ApiProperty({ + description: '기수', + example: 5, + required: true, + }) + generation: number; +} diff --git a/backend/console-server/src/log/metric/speed-rank.metric.ts b/backend/console-server/src/log/metric/speed-rank.metric.ts new file mode 100644 index 00000000..057ededc --- /dev/null +++ b/backend/console-server/src/log/metric/speed-rank.metric.ts @@ -0,0 +1,11 @@ +import { IsNumber, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class SpeedRankMetric { + @IsString() + host: string; + + @Type(() => Number) + @IsNumber() + avg_elapsed_time: number; +} From 996f15499331229683b7fb91c80ceec9fab9813d Mon Sep 17 00:00:00 2001 From: sjy2335 Date: Thu, 21 Nov 2024 15:44:15 +0900 Subject: [PATCH 2/4] =?UTF-8?q?style:=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=86=8C=EB=93=9C=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../console-server/src/log/log.contorller.spec.ts | 6 +++--- .../console-server/src/log/log.repository.spec.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/console-server/src/log/log.contorller.spec.ts b/backend/console-server/src/log/log.contorller.spec.ts index 1725ead8..5895153b 100644 --- a/backend/console-server/src/log/log.contorller.spec.ts +++ b/backend/console-server/src/log/log.contorller.spec.ts @@ -300,9 +300,9 @@ describe('LogController 테스트', () => { mockRequestDto, ); expect(service.getTrafficDailyDifferenceByGeneration).toHaveBeenCalledTimes(1); - } - )} - + }); + }); + describe('getDAUByProject()는', () => { const mockRequestDto = { projectName: 'example-project', date: '2024-11-01' }; diff --git a/backend/console-server/src/log/log.repository.spec.ts b/backend/console-server/src/log/log.repository.spec.ts index 8c316620..ba74d945 100644 --- a/backend/console-server/src/log/log.repository.spec.ts +++ b/backend/console-server/src/log/log.repository.spec.ts @@ -207,7 +207,7 @@ describe('LogRepository 테스트', () => { it('올바른 도메인과 시간 단위를 기준으로 트래픽 데이터를 반환해야 한다.', async () => { mockClickhouse.query.mockResolvedValue(mockTrafficData); - const result = await repository.getTrafficByProject(domain, timeUnit); + const result = await repository.findTrafficByProject(domain, timeUnit); expect(result).toEqual(mockTrafficData); expect(clickhouse.query).toHaveBeenCalledWith( @@ -221,7 +221,7 @@ describe('LogRepository 테스트', () => { it('트래픽 데이터가 없을 경우 빈 배열을 반환해야 한다.', async () => { mockClickhouse.query.mockResolvedValue([]); - const result = await repository.getTrafficByProject(domain, timeUnit); + const result = await repository.findTrafficByProject(domain, timeUnit); expect(result).toEqual([]); expect(clickhouse.query).toHaveBeenCalledWith( @@ -234,7 +234,7 @@ describe('LogRepository 테스트', () => { const error = new Error('Clickhouse query failed'); mockClickhouse.query.mockRejectedValue(error); - await expect(repository.getTrafficByProject(domain, timeUnit)).rejects.toThrow( + await expect(repository.findTrafficByProject(domain, timeUnit)).rejects.toThrow( 'Clickhouse query failed', ); @@ -281,7 +281,7 @@ describe('LogRepository 테스트', () => { const mockResult = [{ dau: 150 }]; mockClickhouse.query.mockResolvedValue(mockResult); - const result = await repository.getDAUByProject(domain, date); + const result = await repository.findDAUByProject(domain, date); expect(result).toBe(150); expect(clickhouse.query).toHaveBeenCalledWith( @@ -295,7 +295,7 @@ describe('LogRepository 테스트', () => { it('DAU 데이터가 없을 경우 0을 반환해야 한다.', async () => { mockClickhouse.query.mockResolvedValue([]); - const result = await repository.getDAUByProject(domain, date); + const result = await repository.findDAUByProject(domain, date); expect(result).toBe(0); expect(clickhouse.query).toHaveBeenCalledWith( @@ -310,7 +310,7 @@ describe('LogRepository 테스트', () => { const mockResult = [{ dau: null }]; mockClickhouse.query.mockResolvedValue(mockResult); - const result = await repository.getDAUByProject(domain, date); + const result = await repository.findDAUByProject(domain, date); expect(result).toBe(0); expect(clickhouse.query).toHaveBeenCalledWith( @@ -325,7 +325,7 @@ describe('LogRepository 테스트', () => { const error = new Error('Clickhouse query failed'); mockClickhouse.query.mockRejectedValue(error); - await expect(repository.getDAUByProject(domain, date)).rejects.toThrow( + await expect(repository.findDAUByProject(domain, date)).rejects.toThrow( 'Clickhouse query failed', ); From 7d60f7085b330783352e9b620cd64f32304425b4 Mon Sep 17 00:00:00 2001 From: sjy2335 Date: Thu, 21 Nov 2024 15:45:19 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20TOP5=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=86=8D=EB=8F=84=20API=20=EA=B5=AC=ED=98=84=20-=20controller,?= =?UTF-8?q?=20service,=20repository=20=EA=B3=84=EC=B8=B5=EB=B3=84=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EA=B5=AC=ED=98=84=20-=20promise.?= =?UTF-8?q?all()=EB=A1=9C=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=EB=AA=85?= =?UTF-8?q?=20=EB=A7=A4=ED=95=91=20=EB=B3=91=EB=A0=AC=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../console-server/src/log/log.controller.ts | 21 ++++++++++++-- .../console-server/src/log/log.repository.ts | 20 ++++++++++--- backend/console-server/src/log/log.service.ts | 28 +++++++++++++++---- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/backend/console-server/src/log/log.controller.ts b/backend/console-server/src/log/log.controller.ts index 693a9876..7f7842b6 100644 --- a/backend/console-server/src/log/log.controller.ts +++ b/backend/console-server/src/log/log.controller.ts @@ -17,6 +17,8 @@ import { GetTrafficByGenerationResponseDto } from './dto/get-traffic-by-generati import { GetSuccessRateByProjectResponseDto } from './dto/get-success-rate-by-project-response.dto'; import { GetTrafficDailyDifferenceResponseDto } from './dto/get-traffic-daily-difference-response.dto'; import { GetTrafficDailyDifferenceDto } from './dto/get-traffic-daily-difference.dto'; +import { GetSpeedRankDto } from './dto/get-speed-rank.dto'; +import { GetSpeedRankResponseDto } from './dto/get-speed-rank-response.dto'; @Controller('log') export class LogController { @@ -33,7 +35,7 @@ export class LogController { description: '평균 응답시간이 성공적으로 반환됨.', type: GetAvgElapsedTimeResponseDto, }) - async elapsedTime() { + async getElapsedTime() { return await this.logService.getAvgElapsedTime(); } @@ -48,10 +50,25 @@ export class LogController { description: '트래픽 랭킹 TOP 5가 정상적으로 반환됨.', type: GetTrafficRankResponseDto, }) - async trafficRank() { + async getTrafficRank() { return await this.logService.getTrafficRank(); } + @Get('/elapsed-time/top5') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: '기수 내 응답 속도 TOP5', + description: '요청받은 기수의 응답 속도 랭킹 TOP 5를 반환합니다.', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: '응답 속도 랭킹 TOP5가 정상적으로 반환됨.', + type: GetSpeedRankResponseDto, + }) + async getSpeedRank(@Query() getSpeedRankDto: GetSpeedRankDto) { + return await this.logService.getSpeedRank(getSpeedRankDto); + } + @Get('/success-rate') @HttpCode(HttpStatus.OK) @ApiOperation({ diff --git a/backend/console-server/src/log/log.repository.ts b/backend/console-server/src/log/log.repository.ts index a312a3b7..452db2c3 100644 --- a/backend/console-server/src/log/log.repository.ts +++ b/backend/console-server/src/log/log.repository.ts @@ -9,6 +9,7 @@ import { TrafficCountMetric } from './metric/traffic-count.metric'; import { ErrorRateMetric } from './metric/error-rate.metric'; import { SuccessRateMetric } from './metric/success-rate.metric'; import { ElapsedTimeByPathMetric } from './metric/elapsed-time-by-path.metric'; +import { SpeedRankMetric } from './metric/speed-rank.metric'; @Injectable() export class LogRepository { @@ -138,7 +139,7 @@ export class LogRepository { return results.map((result) => plainToInstance(ElapsedTimeByPathMetric, result)); } - async getSlowestPathsByDomain(domain: string) { + async findSlowestPathsByDomain(domain: string) { const { query, params } = new TimeSeriesQueryBuilder() .metrics([{ name: 'elapsed_time', aggregation: 'avg' }, { name: 'path' }]) .from('http_log') @@ -153,7 +154,7 @@ export class LogRepository { return results.map((result) => plainToInstance(ElapsedTimeByPathMetric, result)); } - async getTrafficByProject(domain: string, timeUnit: string) { + async findTrafficByProject(domain: string, timeUnit: string) { const { query, params } = new TimeSeriesQueryBuilder() .metrics([ { name: '*', aggregation: 'count' }, @@ -170,7 +171,7 @@ export class LogRepository { return results.map((result) => plainToInstance(TrafficCountMetric, result)); } - async getDAUByProject(domain: string, date: string) { + async findDAUByProject(domain: string, date: string) { const { query, params } = new TimeSeriesQueryBuilder() .metrics([{ name: `SUM(access) as dau` }]) .from('dau') @@ -178,7 +179,18 @@ export class LogRepository { .build(); const [result] = await this.clickhouse.query<{ dau: number }>(query, params); - return result?.dau ? result.dau : 0; } + + async findSpeedRank() { + const { query, params } = new TimeSeriesQueryBuilder() + .metrics([{ name: 'elapsed_time', aggregation: 'avg' }, { name: 'host' }]) + .from('http_log') + .groupBy(['host']) + .orderBy(['avg_elapsed_time'], false) + .limit(5) + .build(); + const results = await this.clickhouse.query(query, params); + return results.map((result) => plainToInstance(SpeedRankMetric, result)); + } } diff --git a/backend/console-server/src/log/log.service.ts b/backend/console-server/src/log/log.service.ts index bb59ad3c..22807c4b 100644 --- a/backend/console-server/src/log/log.service.ts +++ b/backend/console-server/src/log/log.service.ts @@ -26,6 +26,8 @@ import { GetTrafficRankResponseDto } from './dto/get-traffic-rank-response.dto'; import { GetTrafficByGenerationResponseDto } from './dto/get-traffic-by-generation-response.dto'; import { GetTrafficDailyDifferenceDto } from './dto/get-traffic-daily-difference.dto'; import { GetTrafficDailyDifferenceResponseDto } from './dto/get-traffic-daily-difference-response.dto'; +import { GetSpeedRankDto } from './dto/get-speed-rank.dto'; +import { GetSpeedRankResponseDto } from './dto/get-speed-rank-response.dto'; @Injectable() export class LogService { @@ -132,10 +134,8 @@ export class LogService { if (!project) throw new NotFoundException(`Project with name ${projectName} not found`); - console.log('??'); - const fastestPaths = await this.logRepository.getFastestPathsByDomain(project.domain); - const slowestPaths = await this.logRepository.getSlowestPathsByDomain(project.domain); + const slowestPaths = await this.logRepository.findSlowestPathsByDomain(project.domain); return plainToInstance(GetPathSpeedRankResponseDto, { projectName, @@ -163,7 +163,7 @@ export class LogService { }); if (!project) throw new NotFoundException(`Project with name ${projectName} not found`); - const results = await this.logRepository.getTrafficByProject(project.domain, timeUnit); + const results = await this.logRepository.findTrafficByProject(project.domain, timeUnit); return plainToInstance(GetTrafficByProjectResponseDto, { projectName, @@ -182,11 +182,29 @@ export class LogService { if (!project) { throw new NotFoundException(`Project with name ${projectName} not found`); } - const dau = await this.logRepository.getDAUByProject(project.domain, date); + const dau = await this.logRepository.findDAUByProject(project.domain, date); return plainToInstance(GetDAUByProjectResponseDto, { projectName, date, dau, }); } + + async getSpeedRank(_getSpeedRankDto: GetSpeedRankDto) { + const speedRankData = await this.logRepository.findSpeedRank(); + const response = await Promise.all( + speedRankData.map(async (data) => { + const project = await this.projectRepository.findOne({ + where: { domain: data.host }, + select: ['name'], + }); + return { + projectName: project?.name || 'Unknown', + avgResponseTime: data.avg_elapsed_time, + }; + }), + ); + + return plainToInstance(GetSpeedRankResponseDto, response); + } } From 4ef60ab9edea0058f990a100072b558f991dc041 Mon Sep 17 00:00:00 2001 From: sjy2335 Date: Thu, 21 Nov 2024 17:12:19 +0900 Subject: [PATCH 4/4] =?UTF-8?q?test:=20TOP5=20=EC=9D=91=EB=8B=B5=EC=86=8D?= =?UTF-8?q?=EB=8F=84=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84,=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/log/log.contorller.spec.ts | 57 +++++++- .../src/log/log.repository.spec.ts | 60 +-------- .../src/log/log.service.spec.ts | 127 ++++-------------- 3 files changed, 80 insertions(+), 164 deletions(-) diff --git a/backend/console-server/src/log/log.contorller.spec.ts b/backend/console-server/src/log/log.contorller.spec.ts index 5895153b..69419f79 100644 --- a/backend/console-server/src/log/log.contorller.spec.ts +++ b/backend/console-server/src/log/log.contorller.spec.ts @@ -14,7 +14,7 @@ describe('LogController 테스트', () => { const mockLogService = { httpLog: jest.fn(), - elapsedTime: jest.fn(), + getAvgElapsedTime: jest.fn(), trafficRank: jest.fn(), getResponseSuccessRate: jest.fn(), getResponseSuccessRateByProject: jest.fn(), @@ -23,6 +23,7 @@ describe('LogController 테스트', () => { getTrafficByProject: jest.fn(), getTrafficDailyDifferenceByGeneration: jest.fn(), getDAUByProject: jest.fn(), + getSpeedRank: jest.fn(), }; beforeEach(async () => { @@ -53,9 +54,9 @@ describe('LogController 테스트', () => { }; it('평균 응답 시간을 ProjectResponseDto 형식으로 반환해야 한다', async () => { - mockLogService.elapsedTime.mockResolvedValue(mockResult); + mockLogService.getAvgElapsedTime.mockResolvedValue(mockResult); - const result = await controller.elapsedTime(); + const result = await controller.getElapsedTime(); expect(result).toEqual(mockResult); expect(result).toHaveProperty('status', HttpStatus.OK); @@ -79,7 +80,8 @@ describe('LogController 테스트', () => { it('TOP 5 트래픽 순위를 ProjectResponseDto 형식으로 반환해야 한다', async () => { mockLogService.trafficRank.mockResolvedValue(mockResult); - const result = (await controller.trafficRank()) as unknown as TrafficRankResponseType; + const result = + (await controller.getTrafficRank()) as unknown as TrafficRankResponseType; expect(result).toEqual(mockResult); expect(result).toHaveProperty('status', HttpStatus.OK); @@ -335,4 +337,51 @@ describe('LogController 테스트', () => { expect(service.getDAUByProject).toHaveBeenCalledTimes(1); }); }); + + describe('getSpeedRank()는', () => { + const mockRequestDto = { + generation: 5, + }; + + const mockResponseDto = [ + { projectName: 'project1', avgElapsedTime: 123.45 }, + { projectName: 'project2', avgElapsedTime: 145.67 }, + { projectName: 'project3', avgElapsedTime: 150.89 }, + { projectName: 'project4', avgElapsedTime: 180.23 }, + { projectName: 'project5', avgElapsedTime: 200.34 }, + ]; + + it('응답 속도 TOP5 데이터를 반환해야 한다', async () => { + mockLogService.getSpeedRank.mockResolvedValue(mockResponseDto); + + const result = await controller.getSpeedRank(mockRequestDto); + + expect(result).toEqual(mockResponseDto); + expect(result).toHaveLength(5); + expect(result[0]).toHaveProperty('projectName', 'project1'); + expect(result[0]).toHaveProperty('avgElapsedTime', 123.45); + expect(service.getSpeedRank).toHaveBeenCalledWith(mockRequestDto); + expect(service.getSpeedRank).toHaveBeenCalledTimes(1); + }); + + it('서비스 메소드 호출시 에러가 발생하면 예외를 throw 해야 한다', async () => { + const error = new Error('Database error'); + mockLogService.getSpeedRank.mockRejectedValue(error); + + await expect(controller.getSpeedRank(mockRequestDto)).rejects.toThrow(error); + + expect(service.getSpeedRank).toHaveBeenCalledWith(mockRequestDto); + expect(service.getSpeedRank).toHaveBeenCalledTimes(1); + }); + + it('데이터가 없을 때 빈 배열을 반환한다', async () => { + mockLogService.getSpeedRank.mockResolvedValue([]); + + const result = await controller.getSpeedRank(mockRequestDto); + + expect(result).toEqual([]); + expect(service.getSpeedRank).toHaveBeenCalledWith(mockRequestDto); + expect(service.getSpeedRank).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/backend/console-server/src/log/log.repository.spec.ts b/backend/console-server/src/log/log.repository.spec.ts index ba74d945..5287508e 100644 --- a/backend/console-server/src/log/log.repository.spec.ts +++ b/backend/console-server/src/log/log.repository.spec.ts @@ -129,7 +129,7 @@ describe('LogRepository 테스트', () => { describe('findTrafficByGeneration()는 ', () => { it('전체 트래픽 수를 반환해야 한다.', async () => { - const mockResult = [{ count: 5000 }]; + const mockResult = { count: 5000 }; mockClickhouse.query.mockResolvedValue(mockResult); const result = await repository.findTrafficByGeneration(); @@ -142,58 +142,6 @@ describe('LogRepository 테스트', () => { }); }); - describe('getPathSpeedRankByProject()는 ', () => { - const domain = 'example.com'; - const mockFastestPaths = [ - { path: '/api/v1/resource', avg_elapsed_time: 123.45 }, - { path: '/api/v1/users', avg_elapsed_time: 145.67 }, - { path: '/api/v1/orders', avg_elapsed_time: 150.89 }, - ]; - const mockSlowestPaths = [ - { path: '/api/v1/reports', avg_elapsed_time: 345.67 }, - { path: '/api/v1/logs', avg_elapsed_time: 400.23 }, - { path: '/api/v1/stats', avg_elapsed_time: 450.56 }, - ]; - - it('도메인을 기준으로 Top 3 fastest 경로와 slowest 경로를 반환해야 한다.', async () => { - mockClickhouse.query - .mockResolvedValueOnce(mockFastestPaths) - .mockResolvedValueOnce(mockSlowestPaths); - - const result = await repository.getFastestPathsByDomain(domain); - - expect(result).toEqual({ - fastestPaths: mockFastestPaths, - slowestPaths: mockSlowestPaths, - }); - - expect(clickhouse.query).toHaveBeenCalledTimes(2); - expect(clickhouse.query).toHaveBeenNthCalledWith( - 1, - expect.stringMatching( - /SELECT\s+avg\(elapsed_time\) as avg_elapsed_time,\s+path\s+FROM http_log\s+WHERE host = \{host:String}\s+GROUP BY path\s+ORDER BY avg_elapsed_time\s+LIMIT 3/, - ), - expect.objectContaining({ host: domain }), - ); - expect(clickhouse.query).toHaveBeenNthCalledWith( - 2, - expect.stringMatching( - /SELECT\s+avg\(elapsed_time\) as avg_elapsed_time,\s+path\s+FROM http_log\s+WHERE host = \{host:String}\s+GROUP BY path\s+ORDER BY avg_elapsed_time DESC\s+LIMIT 3/, - ), - expect.objectContaining({ host: domain }), - ); - }); - - it('클릭하우스 에러 발생 시 예외를 throw 해야 한다.', async () => { - const error = new Error('Clickhouse query failed'); - mockClickhouse.query.mockRejectedValue(error); - - await expect(repository.getFastestPathsByDomain(domain)).rejects.toThrow( - 'Clickhouse query failed', - ); - }); - }); - describe('getTrafficByProject()는', () => { const domain = 'example.com'; const timeUnit = 'Hour'; @@ -270,9 +218,9 @@ describe('LogRepository 테스트', () => { expect(result).toEqual(mockTraffic); expect(clickhouse.query).toHaveBeenCalledWith(expect.any(String), expect.any(Object)); - } - )} - + }); + }); + describe('getDAUByProject()', () => { const domain = 'example.com'; const date = '2024-11-18'; diff --git a/backend/console-server/src/log/log.service.spec.ts b/backend/console-server/src/log/log.service.spec.ts index 43bb09f9..3adf2a87 100644 --- a/backend/console-server/src/log/log.service.spec.ts +++ b/backend/console-server/src/log/log.service.spec.ts @@ -8,7 +8,6 @@ import { NotFoundException } from '@nestjs/common'; import type { GetTrafficByGenerationResponseDto } from './dto/get-traffic-by-generation-response.dto'; import { GetTrafficByGenerationDto } from './dto/get-traffic-by-generation.dto'; import { GetSuccessRateByProjectResponseDto } from './dto/get-success-rate-by-project-response.dto'; -import { GetSuccessRateByProjectResponseDTO } from './dto/get-success-rate-by-project-response.dto'; import type { GetTrafficDailyDifferenceDto } from './dto/get-traffic-daily-difference.dto'; import { GetTrafficDailyDifferenceResponseDto } from './dto/get-traffic-daily-difference-response.dto'; @@ -19,15 +18,16 @@ describe('LogService 테스트', () => { const mockLogRepository = { findHttpLog: jest.fn(), findAvgElapsedTime: jest.fn(), - findCountByHost: jest.fn(), + findTop5CountByHost: jest.fn(), findResponseSuccessRate: jest.fn(), findResponseSuccessRateByProject: jest.fn(), findTrafficByGeneration: jest.fn(), - getPathSpeedRankByProject: jest.fn(), - getTrafficByProject: jest.fn(), + findPathSpeedRankByProject: jest.fn(), + findTrafficByProject: jest.fn(), findTrafficDailyDifferenceByGeneration: jest.fn(), findTrafficForTimeRange: jest.fn(), - getDAUByProject: jest.fn(), + findDAUByProject: jest.fn(), + findSpeedRank: jest.fn(), }; beforeEach(async () => { @@ -224,87 +224,6 @@ describe('LogService 테스트', () => { }); }); - describe('getPathSpeedRankByProject()는 ', () => { - const mockRequestDto = { projectName: 'example-project' }; - - const mockProject = { - name: 'example-project', - domain: 'example.com', - }; - - const mockPathSpeedRank = { - fastestPaths: [ - { path: '/api/v1/resource', avg_elapsed_time: 123.45 }, - { path: '/api/v1/users', avg_elapsed_time: 145.67 }, - { path: '/api/v1/orders', avg_elapsed_time: 150.89 }, - ], - slowestPaths: [ - { path: '/api/v1/reports', avg_elapsed_time: 345.67 }, - { path: '/api/v1/logs', avg_elapsed_time: 400.23 }, - { path: '/api/v1/stats', avg_elapsed_time: 450.56 }, - ], - }; - - it('프로젝트명을 기준으로 도메인을 조회한 후 경로별 응답 속도 순위를 반환해야 한다', async () => { - const projectRepository = service['projectRepository']; - projectRepository.findOne = jest.fn().mockResolvedValue(mockProject); - - mockLogRepository.getPathSpeedRankByProject = jest - .fn() - .mockResolvedValue(mockPathSpeedRank); - - const result = await service.getPathSpeedRankByProject(mockRequestDto); - - expect(projectRepository.findOne).toHaveBeenCalledWith({ - where: { name: mockRequestDto.projectName }, - select: ['domain'], - }); - expect(mockLogRepository.getPathSpeedRankByProject).toHaveBeenCalledWith( - mockProject.domain, - ); - expect(result).toEqual({ - projectName: mockRequestDto.projectName, - ...mockPathSpeedRank, - }); - }); - - it('존재하지 않는 프로젝트명을 조회할 경우 NotFoundException을 던져야 한다', async () => { - const projectRepository = service['projectRepository']; - projectRepository.findOne = jest.fn().mockResolvedValue(null); - - await expect(service.getPathSpeedRankByProject(mockRequestDto)).rejects.toThrow( - new NotFoundException(`Project with name ${mockRequestDto.projectName} not found`), - ); - - expect(projectRepository.findOne).toHaveBeenCalledWith({ - where: { name: mockRequestDto.projectName }, - select: ['domain'], - }); - expect(mockLogRepository.getPathSpeedRankByProject).not.toHaveBeenCalled(); - }); - - it('로그 레포지토리 호출 중 에러가 발생할 경우 예외를 던져야 한다', async () => { - const projectRepository = service['projectRepository']; - projectRepository.findOne = jest.fn().mockResolvedValue(mockProject); - - mockLogRepository.getPathSpeedRankByProject = jest - .fn() - .mockRejectedValue(new Error('Database error')); - - await expect(service.getPathSpeedRankByProject(mockRequestDto)).rejects.toThrow( - 'Database error', - ); - - expect(projectRepository.findOne).toHaveBeenCalledWith({ - where: { name: mockRequestDto.projectName }, - select: ['domain'], - }); - expect(mockLogRepository.getPathSpeedRankByProject).toHaveBeenCalledWith( - mockProject.domain, - ); - }); - }); - describe('getTrafficByProject()는', () => { const mockRequestDto = { projectName: 'example-project', timeUnit: 'month' }; const mockProject = { @@ -325,7 +244,7 @@ describe('LogService 테스트', () => { const projectRepository = service['projectRepository']; projectRepository.findOne = jest.fn().mockResolvedValue(mockProject); - mockLogRepository.getTrafficByProject = jest.fn().mockResolvedValue(mockTrafficData); + mockLogRepository.findTrafficByProject = jest.fn().mockResolvedValue(mockTrafficData); const result = await service.getTrafficByProject(mockRequestDto); @@ -333,7 +252,7 @@ describe('LogService 테스트', () => { where: { name: mockRequestDto.projectName }, select: ['domain'], }); - expect(mockLogRepository.getTrafficByProject).toHaveBeenCalledWith( + expect(mockLogRepository.findTrafficByProject).toHaveBeenCalledWith( mockProject.domain, mockRequestDto.timeUnit, ); @@ -352,14 +271,14 @@ describe('LogService 테스트', () => { where: { name: mockRequestDto.projectName }, select: ['domain'], }); - expect(mockLogRepository.getTrafficByProject).not.toHaveBeenCalled(); + expect(mockLogRepository.findTrafficByProject).not.toHaveBeenCalled(); }); it('로그 레포지토리 호출 중 에러가 발생할 경우 예외를 던져야 한다', async () => { const projectRepository = service['projectRepository']; projectRepository.findOne = jest.fn().mockResolvedValue(mockProject); - mockLogRepository.getTrafficByProject = jest + mockLogRepository.findTrafficByProject = jest .fn() .mockRejectedValue(new Error('Database error')); @@ -371,7 +290,7 @@ describe('LogService 테스트', () => { where: { name: mockRequestDto.projectName }, select: ['domain'], }); - expect(mockLogRepository.getTrafficByProject).toHaveBeenCalledWith( + expect(mockLogRepository.findTrafficByProject).toHaveBeenCalledWith( mockProject.domain, mockRequestDto.timeUnit, ); @@ -381,7 +300,7 @@ describe('LogService 테스트', () => { const projectRepository = service['projectRepository']; projectRepository.findOne = jest.fn().mockResolvedValue(mockProject); - mockLogRepository.getTrafficByProject = jest.fn().mockResolvedValue([]); + mockLogRepository.findTrafficByProject = jest.fn().mockResolvedValue([]); const result = await service.getTrafficByProject(mockRequestDto); @@ -389,7 +308,7 @@ describe('LogService 테스트', () => { where: { name: mockRequestDto.projectName }, select: ['domain'], }); - expect(mockLogRepository.getTrafficByProject).toHaveBeenCalledWith( + expect(mockLogRepository.findTrafficByProject).toHaveBeenCalledWith( mockProject.domain, mockRequestDto.timeUnit, ); @@ -477,10 +396,10 @@ describe('LogService 테스트', () => { 2, yesterdayStart, yesterdayEnd, - } - )} - } - )} + ); + }); + }); + describe('getDAUByProject()는', () => { const mockRequestDto = { projectName: 'example-project', date: '2024-11-01' }; const mockProject = { @@ -498,7 +417,7 @@ describe('LogService 테스트', () => { const projectRepository = service['projectRepository']; projectRepository.findOne = jest.fn().mockResolvedValue(mockProject); - mockLogRepository.getDAUByProject = jest.fn().mockResolvedValue(mockDAUData); + mockLogRepository.findDAUByProject = jest.fn().mockResolvedValue(mockDAUData); const result = await service.getDAUByProject(mockRequestDto); @@ -506,7 +425,7 @@ describe('LogService 테스트', () => { where: { name: mockRequestDto.projectName }, select: ['domain'], }); - expect(mockLogRepository.getDAUByProject).toHaveBeenCalledWith( + expect(mockLogRepository.findDAUByProject).toHaveBeenCalledWith( mockProject.domain, mockRequestDto.date, ); @@ -525,14 +444,14 @@ describe('LogService 테스트', () => { where: { name: mockRequestDto.projectName }, select: ['domain'], }); - expect(mockLogRepository.getDAUByProject).not.toHaveBeenCalled(); + expect(mockLogRepository.findDAUByProject).not.toHaveBeenCalled(); }); it('존재하는 프로젝트에 DAU 데이터가 없을 경우 0으로 반환해야 한다', async () => { const projectRepository = service['projectRepository']; projectRepository.findOne = jest.fn().mockResolvedValue(mockProject); - mockLogRepository.getDAUByProject = jest.fn().mockResolvedValue(0); + mockLogRepository.findDAUByProject = jest.fn().mockResolvedValue(0); const result = await service.getDAUByProject(mockRequestDto); @@ -540,7 +459,7 @@ describe('LogService 테스트', () => { where: { name: mockRequestDto.projectName }, select: ['domain'], }); - expect(mockLogRepository.getDAUByProject).toHaveBeenCalledWith( + expect(mockLogRepository.findDAUByProject).toHaveBeenCalledWith( mockProject.domain, mockRequestDto.date, ); @@ -555,7 +474,7 @@ describe('LogService 테스트', () => { const projectRepository = service['projectRepository']; projectRepository.findOne = jest.fn().mockResolvedValue(mockProject); - mockLogRepository.getDAUByProject = jest + mockLogRepository.findDAUByProject = jest .fn() .mockRejectedValue(new Error('Database error')); @@ -565,7 +484,7 @@ describe('LogService 테스트', () => { where: { name: mockRequestDto.projectName }, select: ['domain'], }); - expect(mockLogRepository.getDAUByProject).toHaveBeenCalledWith( + expect(mockLogRepository.findDAUByProject).toHaveBeenCalledWith( mockProject.domain, mockRequestDto.date, );