Skip to content

Commit d9e2be8

Browse files
Merge pull request #188 from boostcampwm-2024/dev-back
merge main
2 parents 049be30 + 6af31d2 commit d9e2be8

20 files changed

+801
-29
lines changed

backend/console-server/src/log/elapsed-time/elapsed-time.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export class ElapsedTimeController {
4848
@HttpCode(HttpStatus.OK)
4949
@ApiOperation({
5050
summary: '개별 프로젝트의 경로별 응답 속도 순위',
51-
description: '개별 프로젝트의 경로별 응답 속도 중 가장 빠른/느린 3개를 반환합니다.',
51+
description:
52+
'개별 프로젝트의 경로별 응답 속도 중 가장 빠른/느린 3개를 반환합니다. 빠른 응답은 유효한(상태 코드 200번대)만을 포함합니다.',
5253
})
5354
@ApiResponse({
5455
status: HttpStatus.OK,

backend/console-server/src/log/elapsed-time/elapsed-time.repository.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,16 @@ export class ElapsedTimeRepository {
3333
return results.map((result) => plainToInstance(HostAvgElapsedTimeMetric, result));
3434
}
3535

36-
async getFastestPathsByDomain(domain: string) {
36+
async findFastestPathsByDomain(domain: string) {
3737
const { query, params } = new TimeSeriesQueryBuilder()
38-
.metrics([{ name: 'elapsed_time', aggregation: 'avg' }, { name: 'path' }])
38+
.metrics([
39+
{ name: 'toUInt32(avg(elapsed_time)) as avg_elapsed_time' },
40+
{ name: 'path' },
41+
{ name: 'if(status_code >= 200 AND status_code < 300, 1, 0) AS is_valid' },
42+
])
3943
.from('http_log')
40-
.filter({ host: domain })
41-
.groupBy(['path'])
44+
.filter({ host: domain, is_valid: 1 })
45+
.groupBy(['path', 'is_valid'])
4246
.orderBy(['avg_elapsed_time'], false)
4347
.limit(3)
4448
.build();
@@ -50,7 +54,10 @@ export class ElapsedTimeRepository {
5054

5155
async findSlowestPathsByDomain(domain: string) {
5256
const { query, params } = new TimeSeriesQueryBuilder()
53-
.metrics([{ name: 'elapsed_time', aggregation: 'avg' }, { name: 'path' }])
57+
.metrics([
58+
{ name: 'toUInt32(avg(elapsed_time)) as avg_elapsed_time' },
59+
{ name: 'path' },
60+
])
5461
.from('http_log')
5562
.filter({ host: domain })
5663
.groupBy(['path'])

backend/console-server/src/log/elapsed-time/elapsed-time.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class ElapsedTimeService {
5656

5757
if (!project) throw new NotFoundException(`Project with name ${projectName} not found`);
5858

59-
const fastestPaths = await this.elapsedTimeRepository.getFastestPathsByDomain(
59+
const fastestPaths = await this.elapsedTimeRepository.findFastestPathsByDomain(
6060
project.domain,
6161
);
6262
const slowestPaths = await this.elapsedTimeRepository.findSlowestPathsByDomain(
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNumber, IsString } from 'class-validator';
3+
import { Expose, Type } from 'class-transformer';
4+
5+
export class DAURank {
6+
@IsString()
7+
projectName: string;
8+
9+
@Type(() => Number)
10+
@IsNumber()
11+
dau: number;
12+
}
13+
14+
export class GetDAURankResponseDto {
15+
@ApiProperty({
16+
description: '총 갯수',
17+
example: 30,
18+
})
19+
@IsNumber()
20+
total: number;
21+
22+
@ApiProperty({
23+
description: 'dau 순위',
24+
example: [
25+
{
26+
projectName: 'test059',
27+
dau: 12345,
28+
},
29+
{
30+
projectName: 'test007',
31+
dau: 234234,
32+
},
33+
{
34+
projectName: 'test079',
35+
dau: 21212,
36+
},
37+
],
38+
})
39+
@Expose()
40+
rank: DAURank[];
41+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty } from 'class-validator';
3+
import { Type } from 'class-transformer';
4+
5+
export class GetDAURankDto {
6+
@ApiProperty({ description: '기수', example: 9 })
7+
@Type(() => Number)
8+
@IsNotEmpty()
9+
generation: number;
10+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { Expose } from 'class-transformer';
3+
import { IsNumber, IsString } from 'class-validator';
4+
5+
export class ElapsedTimeRank {
6+
@IsString()
7+
@Expose()
8+
projectName: string;
9+
10+
@IsNumber()
11+
@Expose()
12+
elapsedTime: number;
13+
}
14+
15+
export class GetElapsedTimeRankResponseDto {
16+
@ApiProperty({
17+
description: '총 갯수',
18+
example: 30,
19+
})
20+
@IsNumber()
21+
total: number;
22+
23+
@ApiProperty({
24+
description: '응답 소요 시간 짧은 순으로 정렬된 프로젝트명과 시간(ms)',
25+
example: [
26+
{
27+
projectName: 'test059',
28+
elapsedTime: 100,
29+
},
30+
{
31+
projectName: 'test007',
32+
elapsedTime: 110,
33+
},
34+
{
35+
projectName: 'test079',
36+
elapsedTime: 120,
37+
},
38+
],
39+
})
40+
@Expose()
41+
rank: ElapsedTimeRank[];
42+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Type } from 'class-transformer';
2+
import { IsNumber } from 'class-validator';
3+
import { ApiProperty } from '@nestjs/swagger';
4+
5+
export class GetElapsedTimeRankDto {
6+
@ApiProperty({ description: '기수', example: 9 })
7+
@Type(() => Number)
8+
@IsNumber()
9+
generation: number;
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { IsNumber, IsString } from 'class-validator';
2+
import { Expose, Type } from 'class-transformer';
3+
4+
export class HostDauMetric {
5+
@IsString()
6+
@Expose()
7+
host: string;
8+
9+
@Type(() => Number)
10+
@IsNumber()
11+
@Expose()
12+
dau: number;
13+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type HostElapsedTimeMetric = {
2+
host: string;
3+
avg_elapsed_time: number;
4+
};

backend/console-server/src/log/rank/rank.controller.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ import { Test } from '@nestjs/testing';
33
import { RankController } from './rank.controller';
44
import { RankService } from './rank.service';
55
import type { GetSuccessRateRankDto } from './dto/get-success-rate-rank.dto';
6+
import type { GetDAURankDto } from './dto/get-dau-rank.dto';
67
import type { GetSuccessRateRankResponseDto } from './dto/get-success-rate-rank-response.dto';
78
import type { GetTrafficRankDto } from './dto/get-traffic-rank.dto';
89
import type { GetTrafficRankResponseDto } from './dto/get-traffic-rank-response.dto';
10+
import type { GetElapsedTimeRankDto } from './dto/get-elapsed-time-rank.dto';
911

1012
describe('RankController', () => {
1113
let controller: RankController;
1214
let service: RankService;
1315

1416
const mockRankService = {
1517
getSuccessRateRank: jest.fn(),
18+
getElapsedTimeRank: jest.fn(),
19+
getDAURank: jest.fn(),
1620
getTrafficRank: jest.fn(),
1721
};
1822

@@ -83,6 +87,41 @@ describe('RankController', () => {
8387
});
8488
});
8589

90+
describe('getDAURank()는', () => {
91+
const mockDto: GetDAURankDto = {
92+
generation: 9,
93+
};
94+
95+
const mockResponse = {
96+
total: 2,
97+
rank: [
98+
{
99+
projectName: 'Project A',
100+
dau: 1000,
101+
},
102+
{
103+
projectName: 'Project B',
104+
dau: 500,
105+
},
106+
],
107+
date: '2024-01-01',
108+
};
109+
110+
it('정의되어 있어야 한다', () => {
111+
expect(controller.getDAURank).toBeDefined();
112+
});
113+
114+
it('DAU 랭킹 데이터를 반환해야 한다', async () => {
115+
mockRankService.getDAURank.mockResolvedValue(mockResponse);
116+
117+
const result = await controller.getDAURank(mockDto);
118+
119+
expect(result).toBe(mockResponse);
120+
expect(service.getDAURank).toHaveBeenCalledWith(mockDto);
121+
expect(service.getDAURank).toHaveBeenCalledTimes(1);
122+
});
123+
});
124+
86125
describe('getTrafficRank()는', () => {
87126
let mockDto: GetTrafficRankDto;
88127
let mockResponse: GetTrafficRankResponseDto;
@@ -123,11 +162,59 @@ describe('RankController', () => {
123162

124163
it('서비스 계층에서 오류가 발생하면 해당 오류를 그대로 던져야 한다', async () => {
125164
const error = new Error('Service error');
165+
166+
mockRankService.getDAURank.mockRejectedValue(error);
167+
168+
await expect(controller.getDAURank(mockDto)).rejects.toThrow(error);
169+
expect(service.getDAURank).toHaveBeenCalledWith(mockDto);
170+
126171
mockRankService.getTrafficRank.mockRejectedValue(error);
127172

128173
await expect(controller.getTrafficRank(mockDto)).rejects.toThrow(error);
129174
expect(service.getTrafficRank).toHaveBeenCalledWith(mockDto);
130175
});
131176
});
177+
178+
describe('getElapsedTimeRank()는', () => {
179+
const mockDto: GetElapsedTimeRankDto = {
180+
generation: 1,
181+
};
182+
183+
const mockResponse = {
184+
total: 2,
185+
rank: [
186+
{
187+
projectName: 'Project A',
188+
elapsedTime: 120,
189+
},
190+
{
191+
projectName: 'Project B',
192+
elapsedTime: 130,
193+
},
194+
],
195+
};
196+
197+
it('정의되어 있어야 한다', () => {
198+
expect(controller.getElapsedTimeRank).toBeDefined();
199+
});
200+
201+
it('응답 소요 시간 랭킹 데이터를 반환해야 한다', async () => {
202+
mockRankService.getElapsedTimeRank.mockResolvedValue(mockResponse);
203+
204+
const result = await controller.getElapsedTimeRank(mockDto);
205+
206+
expect(result).toBe(mockResponse);
207+
expect(service.getElapsedTimeRank).toHaveBeenCalledWith(mockDto);
208+
expect(service.getElapsedTimeRank).toHaveBeenCalledTimes(1);
209+
});
210+
211+
it('서비스 계층에서 오류가 발생하면 해당 오류를 그대로 던져야 한다', async () => {
212+
const error = new Error('Service error');
213+
mockRankService.getElapsedTimeRank.mockRejectedValue(error);
214+
215+
await expect(controller.getElapsedTimeRank(mockDto)).rejects.toThrow(error);
216+
expect(service.getElapsedTimeRank).toHaveBeenCalledWith(mockDto);
217+
});
218+
});
132219
});
133220
});

0 commit comments

Comments
 (0)