From 0182131fff21b3f39440c5bb6ba8a064398fcfdf Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Mon, 21 Jul 2025 06:20:22 +0000 Subject: [PATCH] feat(logging): implement Winston logger and integrate into application --- backend/src/app.module.ts | 26 +++++----- .../logging/logger-transports.config.ts | 30 +++++++++++ .../src/entities/logging/logging.module.ts | 9 ++++ .../src/entities/logging/winston-logger.ts | 50 +++++++++++++++++++ backend/src/main.ts | 19 +++---- .../app-logger-middlewate.ts | 11 ++-- 6 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 backend/src/entities/logging/logger-transports.config.ts create mode 100644 backend/src/entities/logging/logging.module.ts create mode 100644 backend/src/entities/logging/winston-logger.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index f8fc496d5..e9ca85130 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -5,14 +5,25 @@ import { DataSource } from 'typeorm'; import { AppController } from './app.controller.js'; import { GlobalDatabaseContext } from './common/application/global-database-context.js'; import { BaseType, UseCaseType } from './common/data-injection.tokens.js'; +import { AIModule } from './entities/ai/ai.module.js'; +import { ApiKeyModule } from './entities/api-key/api-key.module.js'; +import { CompanyFaviconModule } from './entities/company-favicon/company-favicon.module.js'; +import { CompanyInfoModule } from './entities/company-info/company-info.module.js'; +import { CompanyLogoModule } from './entities/company-logo/company-logo.module.js'; +import { CompanyTabTitleModule } from './entities/company-tab-title/company-tab-title.module.js'; import { ConnectionPropertiesModule } from './entities/connection-properties/connection-properties.module.js'; import { ConnectionModule } from './entities/connection/connection.module.js'; import { ConversionModule } from './entities/convention/conversion.module.js'; import { CronJobsModule } from './entities/cron-jobs/cron-jobs.module.js'; import { CustomFieldModule } from './entities/custom-field/custom-field.module.js'; +import { DemoDataModule } from './entities/demo-data/demo-deta.module.js'; +import { EmailModule } from './entities/email/email/email.module.js'; import { GroupModule } from './entities/group/group.module.js'; +import { LoggingModule } from './entities/logging/logging.module.js'; import { PermissionModule } from './entities/permission/permission.module.js'; +import { TableTriggersModule } from './entities/table-actions/table-action-rules-module/action-rules.module.js'; import { TableActionModule } from './entities/table-actions/table-actions-module/table-action.module.js'; +import { TableFiltersModule } from './entities/table-filters/table-filters.module.js'; import { TableLogsModule } from './entities/table-logs/table-logs.module.js'; import { TableSettingsModule } from './entities/table-settings/table-settings.module.js'; import { TableModule } from './entities/table/table.module.js'; @@ -20,21 +31,11 @@ import { UserActionModule } from './entities/user-actions/user-action.module.js' import { UserModule } from './entities/user/user.module.js'; import { TableWidgetModule } from './entities/widget/table-widget.module.js'; import { TimeoutInterceptor } from './interceptors/index.js'; +import { SaaSGatewayModule } from './microservices/gateways/saas-gateway.ts/saas-gateway.module.js'; +import { SaasModule } from './microservices/saas-microservice/saas.module.js'; import { AppLoggerMiddleware } from './middlewares/logging-middleware/app-logger-middlewate.js'; import { DatabaseModule } from './shared/database/database.module.js'; import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js'; -import { SaasModule } from './microservices/saas-microservice/saas.module.js'; -import { SaaSGatewayModule } from './microservices/gateways/saas-gateway.ts/saas-gateway.module.js'; -import { CompanyInfoModule } from './entities/company-info/company-info.module.js'; -import { TableTriggersModule } from './entities/table-actions/table-action-rules-module/action-rules.module.js'; -import { ApiKeyModule } from './entities/api-key/api-key.module.js'; -import { AIModule } from './entities/ai/ai.module.js'; -import { EmailModule } from './entities/email/email/email.module.js'; -import { CompanyLogoModule } from './entities/company-logo/company-logo.module.js'; -import { CompanyFaviconModule } from './entities/company-favicon/company-favicon.module.js'; -import { CompanyTabTitleModule } from './entities/company-tab-title/company-tab-title.module.js'; -import { TableFiltersModule } from './entities/table-filters/table-filters.module.js'; -import { DemoDataModule } from './entities/demo-data/demo-deta.module.js'; @Module({ imports: [ @@ -66,6 +67,7 @@ import { DemoDataModule } from './entities/demo-data/demo-deta.module.js'; CompanyTabTitleModule, TableFiltersModule, DemoDataModule, + LoggingModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/entities/logging/logger-transports.config.ts b/backend/src/entities/logging/logger-transports.config.ts new file mode 100644 index 000000000..cf2f9c806 --- /dev/null +++ b/backend/src/entities/logging/logger-transports.config.ts @@ -0,0 +1,30 @@ +import winston from 'winston'; + +export const LoggerTransports = { + default: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.errors({ stack: true }), + winston.format.json(), + ), + }), + ], + debug: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.errors({ stack: true }), + winston.format.colorize({ + all: true, + colors: { info: 'blue', error: 'red', warn: 'yellow', debug: 'cyan' }, + }), + winston.format.printf(({ timestamp, level, message, context, trace }) => { + const logContext = context ? ` [${context}]` : ''; + const logTrace = trace ? `\nTrace: ${trace}` : ''; + return `${timestamp} [${level}]${logContext}: ${message}${logTrace}`; + }), + ), + }), + ], +}; diff --git a/backend/src/entities/logging/logging.module.ts b/backend/src/entities/logging/logging.module.ts new file mode 100644 index 000000000..d294bccf9 --- /dev/null +++ b/backend/src/entities/logging/logging.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { WinstonLogger } from './winston-logger.js'; + +@Global() +@Module({ + providers: [WinstonLogger], + exports: [WinstonLogger], +}) +export class LoggingModule {} diff --git a/backend/src/entities/logging/winston-logger.ts b/backend/src/entities/logging/winston-logger.ts new file mode 100644 index 000000000..8eaa3a58b --- /dev/null +++ b/backend/src/entities/logging/winston-logger.ts @@ -0,0 +1,50 @@ +import { Injectable, LoggerService } from '@nestjs/common'; +import winston from 'winston'; +import { slackPostMessage } from '../../helpers/index.js'; +import { LoggerTransports } from './logger-transports.config.js'; + +@Injectable() +export class WinstonLogger implements LoggerService { + private readonly logger: winston.Logger; + + constructor() { + this.logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + transports: LoggerTransports.default, + }); + } + + log(message: any, ...optionalParams: any[]) { + this.logger.info(message, ...optionalParams); + } + + error(message: any, ...optionalParams: any[]) { + this.logger.error(message, ...optionalParams); + } + + warn(message: any, ...optionalParams: any[]) { + this.logger.warn(message, ...optionalParams); + } + + logWithSlack(message: any, ...optionalParams: any[]) { + this.logger.error(message, ...optionalParams); + slackPostMessage(message).catch((error) => { + this.logger.error('Failed to send Slack message', error); + }); + } + + debug(message: any, ...optionalParams: any[]) { + this.logger.debug(message, ...optionalParams); + } + + verbose(message: any, ...optionalParams: any[]) { + this.logger.verbose(message, ...optionalParams); + } + + fatal(message: any, ...optionalParams: any[]) { + this.logger.error(message, ...optionalParams); + slackPostMessage(message).catch((error) => { + this.logger.error('Failed to send Slack message', error); + }); + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts index 9c09f8214..e4fc9d97a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,30 +1,31 @@ +import { NestApplicationOptions, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import * as Sentry from '@sentry/node'; +import bodyParser from 'body-parser'; +import { ValidationError } from 'class-validator'; import cookieParser from 'cookie-parser'; import rateLimit from 'express-rate-limit'; +import session from 'express-session'; import helmet from 'helmet'; import { ApplicationModule } from './app.module.js'; +import { WinstonLogger } from './entities/logging/winston-logger.js'; import { AllExceptionsFilter } from './exceptions/all-exceptions.filter.js'; +import { ValidationException } from './exceptions/custom-exceptions/validation-exception.js'; import { Constants } from './helpers/constants/constants.js'; import { requiredEnvironmentVariablesValidator } from './helpers/validators/required-environment-variables.validator.js'; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import { NestApplicationOptions, ValidationPipe } from '@nestjs/common'; -import { ValidationError } from 'class-validator'; -import { ValidationException } from './exceptions/custom-exceptions/validation-exception.js'; -import bodyParser from 'body-parser'; -import { NestExpressApplication } from '@nestjs/platform-express'; -import session from 'express-session'; async function bootstrap() { try { - process.env.NO_COLOR = 'true'; requiredEnvironmentVariablesValidator(); const appOptions: NestApplicationOptions = { rawBody: true, - logger: ['error', 'warn', 'fatal', 'warn'], + logger: new WinstonLogger(), }; const app = await NestFactory.create(ApplicationModule, appOptions); + app.useLogger(app.get(WinstonLogger)); app.set('query parser', 'extended'); Sentry.init({ diff --git a/backend/src/middlewares/logging-middleware/app-logger-middlewate.ts b/backend/src/middlewares/logging-middleware/app-logger-middlewate.ts index 9b9f73640..52d1a022e 100644 --- a/backend/src/middlewares/logging-middleware/app-logger-middlewate.ts +++ b/backend/src/middlewares/logging-middleware/app-logger-middlewate.ts @@ -1,18 +1,21 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import { NextFunction, Request, Response } from 'express'; -import { Logger } from '../../helpers/logging/Logger.js'; +import { WinstonLogger } from '../../entities/logging/winston-logger.js'; @Injectable() export class AppLoggerMiddleware implements NestMiddleware { + constructor(private readonly logger: WinstonLogger) {} + use(request: Request, response: Response, next: NextFunction): void { - const { ip, method, path: url } = request; + const { ip, method, path: url, baseUrl } = request; const userAgent = request.get('user-agent') || ''; - Logger.logInfoString(`START ${method} ${url} - ${userAgent} ${ip}`); + this.logger.log(`START ${method} ${url}${baseUrl} - ${userAgent} ${ip}`, { context: 'HTTP' }); response.on('close', () => { const { statusCode } = response; const contentLength = response.get('content-length'); - Logger.logInfoString( + this.logger.log( `method: ${method}, url: ${url}, statusCode: ${statusCode}, contentLength: ${contentLength}, userAgent: ${userAgent}, ip: ${ip}`, + { context: 'HTTP' }, ); });