Skip to content

Commit

Permalink
Merge branch 'dev-back' into be-feat#135
Browse files Browse the repository at this point in the history
  • Loading branch information
sjy2335 authored Nov 21, 2024
2 parents 4ef60ab + 0be45d6 commit 41114e3
Show file tree
Hide file tree
Showing 23 changed files with 505 additions and 129 deletions.
24 changes: 12 additions & 12 deletions backend/console-server/src/app.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
let appController: AppController;
let appController: AppController;

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();

appController = app.get<AppController>(AppController);
});
appController = app.get<AppController>(AppController);
});

describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});
});
16 changes: 16 additions & 0 deletions backend/console-server/src/log/dto/get-avg-elapsed-time.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IsNotEmpty, IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Expose, Type } from 'class-transformer';

export class GetAvgElapsedTimeDto {
@IsNotEmpty()
@IsNumber()
@ApiProperty({
example: 9,
description: '기수',
type: 'number',
})
@Type(() => Number)
@Expose()
generation: number;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';

export class GetTrafficDailyDifferenceResponseDto {
@ApiProperty({
example: '+9100',
description: '전일 대비 총 트래픽 증감량',
type: String,
})
@IsString()
@IsNotEmpty()
@Expose()
traffic_daily_difference: string;
}
16 changes: 16 additions & 0 deletions backend/console-server/src/log/dto/get-traffic-rank.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose, Type } from 'class-transformer';
import { IsNotEmpty, IsNumber } from 'class-validator';

export class GetTrafficRankDto {
@ApiProperty({
example: 9,
description: '기수',
type: 'number',
})
@Type(() => Number)
@IsNumber()
@IsNotEmpty()
@Expose()
generation: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Expose } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

export class TrafficTop5Chart {
name: string;
traffic: [string, string][];
}

export class GetTrafficTop5ChartResponseDto {
@ApiProperty({
example: [
{
name: 'watchducks',
traffic: [
['2024-01-01 11:12:00', '100'],
['2024-01-02 11:13:00', '100'],
['2024-01-02 11:14:00', '100'],
['2024-01-02 11:15:00', '100'],
],
},
],
description: '해당 기수의 트래픽 Top5 프로젝트에 대한 작일 차트 데이터',
})
@Expose()
trafficCharts: TrafficTop5Chart[];
}
14 changes: 14 additions & 0 deletions backend/console-server/src/log/dto/get-traffic-top5-chart.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IsNumber } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

export class GetTrafficTop5ChartDto {
@IsNumber()
@Type(() => Number)
@ApiProperty({
description: '기수',
example: 9,
required: true,
})
generation: number;
}
14 changes: 11 additions & 3 deletions backend/console-server/src/log/log.contorller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { HttpStatus } from '@nestjs/common';
import type { TestingModule } from '@nestjs/testing';
import { LogController } from './log.controller';
import { LogService } from './log.service';
import { GetTrafficRankDto } from './dto/get-traffic-rank.dto';
import { plainToInstance } from 'class-transformer';
import { GetAvgElapsedTimeDto } from './dto/get-avg-elapsed-time.dto';

interface TrafficRankResponseType {
status: number;
data: Array<{ host: string; count: number }>;
Expand Down Expand Up @@ -56,7 +60,10 @@ describe('LogController 테스트', () => {
it('평균 응답 시간을 ProjectResponseDto 형식으로 반환해야 한다', async () => {
mockLogService.getAvgElapsedTime.mockResolvedValue(mockResult);

const result = await controller.getElapsedTime();

const result = await controller.getElapsedTime(
plainToInstance(GetAvgElapsedTimeDto, { generation: 1 }),
);

expect(result).toEqual(mockResult);
expect(result).toHaveProperty('status', HttpStatus.OK);
Expand All @@ -80,8 +87,9 @@ describe('LogController 테스트', () => {
it('TOP 5 트래픽 순위를 ProjectResponseDto 형식으로 반환해야 한다', async () => {
mockLogService.trafficRank.mockResolvedValue(mockResult);

const result =
(await controller.getTrafficRank()) as unknown as TrafficRankResponseType;
const result = (await controller.getTrafficRank(
plainToInstance(GetTrafficRankDto, { generation: 1 }),
)) as unknown as TrafficRankResponseType;

expect(result).toEqual(mockResult);
expect(result).toHaveProperty('status', HttpStatus.OK);
Expand Down
31 changes: 25 additions & 6 deletions backend/console-server/src/log/log.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { GetTrafficDailyDifferenceResponseDto } from './dto/get-traffic-daily-di
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';
import { GetTrafficTop5ChartDto } from './dto/get-traffic-top5-chart.dto';

@Controller('log')
export class LogController {
Expand All @@ -35,8 +39,8 @@ export class LogController {
description: '평균 응답시간이 성공적으로 반환됨.',
type: GetAvgElapsedTimeResponseDto,
})
async getElapsedTime() {
return await this.logService.getAvgElapsedTime();
async getElapsedTime(@Query() getAvgElapsedTimeDto: GetAvgElapsedTimeDto) {
return await this.logService.getAvgElapsedTime(getAvgElapsedTimeDto);
}

@Get('/traffic/rank')
Expand All @@ -50,8 +54,8 @@ export class LogController {
description: '트래픽 랭킹 TOP 5가 정상적으로 반환됨.',
type: GetTrafficRankResponseDto,
})
async getTrafficRank() {
return await this.logService.getTrafficRank();
async getTrafficRank(@Query() getTrafficRankDto: GetTrafficRankDto) {
return await this.logService.getTrafficRank(getTrafficRankDto);
}

@Get('/elapsed-time/top5')
Expand Down Expand Up @@ -80,7 +84,7 @@ export class LogController {
description: '기수 내 응답 성공률이 성공적으로 반환됨.',
type: GetSuccessRateResponseDto,
})
async getResponseSuccessRate(getSuccessRateDto: GetSuccessRateDto) {
async getResponseSuccessRate(@Query() getSuccessRateDto: GetSuccessRateDto) {
return await this.logService.getResponseSuccessRate(getSuccessRateDto);
}

Expand Down Expand Up @@ -143,7 +147,7 @@ export class LogController {
type: GetTrafficDailyDifferenceResponseDto,
})
async getTrafficDailyDifferenceByGeneration(
getTrafficDailyDifferenceDto: GetTrafficDailyDifferenceDto,
@Query() getTrafficDailyDifferenceDto: GetTrafficDailyDifferenceDto,
) {
return await this.logService.getTrafficDailyDifferenceByGeneration(
getTrafficDailyDifferenceDto,
Expand Down Expand Up @@ -179,4 +183,19 @@ export class LogController {
async getDAUByProject(@Query() getDAUByProjectDto: GetDAUByProjectDto) {
return await this.logService.getDAUByProject(getDAUByProjectDto);
}

@Get('/traffic/top5/line-chart')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '프로젝트 트래픽 TOP 5에 대한 트래픽 데이터 조회',
description: '프로젝트별 작일 데이터 전체 타임스탬프를 반환',
})
@ApiResponse({
status: HttpStatus.OK,
description: '프로젝트별 작일 데이터 전체 타임스탬프가 정상적으로 반환됨',
type: GetTrafficTop5ChartResponseDto,
})
async getTrafficTop5Chart(@Query() getTrafficTop5ChartDto: GetTrafficTop5ChartDto) {
return await this.logService.getTrafficTop5Chart(getTrafficTop5ChartDto);
}
}
48 changes: 48 additions & 0 deletions backend/console-server/src/log/log.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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()
export class LogRepository {
Expand Down Expand Up @@ -182,6 +183,7 @@ export class LogRepository {
return result?.dau ? result.dau : 0;
}


async findSpeedRank() {
const { query, params } = new TimeSeriesQueryBuilder()
.metrics([{ name: 'elapsed_time', aggregation: 'avg' }, { name: 'host' }])
Expand All @@ -193,4 +195,50 @@ export class LogRepository {
const results = await this.clickhouse.query<SpeedRankMetric>(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));
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);

const query = `WITH top_hosts AS (
SELECT host
FROM http_log
WHERE timestamp >= {startTime: DateTime64(3)}
AND timestamp < {endTime: DateTime64(3)}
GROUP BY host
ORDER BY count() DESC
LIMIT 5
)
SELECT
host,
groupArray(
(
toDateTime64(toStartOfInterval(timestamp, INTERVAL 1 MINUTE), 0),
requests_count
)
) as traffic
FROM (
SELECT
host,
toDateTime64(toStartOfInterval(timestamp, INTERVAL 1 MINUTE), 0) as timestamp,
count() as requests_count
FROM http_log
WHERE timestamp >= {startTime: DateTime64(3)}
AND timestamp < {endTime: DateTime64(3)}
AND host IN (SELECT host FROM top_hosts)
GROUP BY
host,
timestamp
ORDER BY
timestamp
)
GROUP BY host;`;
const params = { startTime: yesterday, endTime: today };
const results = await this.clickhouse.query<TrafficChartMetric>(query, params);

return results.map((result) => {
return plainToInstance(TrafficChartMetric, result);
});
}
}
35 changes: 33 additions & 2 deletions backend/console-server/src/log/log.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ import { GetTrafficDailyDifferenceDto } from './dto/get-traffic-daily-difference
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 {
GetTrafficTop5ChartResponseDto,
TrafficTop5Chart,
} from './dto/get-traffic-top5-chart-response.dto';
import { GetTrafficTop5ChartDto } from './dto/get-traffic-top5-chart.dto';

@Injectable()
export class LogService {
Expand All @@ -37,13 +44,13 @@ export class LogService {
private readonly logRepository: LogRepository,
) {}

async getAvgElapsedTime() {
async getAvgElapsedTime(_getAvgElapsedTime: GetAvgElapsedTimeDto) {
const result = await this.logRepository.findAvgElapsedTime();

return plainToInstance(GetAvgElapsedTimeResponseDto, result);
}

async getTrafficRank() {
async getTrafficRank(_getTrafficRankDto: GetTrafficRankDto) {
const result = await this.logRepository.findTop5CountByHost();

return plainToInstance(GetTrafficRankResponseDto, result);
Expand Down Expand Up @@ -207,4 +214,28 @@ export class LogService {

return plainToInstance(GetSpeedRankResponseDto, response);
}

async getTrafficTop5Chart(_getTrafficTop5ChartDto: GetTrafficTop5ChartDto) {
const results = await this.logRepository.findTrafficTop5Chart();

const trafficCharts = await Promise.all(
results.map(async (result) => {
const host = result.host;
const project = await this.projectRepository
.createQueryBuilder('project')
.select('project.name')
.where('project.domain = :domain', { domain: host })
.getOne();

const projectName = project?.name;

return plainToInstance(TrafficTop5Chart, {
name: projectName,
traffic: result.traffic,
});
}),
);

return plainToInstance(GetTrafficTop5ChartResponseDto, trafficCharts);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TrafficRankMetric } from './traffic-rank.metric';
import type { TrafficRankMetric } from './traffic-rank.metric';

export class TrafficRankTop5Metric {
rank: TrafficRankMetric[];
Expand Down
9 changes: 9 additions & 0 deletions backend/console-server/src/log/metric/trafficChart.metric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';

export class TrafficChartMetric {
@IsString()
host: string;

@IsString()
traffic: string[][];
}
Loading

0 comments on commit 41114e3

Please sign in to comment.