From fd366f4cdbb0049ebfdc9e32991d32d7bbcd1310 Mon Sep 17 00:00:00 2001 From: Krystof Date: Sun, 6 Oct 2024 19:09:04 +0200 Subject: [PATCH] feat(be): logging --- backend-nest/prisma/schema.prisma | 2 +- backend-nest/src/app.module.ts | 17 +++- backend-nest/src/enums/log.enum.ts | 13 +++ .../src/middleware/logger.middleware.ts | 99 +++++++++++++++++++ .../src/modules/import/import.controller.ts | 38 +++++-- .../src/modules/import/import.service.ts | 5 - .../src/modules/stop/stop.controller.ts | 1 - backend-nest/src/utils/graphql.utils.ts | 11 +++ backend-nest/src/utils/status-code.utils.ts | 10 ++ 9 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 backend-nest/src/enums/log.enum.ts create mode 100644 backend-nest/src/middleware/logger.middleware.ts create mode 100644 backend-nest/src/utils/graphql.utils.ts create mode 100644 backend-nest/src/utils/status-code.utils.ts diff --git a/backend-nest/prisma/schema.prisma b/backend-nest/prisma/schema.prisma index 708c3c0..b23e3d1 100644 --- a/backend-nest/prisma/schema.prisma +++ b/backend-nest/prisma/schema.prisma @@ -78,7 +78,7 @@ model Log { host String? path String? statusCode Int? - duration Int? + duration Int? // in milliseconds createdAt DateTime @default(now()) } diff --git a/backend-nest/src/app.module.ts b/backend-nest/src/app.module.ts index dbf58cf..aa74a62 100644 --- a/backend-nest/src/app.module.ts +++ b/backend-nest/src/app.module.ts @@ -1,11 +1,17 @@ import { ApolloDriver, type ApolloDriverConfig } from "@nestjs/apollo"; import { CacheModule } from "@nestjs/cache-manager"; -import { Module } from "@nestjs/common"; +import { + MiddlewareConsumer, + Module, + NestModule, + RequestMethod, +} from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { GraphQLModule } from "@nestjs/graphql"; import { ScheduleModule } from "@nestjs/schedule"; import { GRAPHQL_API_ROOT } from "src/constants/graphql.const"; +import { LoggerMiddleware } from "src/middleware/logger.middleware"; import { DepartureModule } from "src/modules/departure/departure.module"; import { ImportModule } from "src/modules/import/import.module"; import { PlatformModule } from "src/modules/platform/platform.module"; @@ -35,4 +41,11 @@ import { StopModule } from "src/modules/stop/stop.module"; controllers: [], providers: [], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(LoggerMiddleware).forRoutes({ + path: "*", + method: RequestMethod.ALL, + }); + } +} diff --git a/backend-nest/src/enums/log.enum.ts b/backend-nest/src/enums/log.enum.ts new file mode 100644 index 0000000..3216602 --- /dev/null +++ b/backend-nest/src/enums/log.enum.ts @@ -0,0 +1,13 @@ +export { LogType } from "@prisma/client"; + +export enum StopSyncTrigger { + CRON = "CRON", + INIT = "INIT", +} + +export enum LogMessage { + IMPORT_STOPS = "Import stops", + REST = "REST", + GRAPHQL = "GraphQL", + GRAPHQL_INTROSPECTION = "GraphQL - Introspection Query", +} diff --git a/backend-nest/src/middleware/logger.middleware.ts b/backend-nest/src/middleware/logger.middleware.ts new file mode 100644 index 0000000..937d5a6 --- /dev/null +++ b/backend-nest/src/middleware/logger.middleware.ts @@ -0,0 +1,99 @@ +import { Injectable, NestMiddleware } from "@nestjs/common"; +import { LogType } from "@prisma/client"; +import { NextFunction, Request, Response } from "express"; + +import { LogMessage } from "src/enums/log.enum"; +import { PrismaService } from "src/modules/prisma/prisma.service"; +import { + isGraphqlRequest, + isIntrospectionQuery, +} from "src/utils/graphql.utils"; +import { isSuccess } from "src/utils/status-code.utils"; + +@Injectable() +export class LoggerMiddleware implements NestMiddleware { + constructor(private prisma: PrismaService) {} + + use(req: Request, res: Response, next: NextFunction) { + const start = Date.now(); + + res.on("finish", async () => { + if (isGraphqlRequest(req)) { + try { + await createGraphqlLog({ + req, + res, + duration: Date.now() - start, + prisma: this.prisma, + }); + } catch (error) { + console.error("Failed to create Graphql log", error); + } + } else { + try { + await createRestLog({ + req, + res, + duration: Date.now() - start, + prisma: this.prisma, + }); + } catch (error) { + console.error("Failed to create REST log", error); + } + } + }); + + if (next) { + next(); + } + } +} + +const createRestLog = async ({ + req, + res, + duration, + prisma, +}: { + req: Request; + res: Response; + duration: number; + prisma: PrismaService; +}): Promise => { + prisma.log.create({ + data: { + type: isSuccess(res.statusCode) ? LogType.INFO : LogType.ERROR, + message: LogMessage.REST, + statusCode: res.statusCode, + host: req.headers.host ?? null, + path: req.originalUrl, + duration, + }, + }); +}; + +const createGraphqlLog = async ({ + req, + res, + duration, + prisma, +}: { + req: Request; + res: Response; + duration: number; + prisma: PrismaService; +}): Promise => { + prisma.log.create({ + data: { + type: isSuccess(res.statusCode) ? LogType.INFO : LogType.ERROR, + message: isIntrospectionQuery(req) + ? LogMessage.GRAPHQL_INTROSPECTION + : LogMessage.GRAPHQL, + statusCode: res.statusCode, + host: req.headers.host ?? null, + duration, + path: req.originalUrl, + description: req.body.query, + }, + }); +}; diff --git a/backend-nest/src/modules/import/import.controller.ts b/backend-nest/src/modules/import/import.controller.ts index 3145681..ac9b068 100644 --- a/backend-nest/src/modules/import/import.controller.ts +++ b/backend-nest/src/modules/import/import.controller.ts @@ -1,29 +1,55 @@ import { Controller, OnModuleInit } from "@nestjs/common"; import { Cron, CronExpression } from "@nestjs/schedule"; +import { LogMessage, LogType, StopSyncTrigger } from "src/enums/log.enum"; import { ImportService } from "src/modules/import/import.service"; +import { PrismaService } from "src/modules/prisma/prisma.service"; @Controller("import") export class ImportController implements OnModuleInit { - constructor(private readonly importService: ImportService) {} + constructor( + private readonly importService: ImportService, + private readonly prismaService: PrismaService, + ) {} async onModuleInit() { console.log("Starting initial stop sync"); - this.syncStops(); + + this.syncStops(StopSyncTrigger.INIT); } @Cron(CronExpression.EVERY_7_HOURS) async cronSyncStops(): Promise { console.log("Starting scheduled stop sync"); - this.syncStops(); + + this.syncStops(StopSyncTrigger.CRON); } - async syncStops(): Promise { + async syncStops(trigger: StopSyncTrigger): Promise { + const start = Date.now(); + try { await this.importService.syncStops(); - console.log("Scheduled stop sync completed successfully"); + + await this.prismaService.log.create({ + data: { + type: LogType.INFO, + message: LogMessage.IMPORT_STOPS, + duration: Date.now() - start, + description: `Trigger: ${trigger};`, + }, + }); } catch (error) { - console.error("Error during scheduled stop sync:", error); + await this.prismaService.log.create({ + data: { + type: LogType.ERROR, + message: LogMessage.IMPORT_STOPS, + duration: Date.now() - start, + description: `Trigger: ${trigger}; Error: ${error}`, + }, + }); + } finally { + console.log("Finished stop sync"); } } } diff --git a/backend-nest/src/modules/import/import.service.ts b/backend-nest/src/modules/import/import.service.ts index c8d2f3d..72953aa 100644 --- a/backend-nest/src/modules/import/import.service.ts +++ b/backend-nest/src/modules/import/import.service.ts @@ -140,8 +140,6 @@ export class ImportService { } async syncStops(): Promise { - console.log("Syncing stops and routes"); - const platformsData = await this.getPlatforms(); const stopsData = await this.getStops(); @@ -156,7 +154,6 @@ export class ImportService { const isMetro = routeNames.some( (routeName) => metroLine.safeParse(routeName).success, ); - return { latitude, longitude, @@ -194,7 +191,5 @@ export class ImportService { routes: platform.routes, })), }); - - console.log(`Import finished`); } } diff --git a/backend-nest/src/modules/stop/stop.controller.ts b/backend-nest/src/modules/stop/stop.controller.ts index 6d5d8c8..afd2cea 100644 --- a/backend-nest/src/modules/stop/stop.controller.ts +++ b/backend-nest/src/modules/stop/stop.controller.ts @@ -17,7 +17,6 @@ export class StopController { @Query("metroOnly") metroOnlyQuery: unknown, ) { - console.log("Fetching all stops"); const metroOnly: boolean = metroOnlyQuery === "true"; const stops = await this.stopService.getAll({ metroOnly }); diff --git a/backend-nest/src/utils/graphql.utils.ts b/backend-nest/src/utils/graphql.utils.ts new file mode 100644 index 0000000..c640593 --- /dev/null +++ b/backend-nest/src/utils/graphql.utils.ts @@ -0,0 +1,11 @@ +import { Request } from "express"; + +import { GRAPHQL_API_ROOT } from "src/constants/graphql.const"; + +export const isGraphqlRequest = (req: Request): boolean => { + return req.originalUrl === GRAPHQL_API_ROOT; +}; + +export const isIntrospectionQuery = (req: Request): boolean => { + return String(req.body?.query).startsWith("query IntrospectionQuery {"); +}; diff --git a/backend-nest/src/utils/status-code.utils.ts b/backend-nest/src/utils/status-code.utils.ts new file mode 100644 index 0000000..4694998 --- /dev/null +++ b/backend-nest/src/utils/status-code.utils.ts @@ -0,0 +1,10 @@ +/** + * Checks if the given HTTP status code represents a successful response. + * @param statusCode - The HTTP status code to check. + * @returns True if the status code is in the range [200, 300), false otherwise. + */ +export const isSuccess = (statusCode: number): boolean => { + // Ensure the status code is a positive integer + const code = Math.floor(Math.abs(statusCode)); + return code >= 200 && code < 300; +};