Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Measure api performance, add statistics api #754

Merged
merged 3 commits into from
Jul 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AppService } from './app.service';
import { StatusDto } from './status.dto';
Expand Down
8 changes: 8 additions & 0 deletions server/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CoreModule } from './core/core.module';
import { AuthModule } from './auth/auth.module';
import { UserModule } from './user/user.module';
import { CacheConfigService } from './cache-config.service';
import { ApiRequest, ApiRequestSchema } from './metrics/schemas/api-request.schema';

const redisPort = isNaN(parseInt(process.env.VIEWTUBE_REDIS_PORT))
? 6379
Expand All @@ -32,6 +33,13 @@ const moduleMetadata: ModuleMetadata = {
useFindAndModify: false,
useCreateIndex: true
}),
MongooseModule.forFeature([
{
name: ApiRequest.name,
schema: ApiRequestSchema,
collection: 'api-requests'
}
]),
CacheModule.registerAsync({
useClass: CacheConfigService
}),
Expand Down
12 changes: 3 additions & 9 deletions server/core/channels/channels.controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import {
Controller,
Get,
Param,
Res,
CacheInterceptor,
UseInterceptors
} from '@nestjs/common';
import { Controller, Get, Param, Res, CacheInterceptor, UseInterceptors } from '@nestjs/common';
import { Response } from 'express';
import { ApiTags } from '@nestjs/swagger';
import { MetricsInterceptor } from 'server/metrics/metrics.interceptor';
import { ChannelsService } from './channels.service';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ChannelDto } from './dto/channel.dto';

@ApiTags('Core')
Expand All @@ -27,6 +20,7 @@ export class ChannelsController {
}

@Get(':id')
@UseInterceptors(MetricsInterceptor)
@UseInterceptors(CacheInterceptor)
getChannel(@Param('id') channelId: string): Promise<ChannelDto> {
return this.channelsService.getChannel(channelId);
Expand Down
6 changes: 6 additions & 0 deletions server/core/channels/channels.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CacheModule, Module, ModuleMetadata } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CacheConfigService } from 'server/cache-config.service';
import { General, GeneralSchema } from 'server/common/general.schema';
import { ApiRequest, ApiRequestSchema } from 'server/metrics/schemas/api-request.schema';
import { ChannelsController } from './channels.controller';
import { ChannelsService } from './channels.service';
import { ChannelBasicInfo, ChannelBasicInfoSchema } from './schemas/channel-basic-info.schema';
Expand All @@ -23,6 +24,11 @@ const moduleMetadata: ModuleMetadata = {
name: General.name,
schema: GeneralSchema,
collection: 'general'
},
{
name: ApiRequest.name,
schema: ApiRequestSchema,
collection: 'api-requests'
}
])
]
Expand Down
20 changes: 17 additions & 3 deletions server/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule } from '@nestjs/config';
import { General, GeneralSchema } from 'server/common/general.schema';
import { CacheConfigService } from 'server/cache-config.service';
import { ApiRequest, ApiRequestSchema } from 'server/metrics/schemas/api-request.schema';
import { User, UserSchema } from 'server/user/schemas/user.schema';
import { VideosController } from './videos/videos.controller';
import { VideosService } from './videos/videos.service';
import { VideoplaybackController } from './videoplayback/videoplayback.controller';
Expand All @@ -20,6 +22,8 @@ import { HomepageModule } from './homepage/homepage.module';
import { ProxyModule } from './proxy/proxy.module';
import { CommentsModule } from './comments/comments.module';
import { PlaylistsModule } from './playlists/playlists.module';
import { StatisticsController } from './statistics/statistics.controller';
import { StatisticsService } from './statistics/statistics.service';

const moduleMetadata: ModuleMetadata = {
imports: [
Expand All @@ -43,6 +47,16 @@ const moduleMetadata: ModuleMetadata = {
name: General.name,
schema: GeneralSchema,
collection: 'general'
},
{
name: ApiRequest.name,
schema: ApiRequestSchema,
collection: 'api-requests'
},
{
name: User.name,
schema: UserSchema,
collection: 'users'
}
]),
CacheModule.registerAsync({
Expand All @@ -57,9 +71,9 @@ const moduleMetadata: ModuleMetadata = {
CommentsModule,
PlaylistsModule
],
controllers: [VideosController, VideoplaybackController],
providers: [VideosService, VideoplaybackService],
exports: [VideosService, VideoplaybackService]
controllers: [VideosController, VideoplaybackController, StatisticsController],
providers: [VideosService, VideoplaybackService, StatisticsService],
exports: [VideosService, VideoplaybackService, StatisticsService]
};
@Module(moduleMetadata)
export class CoreModule {}
11 changes: 10 additions & 1 deletion server/core/homepage/homepage.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { CacheInterceptor, CacheKey, CacheTTL, Controller, Get, UseInterceptors } from '@nestjs/common';
import {
CacheInterceptor,
CacheKey,
CacheTTL,
Controller,
Get,
UseInterceptors
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { MetricsInterceptor } from 'server/metrics/metrics.interceptor';
import { PopularDto } from './dto/popular.dto';
import { HomepageService } from './homepage.service';

@ApiTags('Core')
@UseInterceptors(CacheInterceptor)
@UseInterceptors(MetricsInterceptor)
@Controller('homepage')
export class HomepageController {
constructor(private homepageService: HomepageService) {}
Expand Down
6 changes: 6 additions & 0 deletions server/core/homepage/homepage.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CacheModule, Module, ModuleMetadata } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CacheConfigService } from 'server/cache-config.service';
import { ApiRequest, ApiRequestSchema } from 'server/metrics/schemas/api-request.schema';
import {
ChannelBasicInfo,
ChannelBasicInfoSchema
Expand All @@ -26,6 +27,11 @@ const moduleMetadata: ModuleMetadata = {
name: ChannelBasicInfo.name,
schema: ChannelBasicInfoSchema,
collection: 'channel-basicinfo'
},
{
name: ApiRequest.name,
schema: ApiRequestSchema,
collection: 'api-requests'
}
])
]
Expand Down
19 changes: 19 additions & 0 deletions server/core/statistics/dto/statistics.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class UniqueRequest {
responseTime: number;
timestamp: number;
}

export class EndpointStatisticDto {
url: string;
uniqueRequests: Array<UniqueRequest>;
}

export class UserRegistrationDto {
timestamp: number;
}

export class StatisticsDto {
registrations: Array<UserRegistrationDto>;

endpoints: Array<EndpointStatisticDto>;
}
15 changes: 15 additions & 0 deletions server/core/statistics/statistics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { StatisticsDto } from './dto/statistics.dto';
import { StatisticsService } from './statistics.service';

@ApiTags('Core')
@Controller('statistics')
export class StatisticsController {
constructor(private statisticsService: StatisticsService) {}

@Get()
getStatistics(): Promise<StatisticsDto> {
return this.statisticsService.getStatistics();
}
}
90 changes: 90 additions & 0 deletions server/core/statistics/statistics.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { ApiRequest } from 'server/metrics/schemas/api-request.schema';
import { User } from 'server/user/schemas/user.schema';
import { EndpointStatisticDto, StatisticsDto, UserRegistrationDto } from './dto/statistics.dto';

@Injectable()
export class StatisticsService {
constructor(
@InjectModel(ApiRequest.name)
private readonly ApiRequestModel: Model<ApiRequest>,
@InjectModel(User.name)
private readonly UserModel: Model<User>
) {}

async getStatistics(): Promise<StatisticsDto> {
let statistics: StatisticsDto = null;
try {
const endpoints = await this.getEndpointStatistics();

const registrations = await this.getRegistrations();

statistics = {
registrations,
endpoints
};
} catch (error) {
console.log(error);
}

if (statistics) {
return statistics;
}
throw new InternalServerErrorException();
}

private async getRegistrations(): Promise<Array<UserRegistrationDto>> {
const users = await this.UserModel.find().lean().exec();
return users.map(user => {
const timestamp = new Date((user as any).createdAt);
return {
timestamp: timestamp.valueOf()
};
});
}

private async getEndpointStatistics(): Promise<Array<EndpointStatisticDto>> {
const apiCalls = await this.ApiRequestModel.find().lean().exec();

apiCalls.map(data => {
const newRequestData = data;
if (newRequestData.params && newRequestData.params.id) {
newRequestData.url = newRequestData.url
.replace(newRequestData.params.id as string, '')
.replace(/\?.*/i, '');
}
return newRequestData;
});

const groupedApiCalls = this.groupBy(apiCalls, 'url');

const endpointStatsArray = [];

for (const url in groupedApiCalls) {
if (Object.prototype.hasOwnProperty.call(groupedApiCalls, url)) {
const apiCallsArray = groupedApiCalls[url];

endpointStatsArray.push({
url,
uniqueRequests: apiCallsArray.map(element => {
return {
responseTime: element.requestDuration,
timestamp: element.timestamp
};
})
});
}
}

return endpointStatsArray;
}

private groupBy(arr: Array<any>, property: string) {
return arr.reduce((acc, cur) => {
acc[cur[property]] = [...(acc[cur[property]] || []), cur];
return acc;
}, {});
}
}
5 changes: 4 additions & 1 deletion server/core/videos/videos.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import {
ClassSerializerInterceptor,
SerializeOptions,
CacheInterceptor,
CacheTTL} from '@nestjs/common';
CacheTTL
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { MetricsInterceptor } from 'server/metrics/metrics.interceptor';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { VideoDto } from 'shared/dto/video/video.dto';
import { VideosService } from './videos.service';

@ApiTags('Core')
@UseInterceptors(CacheInterceptor)
@UseInterceptors(MetricsInterceptor)
@Controller('videos')
export class VideosController {
constructor(private readonly videosService: VideosService) {}
Expand Down
8 changes: 8 additions & 0 deletions server/metrics/dto/api-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ParamsDto } from "./params.dto";

export class ApiRequestDto {
url: string;
params: ParamsDto;
requestDuration: number;
timestamp: number;
}
3 changes: 3 additions & 0 deletions server/metrics/dto/params.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class ParamsDto {
[key: string]: unknown;
}
33 changes: 33 additions & 0 deletions server/metrics/metrics.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { tap } from 'rxjs';
import { ApiRequest } from './schemas/api-request.schema';

@Injectable()
export class MetricsInterceptor implements NestInterceptor {
constructor(
@InjectModel(ApiRequest.name)
private readonly ApiRequestModel: Model<ApiRequest>
) {}

intercept(context: ExecutionContext, next: CallHandler) {
const dateBeforeRequest = Date.now();

return next.handle().pipe(
tap(async () => {
const requestDuration = Date.now() - dateBeforeRequest;
const request = context.getArgByIndex(0);
if (request) {
const metricData = {
url: request.url,
params: request.params,
requestDuration,
timestamp: dateBeforeRequest
};
await this.ApiRequestModel.create(metricData);
}
})
);
}
}
20 changes: 20 additions & 0 deletions server/metrics/schemas/api-request.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Document } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { ParamsDto } from '../dto/params.dto';

@Schema({ timestamps: true })
export class ApiRequest extends Document {
@Prop()
url: string;

@Prop()
params: ParamsDto;

@Prop()
requestDuration: number;

@Prop()
timestamp: number;
}

export const ApiRequestSchema = SchemaFactory.createForClass(ApiRequest);