Skip to content

Commit

Permalink
Merge pull request #119 from boostcampwm-2024/dev-back
Browse files Browse the repository at this point in the history
[BE] merge to main
  • Loading branch information
sjy2335 authored Nov 20, 2024
2 parents 518ee29 + b194082 commit 63515d7
Show file tree
Hide file tree
Showing 36 changed files with 2,178 additions and 357 deletions.
2 changes: 1 addition & 1 deletion backend/console-server/src/clickhouse/clickhouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class Clickhouse implements OnModuleInit, OnModuleDestroy {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async query<T>(query: string, params?: Record<string, any>): Promise<T[]> {
async query<T>(query: string, params?: Record<string, unknown>): Promise<T[]> {
try {
const resultSet = await this.client.query({
query,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { MetricAggregationType, metricExpressions } from '../util/metric-expressions';
import type { MetricAggregationType } from '../util/metric-expressions';
import { metricExpressions } from '../util/metric-expressions';
import { mapFilterCondition } from '../util/map-filter-condition';

interface metric {
name: string;
Expand All @@ -7,7 +9,8 @@ interface metric {

export class TimeSeriesQueryBuilder {
private query: string;
private params: Record<string, any> = {};
private params: Record<string, unknown> = {};
private limitValue?: number;

constructor() {
this.query = `SELECT`;
Expand Down Expand Up @@ -59,12 +62,19 @@ export class TimeSeriesQueryBuilder {
return this;
}

filter(filters: Record<string, any>): this {
filter(filters: Record<string, unknown>): this {
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
this.query += ` AND ${key} = {${key}}`;
this.params[key] = value;
const conditions = Object.entries(filters).map(([key, value]) => {
const { condition, param } = mapFilterCondition(key, value);
this.params[key] = param;
return condition;
});

if (this.query.includes('WHERE')) {
this.query += ` AND ${conditions.join(' AND ')}`;
} else {
this.query += ` WHERE ${conditions.join(' AND ')}`;
}
}

return this;
Expand All @@ -88,7 +98,16 @@ export class TimeSeriesQueryBuilder {
return this;
}

limit(value: number): this {
this.limitValue = value;
return this;
}

build() {
if (this.limitValue) {
this.query += ` LIMIT ${this.limitValue}`;
}

console.log(this.query);

return { query: this.query, params: this.params };
Expand Down
19 changes: 19 additions & 0 deletions backend/console-server/src/clickhouse/util/map-filter-condition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type FilterValue = string | number | Date | unknown;

export function mapFilterCondition(
key: string,
value: FilterValue,
): { condition: string; param: FilterValue } {
let type: string;

if (typeof value === 'string') {
type = 'String';
} else if (typeof value === 'number') {
type = Number.isInteger(value) ? 'Int32' : 'Float64';
} else if (value instanceof Date) {
type = 'DateTime64(3)';
} else {
throw new Error(`Unsupported filter value type for key "${key}": ${typeof value}`);
}
return { condition: `${key} = {${key}:${type}}`, param: value };
}
12 changes: 6 additions & 6 deletions backend/console-server/src/clickhouse/util/metric-expressions.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
type MetricFunction = (metric: string) => string;

export const metricExpressions: Record<string, MetricFunction> = {
avg: (metric: string) => `avg(${metric}) as ${metric}`,
avg: (metric: string) => `avg(${metric}) as avg_${metric}`,
count: () => `count() as count`,
sum: (metric: string) => `sum(${metric}) as ${metric}`,
min: (metric: string) => `min(${metric}) as ${metric}`,
max: (metric: string) => `max(${metric}) as ${metric}`,
p95: (metric: string) => `quantile(0.95)(${metric}) as ${metric}`,
p99: (metric: string) => `quantile(0.99)(${metric}) as ${metric}`,
sum: (metric: string) => `sum(${metric}) as sum_${metric}`,
min: (metric: string) => `min(${metric}) as min_${metric}`,
max: (metric: string) => `max(${metric}) as max_${metric}`,
p95: (metric: string) => `quantile(0.95)(${metric}) as p95_${metric}`,
p99: (metric: string) => `quantile(0.99)(${metric}) as p99_${metric}`,
rate: (metric: string) => `(sum(${metric}) / count(*)) * 100 as ${metric}_rate`,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Exclude, Expose, Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

@Exclude()
export class GetDAUByProjectResponseDto {
@ApiProperty({
example: 'my-project',
description: '프로젝트 이름',
})
@Expose()
projectName: string;

@ApiProperty({
example: '2023-10-01',
description: '조회한 날짜',
})
@Expose()
date: string;

@ApiProperty({
example: 12345,
description: '해당 날짜의 DAU 값',
})
@Expose()
@Type(() => Number)
dau: number;
}
20 changes: 20 additions & 0 deletions backend/console-server/src/log/dto/get-dau-by-project.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IsNotEmpty, IsString, IsDateString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class GetDAUByProjectDto {
@ApiProperty({
example: 'watchducks',
description: '프로젝트 이름',
})
@IsNotEmpty()
@IsString()
projectName: string;

@ApiProperty({
example: '2023-10-01',
description: '조회할 날짜 (YYYY-MM-DD 형식)',
})
@IsNotEmpty()
@IsDateString()
date: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Exclude, Expose, Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

class PathResponseDto {
@ApiProperty({
example: '/api/v1/resource',
description: '사용자의 요청 경로',
})
@Expose()
path: string;

@ApiProperty({
example: 123.45,
description: '해당 경로의 평균 응답 소요 시간 (ms).',
})
@Expose()
avg_elapsed_time: number;
}

@Exclude()
export class GetPathSpeedRankResponseDto {
@ApiProperty({
example: 'watchducks',
description: '프로젝트 이름',
})
@Expose()
projectName: string;

@ApiProperty({
type: [PathResponseDto],
description: '프로젝트의 가장 빠른 응답 경로 배열',
})
@Expose()
@Type(() => PathResponseDto)
fastestPaths: Array<PathResponseDto>;

@ApiProperty({
type: [PathResponseDto],
description: '프로젝트의 가장 느린 응답 경로 배열',
})
@Expose()
@Type(() => PathResponseDto)
slowestPaths: Array<PathResponseDto>;
}
7 changes: 7 additions & 0 deletions backend/console-server/src/log/dto/get-path-speed-rank.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';

export class GetPathSpeedRankDto {
@IsNotEmpty()
@IsString()
projectName: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Expose, Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

export class GetSuccessRateByProjectResponseDTO {
@ApiProperty({
description: '프로젝트의 이름',
example: 'watchducks',
})
@Expose()
projectName: string;
@ApiProperty({
description: '프로젝트의 응답 성공률',
example: 85.5,
})
@Expose()
@Type(() => Number)
success_rate: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class GetSuccessRateByProjectDto {
@IsString()
@ApiProperty({
description: '프로젝트 이름',
example: 'watchducks',
required: true,
})
projectName: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';

export class GetSuccessRateResponseDto {
@ApiProperty({
example: 95.5,
description: '응답 성공률 (%)',
type: Number,
})
@Expose()
success_rate: number;
}
14 changes: 14 additions & 0 deletions backend/console-server/src/log/dto/get-success-rate.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNumber } from 'class-validator';

export class GetSuccessRateDto {
@IsNumber()
@Type(() => Number)
@ApiProperty({
description: '기수',
example: 5,
required: true,
})
generation: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';

export class GetTrafficByGenerationResponseDto {
@ApiProperty({
example: 15,
description: '기수 별 트래픽 수',
type: Number,
})
@Expose()
count: number;
}
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 GetTrafficByGenerationDto {
@IsNumber()
@Type(() => Number)
@ApiProperty({
description: '기수',
example: 5,
required: true,
})
generation: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Expose, Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

class TrafficDataPoint {
@ApiProperty({
example: '2024-11-07 23:07:28',
description: '시간 단위 별 타임스탬프',
})
@Expose()
timestamp: string;

@ApiProperty({
example: 1500,
description: '해당 타임스탬프의 트래픽 총량',
})
@Expose()
@Type(() => Number)
count: number;
}

export class GetTrafficByProjectResponseDto {
@ApiProperty({
example: 'watchducks',
description: '프로젝트 이름',
})
@Expose()
projectName: string;

@ApiProperty({
example: 'hour',
description: '시간 단위',
})
@Expose()
timeUnit: string;

@ApiProperty({
type: [TrafficDataPoint],
description: '시간 단위 별 트래픽 데이터',
})
@Expose()
@Type(() => TrafficDataPoint)
trafficData: TrafficDataPoint[];
}
30 changes: 30 additions & 0 deletions backend/console-server/src/log/dto/get-traffic-by-project.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { IsNotEmpty, IsString, IsIn } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';

export class GetTrafficByProjectDto {
@ApiProperty({
example: 'watchducks',
description: '프로젝트 이름',
})
@IsNotEmpty()
@IsString()
projectName: string;

@ApiProperty({
example: 'hour',
description: '시간 단위 (Minute, Hour, Day, Week, Month)',
enum: ['Minute', 'Hour', 'Day', 'Week', 'Month'],
})
@IsNotEmpty()
@IsString()
@Transform(({ value }) => {
if (typeof value === 'string') {
const lower = value.toLowerCase();
return lower.charAt(0).toUpperCase() + lower.slice(1);
}
return value;
})
@IsIn(['Minute', 'Hour', 'Day', 'Week', 'Month'])
timeUnit: string;
}
Loading

0 comments on commit 63515d7

Please sign in to comment.