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/log.contorller.spec.ts b/backend/console-server/src/log/log.contorller.spec.ts index e5d734ea..04a70c49 100644 --- a/backend/console-server/src/log/log.contorller.spec.ts +++ b/backend/console-server/src/log/log.contorller.spec.ts @@ -18,7 +18,7 @@ describe('LogController 테스트', () => { const mockLogService = { httpLog: jest.fn(), - elapsedTime: jest.fn(), + getAvgElapsedTime: jest.fn(), trafficRank: jest.fn(), getResponseSuccessRate: jest.fn(), getResponseSuccessRateByProject: jest.fn(), @@ -27,6 +27,7 @@ describe('LogController 테스트', () => { getTrafficByProject: jest.fn(), getTrafficDailyDifferenceByGeneration: jest.fn(), getDAUByProject: jest.fn(), + getSpeedRank: jest.fn(), }; beforeEach(async () => { @@ -57,9 +58,10 @@ describe('LogController 테스트', () => { }; it('평균 응답 시간을 ProjectResponseDto 형식으로 반환해야 한다', async () => { - mockLogService.elapsedTime.mockResolvedValue(mockResult); + mockLogService.getAvgElapsedTime.mockResolvedValue(mockResult); - const result = await controller.elapsedTime( + + const result = await controller.getElapsedTime( plainToInstance(GetAvgElapsedTimeDto, { generation: 1 }), ); @@ -85,7 +87,7 @@ describe('LogController 테스트', () => { it('TOP 5 트래픽 순위를 ProjectResponseDto 형식으로 반환해야 한다', async () => { mockLogService.trafficRank.mockResolvedValue(mockResult); - const result = (await controller.trafficRank( + const result = (await controller.getTrafficRank( plainToInstance(GetTrafficRankDto, { generation: 1 }), )) as unknown as TrafficRankResponseType; @@ -343,4 +345,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.controller.ts b/backend/console-server/src/log/log.controller.ts index 7a646778..3d6f1527 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'; import { GetTrafficRankDto } from './dto/get-traffic-rank.dto'; import { GetAvgElapsedTimeDto } from './dto/get-avg-elapsed-time.dto'; import { GetTrafficTop5ChartResponseDto } from './dto/get-traffic-top5-chart-response.dto'; @@ -37,7 +39,7 @@ export class LogController { description: '평균 응답시간이 성공적으로 반환됨.', type: GetAvgElapsedTimeResponseDto, }) - async elapsedTime(@Query() getAvgElapsedTimeDto: GetAvgElapsedTimeDto) { + async getElapsedTime(@Query() getAvgElapsedTimeDto: GetAvgElapsedTimeDto) { return await this.logService.getAvgElapsedTime(getAvgElapsedTimeDto); } @@ -52,10 +54,25 @@ export class LogController { description: '트래픽 랭킹 TOP 5가 정상적으로 반환됨.', type: GetTrafficRankResponseDto, }) - async trafficRank(@Query() getTrafficRankDto: GetTrafficRankDto) { + async getTrafficRank(@Query() getTrafficRankDto: GetTrafficRankDto) { return await this.logService.getTrafficRank(getTrafficRankDto); } + @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.spec.ts b/backend/console-server/src/log/log.repository.spec.ts index 8c316620..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'; @@ -207,7 +155,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 +169,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 +182,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', ); @@ -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'; @@ -281,7 +229,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 +243,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 +258,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 +273,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', ); diff --git a/backend/console-server/src/log/log.repository.ts b/backend/console-server/src/log/log.repository.ts index 03b140d0..648156dc 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'; import { TrafficChartMetric } from './metric/trafficChart.metric'; @Injectable() @@ -139,7 +140,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') @@ -154,7 +155,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' }, @@ -171,7 +172,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') @@ -179,10 +180,22 @@ 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)); + } + async findTrafficTop5Chart() { const now = new Date(); const today = new Date(now.setHours(0, 0, 0, 0)); 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, ); diff --git a/backend/console-server/src/log/log.service.ts b/backend/console-server/src/log/log.service.ts index 59edc7e2..a270b7d9 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'; import { GetTrafficRankDto } from './dto/get-traffic-rank.dto'; import { GetAvgElapsedTimeDto } from './dto/get-avg-elapsed-time.dto'; import { @@ -140,7 +142,7 @@ export class LogService { if (!project) throw new NotFoundException(`Project with name ${projectName} not found`); 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, @@ -168,7 +170,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, @@ -187,7 +189,7 @@ 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, @@ -195,6 +197,24 @@ export class LogService { }); } + 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); + } + async getTrafficTop5Chart(_getTrafficTop5ChartDto: GetTrafficTop5ChartDto) { const results = await this.logRepository.findTrafficTop5Chart(); 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; +}