Skip to content

Commit 07b87ac

Browse files
Merge pull request #213 from boostcampwm-2024/dev-back
[BE] Merge to main
2 parents e6b1fd4 + 908d716 commit 07b87ac

File tree

9 files changed

+207
-136
lines changed

9 files changed

+207
-136
lines changed

backend/console-server/src/log/rank/dto/get-success-rate-rank-response.dto.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { ApiProperty } from '@nestjs/swagger';
2-
import { Expose, Type } from 'class-transformer';
2+
import { Expose, Transform, Type } from 'class-transformer';
33
import { IsNumber, IsString } from 'class-validator';
44

55
export class SuccessRateRank {
66
@IsString()
77
projectName: string;
88

99
@Type(() => Number)
10+
@Transform(({ value }) => Math.floor(value))
1011
@IsNumber()
1112
value: number;
1213
}
@@ -24,15 +25,15 @@ export class GetSuccessRateRankResponseDto {
2425
example: [
2526
{
2627
projectName: 'test059',
27-
value: 98.23100936524453,
28+
value: 98,
2829
},
2930
{
3031
projectName: 'test007',
31-
value: 98.1094527363184,
32+
value: 98,
3233
},
3334
{
3435
projectName: 'test079',
35-
value: 98.0083857442348,
36+
value: 98,
3637
},
3738
],
3839
})

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

Lines changed: 94 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { HostElapsedTimeMetric } from './metric/host-elapsed-time.metric';
77

88
jest.mock('../../clickhouse/query-builder/time-series.query-builder');
99

10-
describe('RankRepository', () => {
10+
describe('RankRepository의', () => {
1111
let repository: RankRepository;
1212
let clickhouse: Clickhouse;
1313

@@ -34,129 +34,129 @@ describe('RankRepository', () => {
3434
jest.clearAllMocks();
3535
});
3636

37-
describe('RankRepository의', () => {
38-
describe('findSuccessRateOrderByCount()는', () => {
39-
const mockQueryResult = {
40-
query: 'SELECT host, is_error FROM http_log GROUP BY host ORDER BY is_error_rate',
41-
params: {},
42-
};
37+
describe('findSuccessRateOrderByCount()는', () => {
38+
const mockDate = '2024-11-25';
39+
const mockQueryResult = {
40+
query: 'SELECT host, is_error FROM http_log GROUP BY host ORDER BY is_error_rate',
41+
params: {},
42+
};
4343

44-
const mockResults = [
45-
{ host: 'test1.com', is_error_rate: 10 },
46-
{ host: 'test2.com', is_error_rate: 20 },
47-
];
44+
const mockResults = [
45+
{ host: 'test1.com', is_error_rate: 10 },
46+
{ host: 'test2.com', is_error_rate: 20 },
47+
];
4848

49-
beforeEach(() => {
50-
(TimeSeriesQueryBuilder as jest.Mock).mockImplementation(() => ({
51-
metrics: jest.fn().mockReturnThis(),
52-
from: jest.fn().mockReturnThis(),
53-
groupBy: jest.fn().mockReturnThis(),
54-
orderBy: jest.fn().mockReturnThis(),
55-
build: jest.fn().mockReturnValue(mockQueryResult),
56-
}));
57-
});
49+
beforeEach(() => {
50+
(TimeSeriesQueryBuilder as jest.Mock).mockImplementation(() => ({
51+
metrics: jest.fn().mockReturnThis(),
52+
from: jest.fn().mockReturnThis(),
53+
filter: jest.fn().mockReturnThis(),
54+
groupBy: jest.fn().mockReturnThis(),
55+
orderBy: jest.fn().mockReturnThis(),
56+
build: jest.fn().mockReturnValue(mockQueryResult),
57+
}));
58+
});
5859

59-
it('TimeSeriesQueryBuilder를 사용하여 쿼리를 생성해야 한다', async () => {
60-
mockClickhouse.query.mockResolvedValue(mockResults);
60+
it('TimeSeriesQueryBuilder를 사용하여 쿼리를 생성해야 한다', async () => {
61+
mockClickhouse.query.mockResolvedValue(mockResults);
6162

62-
await repository.findSuccessRateOrderByCount();
63+
await repository.findSuccessRateOrderByCount(mockDate);
6364

64-
expect(TimeSeriesQueryBuilder).toHaveBeenCalled();
65-
});
65+
expect(TimeSeriesQueryBuilder).toHaveBeenCalled();
66+
});
6667

67-
it('생성된 쿼리로 Clickhouse를 호출해야 한다', async () => {
68-
mockClickhouse.query.mockResolvedValue(mockResults);
68+
it('생성된 쿼리로 Clickhouse를 호출해야 한다', async () => {
69+
mockClickhouse.query.mockResolvedValue(mockResults);
6970

70-
await repository.findSuccessRateOrderByCount();
71+
await repository.findSuccessRateOrderByCount(mockDate);
7172

72-
expect(clickhouse.query).toHaveBeenCalledWith(
73-
mockQueryResult.query,
74-
mockQueryResult.params,
75-
);
76-
});
73+
expect(clickhouse.query).toHaveBeenCalledWith(
74+
mockQueryResult.query,
75+
mockQueryResult.params,
76+
);
77+
});
7778

78-
it('조회 결과는 HostErrorRateMetric 객체 타입을 가져야한다', async () => {
79-
mockClickhouse.query.mockResolvedValue(mockResults);
79+
it('조회 결과는 HostErrorRateMetric 객체 타입을 가져야한다', async () => {
80+
mockClickhouse.query.mockResolvedValue(mockResults);
8081

81-
const results = await repository.findSuccessRateOrderByCount();
82+
const results = await repository.findSuccessRateOrderByCount(mockDate);
8283

83-
expect(results).toHaveLength(mockResults.length);
84-
results.forEach((result, index) => {
85-
expect(typeof result.host).toBe('string');
86-
expect(typeof result.is_error_rate).toBe('number');
87-
expect(result.host).toBe(mockResults[index].host);
88-
expect(result.is_error_rate).toBe(mockResults[index].is_error_rate);
89-
});
84+
expect(results).toHaveLength(mockResults.length);
85+
results.forEach((result, index) => {
86+
expect(typeof result.host).toBe('string');
87+
expect(typeof result.is_error_rate).toBe('number');
88+
expect(result.host).toBe(mockResults[index].host);
89+
expect(result.is_error_rate).toBe(mockResults[index].is_error_rate);
9090
});
91+
});
9192

92-
it('Clickhouse 쿼리 실패 시 에러를 전파해야 한다', async () => {
93-
const error = new Error('Clickhouse query failed');
94-
mockClickhouse.query.mockRejectedValue(error);
93+
it('Clickhouse 쿼리 실패 시 에러를 전파해야 한다', async () => {
94+
const error = new Error('Clickhouse query failed');
95+
mockClickhouse.query.mockRejectedValue(error);
9596

96-
await expect(repository.findSuccessRateOrderByCount()).rejects.toThrow(error);
97-
});
97+
await expect(repository.findSuccessRateOrderByCount(mockDate)).rejects.toThrow(error);
9898
});
99+
});
99100

100-
describe('findCountOrderByCount()는', () => {
101-
const mockQueryResult = {
102-
query: 'SELECT host, count() as count FROM http_log GROUP BY host ORDER BY count',
103-
params: {},
104-
};
101+
describe('findCountOrderByCount()는', () => {
102+
const mockQueryResult = {
103+
query: 'SELECT host, count() as count FROM http_log GROUP BY host ORDER BY count',
104+
params: {},
105+
};
105106

106-
const mockResults = [
107-
{ host: 'test1.com', count: 9999 },
108-
{ host: 'test2.com', count: 9898 },
109-
];
107+
const mockResults = [
108+
{ host: 'test1.com', count: 9999 },
109+
{ host: 'test2.com', count: 9898 },
110+
];
110111

111-
beforeEach(() => {
112-
(TimeSeriesQueryBuilder as jest.Mock).mockImplementation(() => ({
113-
metrics: jest.fn().mockReturnThis(),
114-
from: jest.fn().mockReturnThis(),
115-
groupBy: jest.fn().mockReturnThis(),
116-
orderBy: jest.fn().mockReturnThis(),
117-
build: jest.fn().mockReturnValue(mockQueryResult),
118-
}));
119-
});
112+
beforeEach(() => {
113+
(TimeSeriesQueryBuilder as jest.Mock).mockImplementation(() => ({
114+
metrics: jest.fn().mockReturnThis(),
115+
from: jest.fn().mockReturnThis(),
116+
groupBy: jest.fn().mockReturnThis(),
117+
orderBy: jest.fn().mockReturnThis(),
118+
build: jest.fn().mockReturnValue(mockQueryResult),
119+
}));
120+
});
120121

121-
it('TimeSeriesQueryBuilder를 사용하여 쿼리를 생성해야 한다', async () => {
122-
mockClickhouse.query.mockResolvedValue(mockResults);
122+
it('TimeSeriesQueryBuilder를 사용하여 쿼리를 생성해야 한다', async () => {
123+
mockClickhouse.query.mockResolvedValue(mockResults);
123124

124-
await repository.findCountOrderByCount();
125+
await repository.findCountOrderByCount();
125126

126-
expect(TimeSeriesQueryBuilder).toHaveBeenCalled();
127-
});
127+
expect(TimeSeriesQueryBuilder).toHaveBeenCalled();
128+
});
128129

129-
it('생성된 쿼리로 Clickhouse를 호출해야 한다', async () => {
130-
mockClickhouse.query.mockResolvedValue(mockResults);
130+
it('생성된 쿼리로 Clickhouse를 호출해야 한다', async () => {
131+
mockClickhouse.query.mockResolvedValue(mockResults);
131132

132-
await repository.findCountOrderByCount();
133+
await repository.findCountOrderByCount();
133134

134-
expect(clickhouse.query).toHaveBeenCalledWith(
135-
mockQueryResult.query,
136-
mockQueryResult.params,
137-
);
138-
});
135+
expect(clickhouse.query).toHaveBeenCalledWith(
136+
mockQueryResult.query,
137+
mockQueryResult.params,
138+
);
139+
});
139140

140-
it('조회 결과는 HostCountMetric 객체 타입을 가져야한다', async () => {
141-
mockClickhouse.query.mockResolvedValue(mockResults);
141+
it('조회 결과는 HostCountMetric 객체 타입을 가져야한다', async () => {
142+
mockClickhouse.query.mockResolvedValue(mockResults);
142143

143-
const results = await repository.findCountOrderByCount();
144+
const results = await repository.findCountOrderByCount();
144145

145-
expect(results).toHaveLength(mockResults.length);
146-
results.forEach((result, index) => {
147-
expect(typeof result.host).toBe('string');
148-
expect(typeof result.count).toBe('number');
149-
expect(result.host).toBe(mockResults[index].host);
150-
expect(result.count).toBe(mockResults[index].count);
151-
});
146+
expect(results).toHaveLength(mockResults.length);
147+
results.forEach((result, index) => {
148+
expect(typeof result.host).toBe('string');
149+
expect(typeof result.count).toBe('number');
150+
expect(result.host).toBe(mockResults[index].host);
151+
expect(result.count).toBe(mockResults[index].count);
152152
});
153+
});
153154

154-
it('Clickhouse 쿼리 실패 시 에러를 전파해야 한다', async () => {
155-
const error = new Error('Clickhouse query failed');
156-
mockClickhouse.query.mockRejectedValue(error);
155+
it('Clickhouse 쿼리 실패 시 에러를 전파해야 한다', async () => {
156+
const error = new Error('Clickhouse query failed');
157+
mockClickhouse.query.mockRejectedValue(error);
157158

158-
await expect(repository.findCountOrderByCount()).rejects.toThrow(error);
159-
});
159+
await expect(repository.findCountOrderByCount()).rejects.toThrow(error);
160160
});
161161
});
162162

backend/console-server/src/log/rank/rank.repository.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ import { HostCountMetric } from './metric/host-count.metric';
1010
export class RankRepository {
1111
constructor(private readonly clickhouse: Clickhouse) {}
1212

13-
async findSuccessRateOrderByCount() {
13+
async findSuccessRateOrderByCount(date: string) {
1414
const { query, params } = new TimeSeriesQueryBuilder()
15-
.metrics([{ name: 'host' }, { name: 'is_error', aggregation: 'rate' }])
15+
.metrics([
16+
{ name: 'host' },
17+
{ name: 'is_error', aggregation: 'rate' },
18+
{ name: 'toDate(timestamp) as timestamp' },
19+
])
1620
.from('http_log')
17-
.groupBy(['host'])
21+
.filter({ timestamp: date })
22+
.groupBy(['host', 'timestamp'])
1823
.orderBy(['is_error_rate'])
1924
.build();
2025

backend/console-server/src/log/rank/rank.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ export class RankService {
2828
) {}
2929

3030
async getSuccessRateRank(_getSuccessRateRankDto: GetSuccessRateRankDto) {
31-
const results = await this.rankRepository.findSuccessRateOrderByCount();
31+
const results = await this.rankRepository.findSuccessRateOrderByCount(
32+
this.getYesterdayDateString(),
33+
);
3234
const hosts = results.map((result) => result.host);
3335

3436
const projectMap = await this.hostsToProjectMap(hosts);
Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { Expose } from 'class-transformer';
3-
import { TrafficRankMetric } from '../metric/traffic-rank.metric';
3+
import { IsNotEmpty, IsNumber, IsString, ValidateNested } from 'class-validator';
4+
5+
export class TrafficRankData {
6+
@IsNotEmpty()
7+
@IsString()
8+
projectName: string;
9+
10+
@IsNotEmpty()
11+
@IsNumber()
12+
count: number;
13+
}
414

515
export class GetTrafficTop5ResponseDto {
616
@ApiProperty({
717
example: [
8-
{ host: 'watchducks01', count: 100 },
9-
{ host: 'watchducks02', count: 99 },
10-
{ host: 'watchducks03', count: 98 },
11-
{ host: 'watchducks04', count: 97 },
12-
{ host: 'watchducks05', count: 96 },
18+
{ projectName: 'watchducks01', count: 100 },
19+
{ projectName: 'watchducks02', count: 99 },
20+
{ projectName: 'watchducks03', count: 98 },
21+
{ projectName: 'watchducks04', count: 97 },
22+
{ projectName: 'watchducks05', count: 96 },
1323
],
1424
description: '트래픽 수가 가장 많은 Top5 호스트, 트래픽 수',
1525
})
1626
@Expose()
17-
rank: TrafficRankMetric[];
27+
@ValidateNested()
28+
rank: TrafficRankData[];
1829
}

backend/console-server/src/log/traffic/metric/traffic-rank-top5.metric.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

backend/console-server/src/log/traffic/traffic.repository.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { plainToInstance } from 'class-transformer';
33
import { Clickhouse } from '../../clickhouse/clickhouse';
44
import { TimeSeriesQueryBuilder } from '../../clickhouse/query-builder/time-series.query-builder';
55
import { TrafficRankMetric } from './metric/traffic-rank.metric';
6-
import { TrafficRankTop5Metric } from './metric/traffic-rank-top5.metric';
76
import { TrafficCountMetric } from './metric/traffic-count.metric';
87
import { Injectable } from '@nestjs/common';
98
import type { TimeUnit } from './traffic.constant';
@@ -29,12 +28,7 @@ export class TrafficRepository {
2928
.limit(5)
3029
.build();
3130

32-
const results = await this.clickhouse.query<TrafficRankMetric>(query, params);
33-
34-
return plainToInstance(
35-
TrafficRankTop5Metric,
36-
results.map((result) => plainToInstance(TrafficRankMetric, result)),
37-
);
31+
return await this.clickhouse.query<TrafficRankMetric>(query, params);
3832
}
3933

4034
async findTrafficByGeneration() {

0 commit comments

Comments
 (0)