From 0949f7291afaa2ab5ccc722d6e48fd6785f29549 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Tue, 15 Apr 2025 17:59:48 +0530 Subject: [PATCH 1/8] server.ts refactored --- src/app.ts | 35 ++++++ src/config/database.ts | 66 ++++++++++ src/config/logger.ts | 28 +++++ src/config/nats.ts | 56 +++++++++ src/middleware/metrics.ts | 27 ++++ src/routes/healthRoutes.ts | 35 ++++++ src/routes/index.ts | 32 +++++ src/routes/metricsRoutes.ts | 27 ++++ src/routes/notificationRoutes.ts | 40 ++++++ src/server.ts | 191 +++++------------------------ src/services/serviceInitializer.ts | 107 ++++++++++++++++ 11 files changed, 485 insertions(+), 159 deletions(-) create mode 100644 src/app.ts create mode 100644 src/config/database.ts create mode 100644 src/config/logger.ts create mode 100644 src/config/nats.ts create mode 100644 src/middleware/metrics.ts create mode 100644 src/routes/healthRoutes.ts create mode 100644 src/routes/index.ts create mode 100644 src/routes/metricsRoutes.ts create mode 100644 src/routes/notificationRoutes.ts create mode 100644 src/services/serviceInitializer.ts diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..d82661c --- /dev/null +++ b/src/app.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import bodyParser from 'body-parser'; +import { createRouter } from './routes'; +import { metricsMiddleware } from './middleware/metrics'; +import { NotificationService } from './notification/service/notificationService'; + +export const createApp = (notificationService: NotificationService) => { + const app = express(); + + // Middleware + app.use(bodyParser.json({ limit: '10mb' })); + app.use(express.json()); + app.use(metricsMiddleware); + + // Routes + app.use(createRouter(notificationService)); + + return app; +}; diff --git a/src/config/database.ts b/src/config/database.ts new file mode 100644 index 0000000..17cfd63 --- /dev/null +++ b/src/config/database.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConnectionOptions, createConnection } from "typeorm"; +import { NotificationSettings } from "../entities/notificationSettings"; +import { NotifierEventLog } from "../entities/notifierEventLogs"; +import { Event } from "../notification/service/notificationService"; +import { NotificationTemplates } from "../entities/notificationTemplates"; +import { SlackConfig } from "../entities/slackConfig"; +import { SesConfig } from "../entities/sesConfig"; +import { SMTPConfig } from "../entities/smtpConfig"; +import { WebhookConfig } from "../entities/webhookconfig"; +import { Users } from "../entities/users"; +import { logger } from "./logger"; +import * as process from "process"; + +export const connectToDatabase = async () => { + const dbHost: string = process.env.DB_HOST; + const dbPort: number = +process.env.DB_PORT; + const user: string = process.env.DB_USER; + const pwd: string = process.env.DB_PWD; + const db: string = process.env.DB; + + const dbOptions: ConnectionOptions = { + type: "postgres", + host: dbHost, + port: dbPort, + username: user, + password: pwd, + database: db, + entities: [ + NotificationSettings, + NotifierEventLog, + Event, + NotificationTemplates, + SlackConfig, + SesConfig, + SMTPConfig, + WebhookConfig, + Users + ] + }; + + try { + const connection = await createConnection(dbOptions); + logger.info("Connected to DB"); + return connection; + } catch (error) { + logger.error("TypeORM connection error: ", error); + logger.error("shutting down notifier due to un-successful database connection..."); + process.exit(1); + } +}; diff --git a/src/config/logger.ts b/src/config/logger.ts new file mode 100644 index 0000000..b73cee2 --- /dev/null +++ b/src/config/logger.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as winston from 'winston'; + +export const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.printf(info => { + return `${info.timestamp} ${info.level}: ${info.message}`; + }) + ), + transports: [new winston.transports.Console()] +}); diff --git a/src/config/nats.ts b/src/config/nats.ts new file mode 100644 index 0000000..f2f8f55 --- /dev/null +++ b/src/config/nats.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { connect, NatsConnection } from "nats"; +import { logger } from "./logger"; +import { PubSubServiceImpl } from "../pubSub/pubSub"; +import { NOTIFICATION_EVENT_TOPIC } from "../pubSub/utils"; +import { Event } from "../notification/service/notificationService"; +import { NotificationService } from "../notification/service/notificationService"; +import { successNotificationMetricsCounter, failedNotificationMetricsCounter } from '../common/metrics'; + +export const natsEventHandler = (notificationService: NotificationService) => async (msg: string) => { + const eventAsString = JSON.parse(msg); + const event = JSON.parse(eventAsString) as Event; + logger.info({ natsEventBody: event }); + const response = await notificationService.sendNotification(event); + if (response.status != 0) { + successNotificationMetricsCounter.inc(); + } else { + failedNotificationMetricsCounter.inc(); + } +}; + +export const connectToNats = async (notificationService: NotificationService) => { + const natsUrl = process.env.NATS_URL; + + if (!natsUrl) { + logger.info("NATS_URL not provided, skipping NATS connection"); + return; + } + + try { + logger.info("Connecting to NATS server..."); + const conn: NatsConnection = await connect({ servers: natsUrl }); + const jsm = await conn.jetstreamManager(); + const pubSubService = new PubSubServiceImpl(conn, jsm, logger); + await pubSubService.Subscribe(NOTIFICATION_EVENT_TOPIC, natsEventHandler(notificationService)); + logger.info("Successfully connected to NATS"); + return conn; + } catch (err) { + logger.error("Error connecting to NATS:", err); + } +}; diff --git a/src/middleware/metrics.ts b/src/middleware/metrics.ts new file mode 100644 index 0000000..b8c397a --- /dev/null +++ b/src/middleware/metrics.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Request, Response, NextFunction } from 'express'; +import { httpRequestMetricsCounter } from '../common/metrics'; + +export const metricsMiddleware = (req: Request, res: Response, next: NextFunction) => { + httpRequestMetricsCounter.labels({ + method: req.method, + endpoint: req.url, + statusCode: res.statusCode + }).inc(); + next(); +}; diff --git a/src/routes/healthRoutes.ts b/src/routes/healthRoutes.ts new file mode 100644 index 0000000..9ca4489 --- /dev/null +++ b/src/routes/healthRoutes.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Router } from 'express'; +import { send } from '../tests/sendSlackNotification'; + +const router = Router(); + +router.get('/', (req, res) => { + res.send('Welcome to notifier Notifier!'); +}); + +router.get('/health', (req, res) => { + res.status(200).send("healthy"); +}); + +router.get('/test', (req, res) => { + send(); + res.send('Test!'); +}); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..5acdc09 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Router } from 'express'; +import healthRoutes from './healthRoutes'; +import createNotificationRouter from './notificationRoutes'; +import metricsRoutes from './metricsRoutes'; +import { NotificationService } from '../notification/service/notificationService'; + +export const createRouter = (notificationService: NotificationService) => { + const router = Router(); + + // Mount routes + router.use('/', healthRoutes); + router.use('/', createNotificationRouter(notificationService)); + router.use('/', metricsRoutes); + + return router; +}; diff --git a/src/routes/metricsRoutes.ts b/src/routes/metricsRoutes.ts new file mode 100644 index 0000000..8a27d0a --- /dev/null +++ b/src/routes/metricsRoutes.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Router } from 'express'; +import { register } from 'prom-client'; + +const router = Router(); + +router.get('/metrics', async (req, res) => { + res.setHeader('Content-Type', register.contentType); + res.send(await register.metrics()); +}); + +export default router; diff --git a/src/routes/notificationRoutes.ts b/src/routes/notificationRoutes.ts new file mode 100644 index 0000000..bf3567b --- /dev/null +++ b/src/routes/notificationRoutes.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Router } from 'express'; +import { NotificationService } from '../notification/service/notificationService'; +import { logger } from '../config/logger'; +import { successNotificationMetricsCounter, failedNotificationMetricsCounter } from '../common/metrics'; + +export const createNotificationRouter = (notificationService: NotificationService) => { + const router = Router(); + + router.post('/notify', async(req, res) => { + logger.info("notifications Received"); + const response = await notificationService.sendNotification(req.body); + if (response.status != 0) { + res.status(response.status).json({message: response.message}).send(); + successNotificationMetricsCounter.inc(); + } else { + res.status(response.error.statusCode).json({message: response.error.message}).send(); + failedNotificationMetricsCounter.inc(); + } + }); + + return router; +}; + +export default createNotificationRouter; diff --git a/src/server.ts b/src/server.ts index 30214d9..a4ace74 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,164 +14,37 @@ * limitations under the License. */ -import express from 'express'; -import { NotificationService, Event, Handler } from './notification/service/notificationService' -import "reflect-metadata" -import { ConnectionOptions, createConnection } from "typeorm" -import { NotificationSettingsRepository } from "./repository/notificationSettingsRepository" -import { SlackService } from './destination/destinationHandlers/slackHandler' -import { SESService } from './destination/destinationHandlers/sesHandler' -import { SMTPService } from './destination/destinationHandlers/smtpHandler' -import { EventLogRepository } from './repository/notifierEventLogRepository' -import { EventLogBuilder } from './common/eventLogBuilder' -import { EventRepository } from './repository/eventsRepository' -import { NotificationTemplatesRepository } from "./repository/templatesRepository"; -import { SlackConfigRepository } from "./repository/slackConfigRepository"; -import { NotificationSettings } from "./entities/notificationSettings"; -import { NotifierEventLog } from "./entities/notifierEventLogs"; -import { NotificationTemplates } from "./entities/notificationTemplates"; -import { SlackConfig } from "./entities/slackConfig"; -import * as winston from 'winston'; -import { SesConfig } from "./entities/sesConfig"; -import { SESConfigRepository } from "./repository/sesConfigRepository"; -import { SMTPConfig } from "./entities/smtpConfig"; -import { SMTPConfigRepository } from "./repository/smtpConfigRepository"; -import { UsersRepository } from './repository/usersRepository'; -import { Users } from "./entities/users"; -import { send } from './tests/sendSlackNotification'; -import { MustacheHelper } from './common/mustacheHelper'; -import { WebhookConfigRepository } from './repository/webhookConfigRepository'; -import { WebhookService } from './destination/destinationHandlers/webhookHandler'; -import { WebhookConfig } from './entities/webhookconfig'; -import * as process from "process"; -import bodyParser from 'body-parser'; -import {connect, NatsConnection} from "nats"; -import { register } from 'prom-client' -import {NOTIFICATION_EVENT_TOPIC} from "./pubSub/utils"; -import {PubSubServiceImpl} from "./pubSub/pubSub"; -import { failedNotificationMetricsCounter, httpRequestMetricsCounter, successNotificationMetricsCounter } from './common/metrics'; - -const app = express(); -const natsUrl = process.env.NATS_URL -app.use(bodyParser.json({ limit: '10mb' })); -app.use(express.json()); - -let logger = winston.createLogger({ - level: 'info', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.printf(info => { - return `${info.timestamp} ${info.level}: ${info.message}`; - }) - ), - transports: [new winston.transports.Console()] -}); - -let eventLogRepository: EventLogRepository = new EventLogRepository() -let eventLogBuilder: EventLogBuilder = new EventLogBuilder() -let slackConfigRepository: SlackConfigRepository = new SlackConfigRepository() -let webhookConfigRepository: WebhookConfigRepository = new WebhookConfigRepository() -let sesConfigRepository: SESConfigRepository = new SESConfigRepository() -let smtpConfigRepository: SMTPConfigRepository = new SMTPConfigRepository() -let usersRepository: UsersRepository = new UsersRepository() -let mustacheHelper: MustacheHelper = new MustacheHelper() -let slackService = new SlackService(eventLogRepository, eventLogBuilder, slackConfigRepository, logger, mustacheHelper) -let webhookService = new WebhookService(eventLogRepository, eventLogBuilder, webhookConfigRepository, logger, mustacheHelper) -let sesService = new SESService(eventLogRepository, eventLogBuilder, sesConfigRepository, usersRepository, logger, mustacheHelper) -let smtpService = new SMTPService(eventLogRepository, eventLogBuilder, smtpConfigRepository, usersRepository, logger, mustacheHelper) - -let handlers: Handler[] = [] -handlers.push(slackService) -handlers.push(webhookService) -handlers.push(sesService) -handlers.push(smtpService) - -let notificationService = new NotificationService(new EventRepository(), new NotificationSettingsRepository(), new NotificationTemplatesRepository(), handlers, logger) - -let dbHost: string = process.env.DB_HOST; -const dbPort: number = +process.env.DB_PORT; -const user: string = process.env.DB_USER; -const pwd: string = process.env.DB_PWD; -const db: string = process.env.DB; - -let dbOptions: ConnectionOptions = { - type: "postgres", - host: dbHost, - port: dbPort, - username: user, - password: pwd, - database: db, - entities: [NotificationSettings, NotifierEventLog, Event, NotificationTemplates, SlackConfig, SesConfig, SMTPConfig, WebhookConfig, Users] -} - -createConnection(dbOptions).then(async connection => { - logger.info("Connected to DB") - if(natsUrl){ - let conn: NatsConnection - (async () => { - logger.info("Connecting to NATS server..."); - conn = await connect({servers:natsUrl}) - const jsm = await conn.jetstreamManager() - const obj = new PubSubServiceImpl(conn, jsm,logger) - await obj.Subscribe(NOTIFICATION_EVENT_TOPIC, natsEventHandler) - })().catch( - (err) => { - logger.error("error occurred due to", err) - } - ) +import "reflect-metadata"; +import { createApp } from './app'; +import { connectToDatabase } from './config/database'; +import { connectToNats } from './config/nats'; +import { initializeServices } from './services/serviceInitializer'; +import { logger } from './config/logger'; + +const PORT = process.env.PORT || 3000; + +const startServer = async () => { + try { + // Initialize database connection + await connectToDatabase(); + + // Initialize services + const { notificationService } = initializeServices(); + + // Connect to NATS if configured + await connectToNats(notificationService); + + // Create and start the Express app + const app = createApp(notificationService); + + app.listen(PORT, () => { + logger.info(`Notifier app listening on port ${PORT}!`); + }); + } catch (error) { + logger.error('Failed to start server:', error); + process.exit(1); } -}).catch(error => { - logger.error("TypeORM connection error: ", error); - logger.error("shutting down notifier due to un-successful database connection...") - process.exit(1) -}); - -const natsEventHandler = async (msg: string) => { - const eventAsString = JSON.parse(msg) - const event = JSON.parse(eventAsString) as Event - logger.info({natsEventBody: event}) - const response = await notificationService.sendNotification(event) - if (response.status != 0){ - successNotificationMetricsCounter.inc() - } else{ - failedNotificationMetricsCounter.inc() - } -} - -// Request counter for all endpoints -app.use((req, res, next) => { - httpRequestMetricsCounter.labels({method: req.method, endpoint: req.url, statusCode: res.statusCode}).inc() - next() - }) - -app.get('/', (req, res) => { - res.send('Welcome to notifier Notifier!') -}) - -app.get('/health', (req, res) => { - res.status(200).send("healthy") -}) - -app.get('/test', (req, res) => { - send(); - res.send('Test!'); -}) - -app.post('/notify', async(req, res) => { - logger.info("notifications Received") - const response=await notificationService.sendNotification(req.body); - if (response.status!=0){ - res.status(response.status).json({message:response.message}).send() - successNotificationMetricsCounter.inc() - }else{ - res.status(response.error.statusCode).json({message:response.error.message}).send() - failedNotificationMetricsCounter.inc() - } -}); - -app.get('/metrics', async (req, res) => { - res.setHeader('Content-Type', register.contentType) - res.send(await register.metrics()) -}); +}; -app.listen(3000, () => logger.info('Notifier app listening on port 3000!')) +// Start the server +startServer(); diff --git a/src/services/serviceInitializer.ts b/src/services/serviceInitializer.ts new file mode 100644 index 0000000..196cfce --- /dev/null +++ b/src/services/serviceInitializer.ts @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NotificationService, Handler } from '../notification/service/notificationService'; +import { EventLogRepository } from '../repository/notifierEventLogRepository'; +import { EventLogBuilder } from '../common/eventLogBuilder'; +import { SlackConfigRepository } from '../repository/slackConfigRepository'; +import { WebhookConfigRepository } from '../repository/webhookConfigRepository'; +import { SESConfigRepository } from '../repository/sesConfigRepository'; +import { SMTPConfigRepository } from '../repository/smtpConfigRepository'; +import { UsersRepository } from '../repository/usersRepository'; +import { MustacheHelper } from '../common/mustacheHelper'; +import { SlackService } from '../destination/destinationHandlers/slackHandler'; +import { WebhookService } from '../destination/destinationHandlers/webhookHandler'; +import { SESService } from '../destination/destinationHandlers/sesHandler'; +import { SMTPService } from '../destination/destinationHandlers/smtpHandler'; +import { EventRepository } from '../repository/eventsRepository'; +import { NotificationSettingsRepository } from '../repository/notificationSettingsRepository'; +import { NotificationTemplatesRepository } from '../repository/templatesRepository'; +import { logger } from '../config/logger'; + +export const initializeServices = () => { + // Initialize repositories + const eventLogRepository = new EventLogRepository(); + const eventLogBuilder = new EventLogBuilder(); + const slackConfigRepository = new SlackConfigRepository(); + const webhookConfigRepository = new WebhookConfigRepository(); + const sesConfigRepository = new SESConfigRepository(); + const smtpConfigRepository = new SMTPConfigRepository(); + const usersRepository = new UsersRepository(); + const eventRepository = new EventRepository(); + const notificationSettingsRepository = new NotificationSettingsRepository(); + const notificationTemplatesRepository = new NotificationTemplatesRepository(); + + // Initialize helpers + const mustacheHelper = new MustacheHelper(); + + // Initialize services + const slackService = new SlackService( + eventLogRepository, + eventLogBuilder, + slackConfigRepository, + logger, + mustacheHelper + ); + + const webhookService = new WebhookService( + eventLogRepository, + eventLogBuilder, + webhookConfigRepository, + logger, + mustacheHelper + ); + + const sesService = new SESService( + eventLogRepository, + eventLogBuilder, + sesConfigRepository, + usersRepository, + logger, + mustacheHelper + ); + + const smtpService = new SMTPService( + eventLogRepository, + eventLogBuilder, + smtpConfigRepository, + usersRepository, + logger, + mustacheHelper + ); + + // Combine handlers + const handlers: Handler[] = [ + slackService, + webhookService, + sesService, + smtpService + ]; + + // Create notification service + const notificationService = new NotificationService( + eventRepository, + notificationSettingsRepository, + notificationTemplatesRepository, + handlers, + logger + ); + + return { + notificationService, + // Export other services if needed + }; +}; From 1ae839f3c2925b0b69779ce7cf96280390936177 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Tue, 15 Apr 2025 19:07:34 +0530 Subject: [PATCH 2/8] sendNotification refactored --- .../service/notificationService.ts | 586 +++++++++++++++--- src/tests/notificationService.test.ts | 301 +++++++++ 2 files changed, 805 insertions(+), 82 deletions(-) create mode 100644 src/tests/notificationService.test.ts diff --git a/src/notification/service/notificationService.ts b/src/notification/service/notificationService.ts index 31de056..4beff3c 100644 --- a/src/notification/service/notificationService.ts +++ b/src/notification/service/notificationService.ts @@ -137,109 +137,531 @@ class NotificationService { } } - public async sendNotification(event: Event):Promise { + /** + * Main function to send notifications based on event type + * @param event The event to send notifications for + * @returns CustomResponse with status and message + */ + public async sendNotification(event: Event): Promise { try { + this.logger.info(`Processing notification for event type: ${event.eventTypeId}, correlationId: ${event.correlationId}`); + + // Handle approval notifications if (event.payload.providers && event.payload.providers.length > 0) { - await this.sendApprovalNotification(event) - return new CustomResponse("notification sent",200) + this.logger.info(`Processing approval notification with ${event.payload.providers.length} providers`); + await this.sendApprovalNotification(event); + this.logger.info(`Approval notification sent successfully`); + return new CustomResponse("notification sent", 200); + } + + // Handle scoop notification events + if (event.eventTypeId == EVENT_TYPE.ScoopNotification) { + this.logger.info(`Processing scoop notification event`); + return await this.handleScoopNotification(event); } - // check webhook for scoop notification event type - if (event.eventTypeId == EVENT_TYPE.ScoopNotification && event.payload.scoopNotificationConfig.webhookConfig) { - await this.sendWebhookNotification(event) - return new CustomResponse("notification sent",200) + // Handle regular notifications + this.logger.info(`Processing regular notification event`); + return await this.handleRegularNotification(event); + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + const errorStack = error.stack || ''; + this.logger.error(`Error in sendNotification: ${errorMessage}\nStack: ${errorStack}`); + + if (error instanceof CustomError) { + this.logger.error(`CustomError with status code: ${error.statusCode}`); + return new CustomResponse("", 0, error); + } else { + const customError = new CustomError(errorMessage, 400); + this.logger.error(`Converted to CustomError with status code: 400`); + return new CustomResponse("", 0, customError); } + } + } + + /** + * Handle scoop notification events (webhook or slack) + * @param event The scoop notification event + * @returns CustomResponse with status and message + */ + private async handleScoopNotification(event: Event): Promise { + try { + this.logger.info(`Handling scoop notification for event ID: ${event.correlationId}`); - // check slack for scoop notification event type - if (event.eventTypeId == EVENT_TYPE.ScoopNotification && event.payload.scoopNotificationConfig.slackConfig) { - await this.sendSlackNotification(event) - return new CustomResponse("notification sent",200) + // Check webhook for scoop notification event type + if (event.payload.scoopNotificationConfig.webhookConfig) { + this.logger.info(`Found webhook config in scoop notification, processing...`); + await this.sendWebhookNotification(event); + this.logger.info(`Webhook notification sent successfully`); + return new CustomResponse("notification sent", 200); } + // Check slack for scoop notification event type + if (event.payload.scoopNotificationConfig.slackConfig) { + this.logger.info(`Found slack config in scoop notification, processing...`); + await this.sendSlackNotification(event); + this.logger.info(`Slack notification sent successfully`); + return new CustomResponse("notification sent", 200); + } + + this.logger.error(`No valid webhook or slack configuration found in scoop notification`); + return new CustomResponse("", 0, new CustomError("No valid notification configuration found for scoop notification", 400)); + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + this.logger.error(`Error in handleScoopNotification: ${errorMessage}`); + if (error.stack) { + this.logger.error(`Stack trace: ${error.stack}`); + } + throw error; // Let the parent function handle the error + } + } + + /** + * Handle regular notification events + * @param event The regular notification event + * @returns CustomResponse with status and message + */ + private async handleRegularNotification(event: Event): Promise { + try { + this.logger.info(`Handling regular notification for event ID: ${event.correlationId}, type: ${event.eventTypeId}`); + // Validate event if (!this.isValidEvent(event)) { - throw new CustomError("Event is not valid", 400) + this.logger.error(`Invalid event: ${JSON.stringify({ + eventTypeId: event.eventTypeId, + pipelineType: event.pipelineType, + correlationId: event.correlationId, + hasPayload: !!event.payload, + hasBaseUrl: !!event.baseUrl + })}`); + throw new CustomError("Event is not valid", 400); } + this.logger.info(`Event validation passed`); - const settingsResults=await this.notificationSettingsRepository.findByEventSource(event.pipelineType, event.pipelineId, event.eventTypeId, event.appId, event.envId, event.teamId, event.clusterId, event.isProdEnv,event.envIdsForCiPipeline); - this.logger.info('notificationSettingsRepository.findByEventSource') - if (!settingsResults || settingsResults.length == 0) { - this.logger.info("no notification settings found for event " + event.correlationId); - return new CustomResponse("",0,new CustomError("no notification settings found for event",404)) + // Get notification settings + this.logger.info(`Finding notification settings for event: ${event.correlationId}`); + const settingsResults = await this.findNotificationSettings(event); + if (!settingsResults || settingsResults.length == 0) { + this.logger.warn(`No notification settings found for event ${event.correlationId}`); + return new CustomResponse("", 0, new CustomError("no notification settings found for event", 404)); + } + this.logger.info(`Found ${settingsResults.length} notification settings`); + + // Process notification settings + this.logger.info(`Preparing notification maps`); + const { destinationMap, configsMap } = this.prepareNotificationMaps(settingsResults); + + // Process each setting + this.logger.info(`Processing ${settingsResults.length} notification settings`); + for (let i = 0; i < settingsResults.length; i++) { + const setting = settingsResults[i]; + this.logger.info(`Processing notification setting ${i+1}/${settingsResults.length}, ID: ${setting.id}`); + const result = await this.processNotificationSetting(event, setting, configsMap, destinationMap); + if (result.status === 0) { + this.logger.error(`Error processing notification setting: ${result.error?.message}`); + return result; // Return error if any } - let destinationMap = new Map(); - let configsMap = new Map(); - this.logger.info("notification settings "); - this.logger.info(JSON.stringify(settingsResults)) - settingsResults.forEach((setting) => { - const providerObjects = setting.config - const providersSet = new Set(providerObjects); - providersSet.forEach(p => { - let id = p['dest'] + '-' + p['configId'] - configsMap.set(id, false) - }); + } + + this.logger.info(`All notifications processed successfully`); + return new CustomResponse("notification sent", 200); + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + this.logger.error(`Error in handleRegularNotification: ${errorMessage}`); + if (error.stack) { + this.logger.error(`Stack trace: ${error.stack}`); + } + throw error; // Let the parent function handle the error + } + } + + /** + * Find notification settings for an event + * @param event The event to find settings for + * @returns Array of notification settings + */ + private async findNotificationSettings(event: Event): Promise { + try { + this.logger.info(`Finding notification settings for event ID: ${event.correlationId}`); + this.logger.info(`Search parameters: pipelineType=${event.pipelineType}, pipelineId=${event.pipelineId}, ` + + `eventTypeId=${event.eventTypeId}, appId=${event.appId}, envId=${event.envId}, ` + + `teamId=${event.teamId}, clusterId=${event.clusterId}, isProdEnv=${event.isProdEnv}`); + + if (event.envIdsForCiPipeline && event.envIdsForCiPipeline.length > 0) { + this.logger.info(`Additional envIdsForCiPipeline: ${event.envIdsForCiPipeline.join(', ')}`); + } + + const settings = await this.notificationSettingsRepository.findByEventSource( + event.pipelineType, + event.pipelineId, + event.eventTypeId, + event.appId, + event.envId, + event.teamId, + event.clusterId, + event.isProdEnv, + event.envIdsForCiPipeline + ); + + this.logger.info(`Found ${settings ? settings.length : 0} notification settings`); + return settings; + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + this.logger.error(`Error in findNotificationSettings: ${errorMessage}`); + if (error.stack) { + this.logger.error(`Stack trace: ${error.stack}`); + } + throw error; // Let the parent function handle the error + } + } + + /** + * Prepare notification maps for tracking destinations and configs + * @param settingsResults The notification settings + * @returns Object containing destinationMap and configsMap + */ + private prepareNotificationMaps(settingsResults: NotificationSettings[]): { destinationMap: Map, configsMap: Map } { + try { + this.logger.info(`Preparing notification maps for ${settingsResults.length} settings`); + let destinationMap = new Map(); + let configsMap = new Map(); + + // Log settings at debug level to avoid excessive logging in production + if (this.logger.level === 'debug') { + this.logger.debug(`Notification settings details: ${JSON.stringify(settingsResults)}`); + } else { + this.logger.info(`Processing ${settingsResults.length} notification settings (set log level to debug for details)`); + } + + let configCount = 0; + settingsResults.forEach((setting, index) => { + this.logger.info(`Processing setting ${index+1}/${settingsResults.length}, ID: ${setting.id}`); + const providerObjects = setting.config; + + if (!providerObjects) { + this.logger.warn(`No provider objects found in setting ID: ${setting.id}`); + return; + } + + const providersSet = new Set(providerObjects); + this.logger.info(`Found ${providersSet.size} unique providers in setting ID: ${setting.id}`); + + providersSet.forEach(p => { + if (!p['dest'] || !p['configId']) { + this.logger.warn(`Invalid provider found: ${JSON.stringify(p)}`); + return; + } + + let id = p['dest'] + '-' + p['configId']; + configsMap.set(id, false); + configCount++; + this.logger.info(`Added config to map: ${id}`); }); + }); - for (const setting of settingsResults) { - - const configArray = setting.config as any; - if (Array.isArray(configArray)) { - const webhookConfig = configArray.filter((config) => config.dest === 'webhook'); - - if (webhookConfig.length) { - const webhookConfigRepository = new WebhookConfigRepository(); - for (const config of webhookConfig) { - const templateResults: WebhookConfig[] = await webhookConfigRepository.getAllWebhookConfigs() - const newTemplateResult = templateResults.filter((t) => t.id === config.configId); - - if (newTemplateResult.length === 0) { - this.logger.info("no templates found for event ", event); - return new CustomResponse("",0,new CustomError("no templates found for event", 404)); - } - - let ImageScanEvent = JSON.parse(JSON.stringify(event)); - if (!!event.payload.imageScanExecutionInfo) { - ImageScanEvent.payload.imageScanExecutionInfo = JSON.parse(JSON.stringify(event.payload.imageScanExecutionInfo[setting.id] ?? {})); - } - for (const h of this.handlers) { - if (h instanceof WebhookService) { - if (event.eventTypeId === EVENT_TYPE.ImageScan && !!event.payload.imageScanExecutionInfo) { - await h.handle(ImageScanEvent, newTemplateResult, setting, configsMap, destinationMap); - } - await h.handle(event, newTemplateResult, setting, configsMap, destinationMap); - } - } - }; - }; - if (configArray.length > webhookConfig.length) { - const templateResults: NotificationTemplates[] = await this.templatesRepository.findByEventTypeIdAndNodeType(event.eventTypeId, event.pipelineType) - if (!templateResults) { - this.logger.info("no templates found for event ", event); - return new CustomResponse("",0,new CustomError("no templates found for event", 404)); - } - for (let h of this.handlers) { - await h.handle(event, templateResults, setting, configsMap, destinationMap) - } - } + this.logger.info(`Notification maps prepared with ${configCount} total configs`); + return { destinationMap, configsMap }; + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + this.logger.error(`Error in prepareNotificationMaps: ${errorMessage}`); + if (error.stack) { + this.logger.error(`Stack trace: ${error.stack}`); + } + throw error; // Let the parent function handle the error + } + } + + /** + * Process a single notification setting + * @param event The event to process + * @param setting The notification setting to process + * @param configsMap Map of configs that have been processed + * @param destinationMap Map of destinations that have been processed + * @returns CustomResponse with status and message + */ + private async processNotificationSetting( + event: Event, + setting: NotificationSettings, + configsMap: Map, + destinationMap: Map + ): Promise { + try { + this.logger.info(`Processing notification setting ID: ${setting.id}, event type: ${setting.event_type_id}`); + + const configArray = setting.config as any; + if (!Array.isArray(configArray)) { + this.logger.warn(`Config is not an array for setting ID: ${setting.id}, skipping processing`); + return new CustomResponse("notification sent", 200); + } + + this.logger.info(`Found ${configArray.length} configurations in setting`); + + // Handle webhook configurations + const webhookConfig = configArray.filter((config) => config.dest === 'webhook'); + if (webhookConfig.length) { + this.logger.info(`Found ${webhookConfig.length} webhook configurations, processing...`); + const result = await this.processWebhookConfigs(event, setting, webhookConfig, configsMap, destinationMap); + if (result.status === 0) { + this.logger.error(`Error processing webhook configs: ${result.error?.message}`); + return result; // Return error if any + } + this.logger.info(`Webhook configurations processed successfully`); + } + + // Handle other configurations if there are any + if (configArray.length > webhookConfig.length) { + const otherConfigsCount = configArray.length - webhookConfig.length; + this.logger.info(`Found ${otherConfigsCount} non-webhook configurations, processing...`); + const result = await this.processOtherConfigs(event, setting, configsMap, destinationMap); + if (result.status === 0) { + this.logger.error(`Error processing other configs: ${result.error?.message}`); + return result; // Return error if any + } + this.logger.info(`Other configurations processed successfully`); + } + + this.logger.info(`All configurations in setting ID: ${setting.id} processed successfully`); + return new CustomResponse("notification sent", 200); + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + this.logger.error(`Error in processNotificationSetting for setting ID ${setting.id}: ${errorMessage}`); + if (error.stack) { + this.logger.error(`Stack trace: ${error.stack}`); + } + throw error; // Let the parent function handle the error + } + } + + /** + * Process webhook configurations + * @param event The event to process + * @param setting The notification setting + * @param webhookConfig Array of webhook configurations + * @param configsMap Map of configs that have been processed + * @param destinationMap Map of destinations that have been processed + * @returns CustomResponse with status and message + */ + private async processWebhookConfigs( + event: Event, + setting: NotificationSettings, + webhookConfig: any[], + configsMap: Map, + destinationMap: Map + ): Promise { + try { + this.logger.info(`Processing ${webhookConfig.length} webhook configurations for event ID: ${event.correlationId}`); + const webhookConfigRepository = new WebhookConfigRepository(); + + for (let i = 0; i < webhookConfig.length; i++) { + const config = webhookConfig[i]; + this.logger.info(`Processing webhook config ${i+1}/${webhookConfig.length}, configId: ${config.configId}`); + + this.logger.info(`Fetching webhook templates from repository`); + const templateResults: WebhookConfig[] = await webhookConfigRepository.getAllWebhookConfigs(); + this.logger.info(`Found ${templateResults.length} webhook templates in repository`); + + const newTemplateResult = templateResults.filter((t) => t.id === config.configId); + this.logger.info(`Filtered ${newTemplateResult.length} matching templates for configId: ${config.configId}`); + + if (newTemplateResult.length === 0) { + this.logger.error(`No templates found for event ${event.correlationId} with configId: ${config.configId}`); + return new CustomResponse("", 0, new CustomError("no templates found for event", 404)); + } + + this.logger.info(`Processing webhook handlers for configId: ${config.configId}`); + await this.processWebhookHandlers(event, newTemplateResult, setting, configsMap, destinationMap); + this.logger.info(`Webhook handlers processed successfully for configId: ${config.configId}`); + } + + this.logger.info(`All webhook configurations processed successfully`); + return new CustomResponse("notification sent", 200); + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + this.logger.error(`Error in processWebhookConfigs: ${errorMessage}`); + if (error.stack) { + this.logger.error(`Stack trace: ${error.stack}`); + } + throw error; // Let the parent function handle the error + } + } + + /** + * Process webhook handlers for an event + * @param event The event to process + * @param templates The webhook templates + * @param setting The notification setting + * @param configsMap Map of configs that have been processed + * @param destinationMap Map of destinations that have been processed + */ + private async processWebhookHandlers( + event: Event, + templates: WebhookConfig[], + setting: NotificationSettings, + configsMap: Map, + destinationMap: Map + ): Promise { + try { + this.logger.info(`Processing webhook handlers for event ID: ${event.correlationId}, setting ID: ${setting.id}`); + this.logger.info(`Using ${templates.length} webhook templates`); + + let imageScanEvent = JSON.parse(JSON.stringify(event)); + if (!!event.payload.imageScanExecutionInfo) { + this.logger.info(`Event is an image scan event, preparing specialized payload for setting ID: ${setting.id}`); + imageScanEvent.payload.imageScanExecutionInfo = JSON.parse(JSON.stringify(event.payload.imageScanExecutionInfo[setting.id] ?? {})); + } + + let webhookHandlerFound = false; + for (const h of this.handlers) { + if (h instanceof WebhookService) { + webhookHandlerFound = true; + this.logger.info(`Found webhook handler, processing templates`); + + if (event.eventTypeId === EVENT_TYPE.ImageScan && !!event.payload.imageScanExecutionInfo) { + this.logger.info(`Processing image scan event with specialized payload`); + await h.handle(imageScanEvent, templates, setting, configsMap, destinationMap); + this.logger.info(`Image scan event processed successfully`); } - }; - this.logger.info("notification sent"); - return new CustomResponse("notification sent",200) - }catch (error:any){ - return await error instanceof CustomError?new CustomResponse("",0,error):new CustomResponse("",0,new CustomError(error.message,400)) + + this.logger.info(`Processing regular webhook notification`); + await h.handle(event, templates, setting, configsMap, destinationMap); + this.logger.info(`Regular webhook notification processed successfully`); + } + } + + if (!webhookHandlerFound) { + this.logger.warn(`No webhook handlers found for processing templates`); + } + + this.logger.info(`Webhook handlers processing completed`); + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + this.logger.error(`Error in processWebhookHandlers: ${errorMessage}`); + if (error.stack) { + this.logger.error(`Stack trace: ${error.stack}`); + } + throw error; // Let the parent function handle the error } } - private isValidEvent(event: Event) { - if ((event.eventTypeId && event.pipelineType && event.correlationId && event.payload && event.baseUrl) || (event.eventTypeId == EVENT_TYPE.ScoopNotification)) - return true; - return false; + /** + * Process other (non-webhook) configurations + * @param event The event to process + * @param setting The notification setting + * @param configsMap Map of configs that have been processed + * @param destinationMap Map of destinations that have been processed + * @returns CustomResponse with status and message + */ + private async processOtherConfigs( + event: Event, + setting: NotificationSettings, + configsMap: Map, + destinationMap: Map + ): Promise { + try { + this.logger.info(`Processing other configurations for event ID: ${event.correlationId}, setting ID: ${setting.id}`); + this.logger.info(`Finding templates for event type: ${event.eventTypeId}, pipeline type: ${event.pipelineType}`); + + const templateResults: NotificationTemplates[] = await this.templatesRepository.findByEventTypeIdAndNodeType( + event.eventTypeId, + event.pipelineType + ); + + if (!templateResults) { + this.logger.error(`No templates found for event ${event.correlationId}, event type: ${event.eventTypeId}, pipeline type: ${event.pipelineType}`); + return new CustomResponse("", 0, new CustomError("no templates found for event", 404)); + } + + this.logger.info(`Found ${templateResults.length} templates, processing with handlers`); + + let handlerCount = 0; + for (let h of this.handlers) { + handlerCount++; + this.logger.info(`Processing with handler ${handlerCount}/${this.handlers.length}`); + await h.handle(event, templateResults, setting, configsMap, destinationMap); + this.logger.info(`Handler ${handlerCount} processed successfully`); + } + + this.logger.info(`All handlers processed successfully for other configurations`); + return new CustomResponse("notification sent", 200); + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + this.logger.error(`Error in processOtherConfigs: ${errorMessage}`); + if (error.stack) { + this.logger.error(`Stack trace: ${error.stack}`); + } + throw error; // Let the parent function handle the error + } } - private isValidEventForApproval(event: Event) { - if (event.eventTypeId && event.correlationId && event.payload && (event.baseUrl || event.eventTypeId == EVENT_TYPE.ScoopNotification)) { - return true; + + /** + * Validate if an event has all required fields + * @param event The event to validate + * @returns boolean indicating if the event is valid + */ + private isValidEvent(event: Event): boolean { + try { + // Check if it's a scoop notification event (special case) + if (event.eventTypeId == EVENT_TYPE.ScoopNotification) { + this.logger.info(`Event is a scoop notification, validation passed`); + return true; + } + + // Check required fields for regular events + const missingFields = []; + if (!event.eventTypeId) missingFields.push('eventTypeId'); + if (!event.pipelineType) missingFields.push('pipelineType'); + if (!event.correlationId) missingFields.push('correlationId'); + if (!event.payload) missingFields.push('payload'); + if (!event.baseUrl) missingFields.push('baseUrl'); + + const isValid = missingFields.length === 0; + + if (!isValid) { + this.logger.error(`Event validation failed, missing fields: ${missingFields.join(', ')}`); + } else { + this.logger.info(`Event validation passed, all required fields present`); + } + + return isValid; + } catch (error: any) { + this.logger.error(`Error in isValidEvent: ${error.message || 'Unknown error'}`); + return false; + } + } + + /** + * Validate if an event has all required fields for approval + * @param event The event to validate + * @returns boolean indicating if the event is valid for approval + */ + private isValidEventForApproval(event: Event): boolean { + try { + // Check if it's a scoop notification event (special case) + if (event.eventTypeId == EVENT_TYPE.ScoopNotification && event.correlationId && event.payload) { + this.logger.info(`Event is a scoop notification for approval, validation passed`); + return true; + } + + // Check required fields for regular approval events + const missingFields = []; + if (!event.eventTypeId) missingFields.push('eventTypeId'); + if (!event.correlationId) missingFields.push('correlationId'); + if (!event.payload) missingFields.push('payload'); + if (!event.baseUrl) missingFields.push('baseUrl'); + + const isValid = missingFields.length === 0; + + if (!isValid) { + this.logger.error(`Approval event validation failed, missing fields: ${missingFields.join(', ')}`); + } else { + this.logger.info(`Approval event validation passed, all required fields present`); + } + + return isValid; + } catch (error: any) { + this.logger.error(`Error in isValidEventForApproval: ${error.message || 'Unknown error'}`); + return false; } - return false; } } diff --git a/src/tests/notificationService.test.ts b/src/tests/notificationService.test.ts new file mode 100644 index 0000000..9567412 --- /dev/null +++ b/src/tests/notificationService.test.ts @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { NotificationService, Event } from '../notification/service/notificationService'; +import { CustomError, CustomResponse } from '../entities/events'; +import { EVENT_TYPE } from '../common/types'; + +// Sample real event based on the provided example +const sampleEvent: Event = { + eventTypeId: 1, + pipelineId: 77, + pipelineType: "CI", + correlationId: "84fbc6d6-d874-4c47-9e84-3825dd68b501", + payload: { + appName: "amit-test", + envName: "", + pipelineName: "ci-93-j324", + source: "", + dockerImageUrl: "", + triggeredBy: "admin", + stage: "", + deploymentHistoryLink: "", + appDetailLink: "", + downloadLink: "", + buildHistoryLink: "/dashboard/app/93/ci-details/77/274/artifacts", + material: { + gitTriggers: { + "76": { + Commit: "a61f69d7889eae63bbfe95d87f5dde1e4d090cad", + Author: "Badal Kumar Prusty ", + Date: "2025-04-08T07:28:12Z", + Message: "added react project\n", + Changes: null, + WebhookData: { + id: 0, + eventActionType: "", + data: null + }, + CiConfigureSourceValue: "main", + GitRepoUrl: "https://github.com/badal773/test.git", + GitRepoName: "test", + CiConfigureSourceType: "SOURCE_TYPE_BRANCH_FIXED" + } + }, + ciMaterials: [ + { + id: 76, + gitMaterialId: 62, + gitMaterialUrl: "", + gitMaterialName: "test", + type: "SOURCE_TYPE_BRANCH_FIXED", + value: "main", + active: true, + lastFetchTime: "0001-01-01T00:00:00Z", + isRepoError: false, + repoErrorMsg: "", + isBranchError: false, + branchErrorMsg: "", + url: "https://github.com/badal773/test.git" + } + ] + }, + approvedByEmail: null, + failureReason: "", + providers: null, + imageTagNames: null, + imageComment: "", + imageApprovalLink: "", + protectConfigFileType: "", + protectConfigFileName: "", + protectConfigComment: "", + protectConfigLink: "", + approvalLink: "", + timeWindowComment: "", + imageScanExecutionInfo: null, + artifactPromotionRequestViewLink: "", + artifactPromotionApprovalLink: "", + promotionArtifactSource: "", + scoopNotificationConfig: null + }, + eventTime: "2025-04-10T08:34:39Z", + teamId: 8, + appId: 93, + envId: 0, + isProdEnv: false, + clusterId: 0, + baseUrl: "https://devtron-ent-2.devtron.info", + envIdsForCiPipeline: null +}; + +// Create a sample approval event +const approvalEvent: Event = { + ...sampleEvent, + payload: { + ...sampleEvent.payload, + providers: [{ dest: 'email', configId: 1 }] + } +}; + +// Create a sample scoop notification event with webhook config +const scoopWebhookEvent: Event = { + ...sampleEvent, + eventTypeId: EVENT_TYPE.ScoopNotification, + payload: { + ...sampleEvent.payload, + scoopNotificationConfig: { + webhookConfig: { url: 'http://example.com' } + } + } +}; + +// Create a sample scoop notification event with slack config +const scoopSlackEvent: Event = { + ...sampleEvent, + eventTypeId: EVENT_TYPE.ScoopNotification, + payload: { + ...sampleEvent.payload, + scoopNotificationConfig: { + slackConfig: { webhookUrl: 'http://example.com' } + } + } +}; + +// Create an invalid event (missing required fields) +const invalidEvent: Event = { + eventTypeId: 1, + pipelineId: 1, + // Missing pipelineType + payload: {}, + eventTime: "2025-04-10T08:34:39Z", + appId: 1, + envId: 1, + teamId: 1, + clusterId: 1, + isProdEnv: false + // Missing baseUrl +}; + +describe('NotificationService', () => { + describe('sendNotification', () => { + it('should handle different types of events correctly', async () => { + // Create a test class that extends NotificationService + class TestNotificationService extends NotificationService { + // Track method calls + public sendApprovalNotificationCalled = false; + public handleScoopNotificationResult: CustomResponse | null = null; + public handleRegularNotificationResult: CustomResponse | null = null; + + // Override public methods + public async sendApprovalNotification(event: Event): Promise { + this.sendApprovalNotificationCalled = true; + return Promise.resolve(); + } + + // Override protected methods by making them public in the test class + public async testHandleScoopNotification(event: Event): Promise { + this.handleScoopNotificationResult = new CustomResponse("notification sent", 200); + return this.handleScoopNotificationResult; + } + + public async testHandleRegularNotification(event: Event): Promise { + if (event === invalidEvent) { + this.handleRegularNotificationResult = new CustomResponse("", 0, new CustomError("Event is not valid", 400)); + } else { + this.handleRegularNotificationResult = new CustomResponse("notification sent", 200); + } + return this.handleRegularNotificationResult; + } + + // Override the main method to use our test methods + public async sendNotification(event: Event): Promise { + try { + // Handle approval notifications + if (event.payload.providers && event.payload.providers.length > 0) { + await this.sendApprovalNotification(event); + return new CustomResponse("notification sent", 200); + } + + // Handle scoop notification events + if (event.eventTypeId == EVENT_TYPE.ScoopNotification) { + return await this.testHandleScoopNotification(event); + } + + // Handle regular notifications + return await this.testHandleRegularNotification(event); + } catch (error: any) { + return error instanceof CustomError + ? new CustomResponse("", 0, error) + : new CustomResponse("", 0, new CustomError(error.message, 400)); + } + } + + // Add test methods to check private methods + public testIsValidEvent(event: Event): boolean { + // Reimplement the private method for testing + if ((event.eventTypeId && event.pipelineType && event.correlationId && event.payload && event.baseUrl) || + (event.eventTypeId == EVENT_TYPE.ScoopNotification)) { + return true; + } + return false; + } + + public testIsValidEventForApproval(event: Event): boolean { + // Reimplement the private method for testing + if (event.eventTypeId && event.correlationId && event.payload && + (event.baseUrl || event.eventTypeId == EVENT_TYPE.ScoopNotification)) { + return true; + } + return false; + } + } + + // Create instance with null dependencies (we're not using them in the test) + const service = new TestNotificationService(null, null, null, [], null); + + // Test approval notification + const approvalResult = await service.sendNotification(approvalEvent); + expect(service.sendApprovalNotificationCalled).to.be.true; + expect(approvalResult.status).to.equal(200); + expect(approvalResult.message).to.equal("notification sent"); + + // Test scoop notification with webhook + const scoopResult = await service.sendNotification(scoopWebhookEvent); + expect(service.handleScoopNotificationResult).to.not.be.null; + expect(scoopResult.status).to.equal(200); + expect(scoopResult.message).to.equal("notification sent"); + + // Test regular notification + const regularResult = await service.sendNotification(sampleEvent); + expect(service.handleRegularNotificationResult).to.not.be.null; + expect(regularResult.status).to.equal(200); + expect(regularResult.message).to.equal("notification sent"); + + // Test invalid event + const invalidResult = await service.sendNotification(invalidEvent); + expect(invalidResult.status).to.equal(0); + expect(invalidResult.error).to.be.an.instanceOf(CustomError); + expect(invalidResult.error.message).to.equal("Event is not valid"); + }); + }); + + describe('Event validation', () => { + it('should validate events correctly', () => { + class TestNotificationService extends NotificationService { + public testIsValidEvent(event: Event): boolean { + // Reimplement the private method for testing + if ((event.eventTypeId && event.pipelineType && event.correlationId && event.payload && event.baseUrl) || + (event.eventTypeId == EVENT_TYPE.ScoopNotification)) { + return true; + } + return false; + } + + public testIsValidEventForApproval(event: Event): boolean { + // Reimplement the private method for testing + if (event.eventTypeId && event.correlationId && event.payload && + (event.baseUrl || event.eventTypeId == EVENT_TYPE.ScoopNotification)) { + return true; + } + return false; + } + } + + const service = new TestNotificationService(null, null, null, [], null); + + // Test valid event + expect(service.testIsValidEvent(sampleEvent)).to.be.true; + + // Test scoop notification event + expect(service.testIsValidEvent(scoopWebhookEvent)).to.be.true; + + // Test invalid event + expect(service.testIsValidEvent(invalidEvent)).to.be.false; + + // Test valid approval event + expect(service.testIsValidEventForApproval(sampleEvent)).to.be.true; + + // Test scoop approval event + expect(service.testIsValidEventForApproval(scoopWebhookEvent)).to.be.true; + + // Test invalid approval event + const invalidApprovalEvent = { ...sampleEvent, correlationId: undefined, baseUrl: undefined }; + expect(service.testIsValidEventForApproval(invalidApprovalEvent)).to.be.false; + }); + }); +}); From 571a6171c76f7b80baf782f4a543047a148af470 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Wed, 16 Apr 2025 13:25:42 +0530 Subject: [PATCH 3/8] sendNotificationV2 added --- .../service/notificationService.ts | 79 +++++++++++++++++++ src/routes/notificationRoutes.ts | 21 +++++ 2 files changed, 100 insertions(+) diff --git a/src/notification/service/notificationService.ts b/src/notification/service/notificationService.ts index 4beff3c..c8b2529 100644 --- a/src/notification/service/notificationService.ts +++ b/src/notification/service/notificationService.ts @@ -179,6 +179,85 @@ class NotificationService { } } + /** + * Enhanced function to send notifications with pre-provided notification settings + * @param event The event to send notifications for + * @param notificationSettings The pre-provided notification settings + * @returns CustomResponse with status and message + */ + public async sendNotificationV2(event: Event, notificationSettings: NotificationSettings[]): Promise { + try { + this.logger.info(`Processing notification V2 for event type: ${event.eventTypeId}, correlationId: ${event.correlationId}`); + this.logger.info(`Using ${notificationSettings.length} pre-provided notification settings`); + + // Handle approval notifications + if (event.payload.providers && event.payload.providers.length > 0) { + this.logger.info(`Processing approval notification with ${event.payload.providers.length} providers`); + await this.sendApprovalNotification(event); + this.logger.info(`Approval notification sent successfully`); + return new CustomResponse("notification sent", 200); + } + + // Handle scoop notification events + if (event.eventTypeId == EVENT_TYPE.ScoopNotification) { + this.logger.info(`Processing scoop notification event`); + return await this.handleScoopNotification(event); + } + + // Validate event + if (!this.isValidEvent(event)) { + this.logger.error(`Invalid event: ${JSON.stringify({ + eventTypeId: event.eventTypeId, + pipelineType: event.pipelineType, + correlationId: event.correlationId, + hasPayload: !!event.payload, + hasBaseUrl: !!event.baseUrl + })}`); + throw new CustomError("Event is not valid", 400); + } + this.logger.info(`Event validation passed`); + + // Check if notification settings are provided + if (!notificationSettings || notificationSettings.length === 0) { + this.logger.warn(`No notification settings provided for event ${event.correlationId}`); + return new CustomResponse("", 0, new CustomError("no notification settings provided", 400)); + } + this.logger.info(`Found ${notificationSettings.length} notification settings`); + + // Process notification settings + this.logger.info(`Preparing notification maps`); + const { destinationMap, configsMap } = this.prepareNotificationMaps(notificationSettings); + + // Process each setting + this.logger.info(`Processing ${notificationSettings.length} notification settings`); + for (let i = 0; i < notificationSettings.length; i++) { + const setting = notificationSettings[i]; + this.logger.info(`Processing notification setting ${i+1}/${notificationSettings.length}, ID: ${setting.id}`); + const result = await this.processNotificationSetting(event, setting, configsMap, destinationMap); + if (result.status === 0) { + this.logger.error(`Error processing notification setting: ${result.error?.message}`); + return result; // Return error if any + } + } + + this.logger.info(`All notifications processed successfully`); + return new CustomResponse("notification sent", 200); + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + const errorStack = error.stack || ''; + this.logger.error(`Error in sendNotificationV2: ${errorMessage}\nStack: ${errorStack}`); + + if (error instanceof CustomError) { + this.logger.error(`CustomError with status code: ${error.statusCode}`); + return new CustomResponse("", 0, error); + } else { + const customError = new CustomError(errorMessage, 400); + this.logger.error(`Converted to CustomError with status code: 400`); + return new CustomResponse("", 0, customError); + } + } + } + /** * Handle scoop notification events (webhook or slack) * @param event The scoop notification event diff --git a/src/routes/notificationRoutes.ts b/src/routes/notificationRoutes.ts index bf3567b..bad9285 100644 --- a/src/routes/notificationRoutes.ts +++ b/src/routes/notificationRoutes.ts @@ -34,6 +34,27 @@ export const createNotificationRouter = (notificationService: NotificationServic } }); + router.post('/notify/v2', async(req, res) => { + logger.info("notifications V2 Received"); + const { event, notificationSettings } = req.body; + + if (!event || !notificationSettings) { + logger.error("Missing required fields: event or notificationSettings"); + res.status(400).json({message: "Missing required fields: event or notificationSettings"}).send(); + failedNotificationMetricsCounter.inc(); + return; + } + + const response = await notificationService.sendNotificationV2(event, notificationSettings); + if (response.status != 0) { + res.status(response.status).json({message: response.message}).send(); + successNotificationMetricsCounter.inc(); + } else { + res.status(response.error.statusCode).json({message: response.error.message}).send(); + failedNotificationMetricsCounter.inc(); + } + }); + return router; }; From 4cf4544593ea8cb06c1e1948d2c5cf941ff7c7b7 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Wed, 16 Apr 2025 16:49:40 +0530 Subject: [PATCH 4/8] logs added --- src/routes/notificationRoutes.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/notificationRoutes.ts b/src/routes/notificationRoutes.ts index bad9285..2863092 100644 --- a/src/routes/notificationRoutes.ts +++ b/src/routes/notificationRoutes.ts @@ -38,6 +38,10 @@ export const createNotificationRouter = (notificationService: NotificationServic logger.info("notifications V2 Received"); const { event, notificationSettings } = req.body; + // log the event and notificationSettings + logger.info("event: ", event); + logger.info("notificationSettings: ", notificationSettings); + if (!event || !notificationSettings) { logger.error("Missing required fields: event or notificationSettings"); res.status(400).json({message: "Missing required fields: event or notificationSettings"}).send(); From d5f5f228cd1e3bcb604efd5d78218d9993170f1d Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Tue, 22 Apr 2025 14:18:04 +0530 Subject: [PATCH 5/8] removed example removed extra code --- examples/getCommitsExample.ts | 91 ------------------- src/routes/healthRoutes.ts | 2 +- src/templateService.ts | 40 -------- .../tests}/getCommitsFromGitTriggers.test.ts | 2 +- ...emplate.ts => getMustacheTemplate.test.ts} | 2 +- ...tificationTest.ts => notification.test.ts} | 0 ...ation.ts => sendSlackNotification.test.ts} | 4 +- 7 files changed, 5 insertions(+), 136 deletions(-) delete mode 100644 examples/getCommitsExample.ts delete mode 100644 src/templateService.ts rename {test/common => src/tests}/getCommitsFromGitTriggers.test.ts (98%) rename src/tests/{getMustacheTemplate.ts => getMustacheTemplate.test.ts} (96%) rename src/tests/{notificationTest.ts => notification.test.ts} (100%) rename src/tests/{sendSlackNotification.ts => sendSlackNotification.test.ts} (91%) diff --git a/examples/getCommitsExample.ts b/examples/getCommitsExample.ts deleted file mode 100644 index dd46024..0000000 --- a/examples/getCommitsExample.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getCommitsFromGitTriggers } from '../src/common/getCommitsFromGitTriggers'; - -// Example data from the request -const material = { - "gitTriggers": { - "101": { - "Commit": "a61f69d7889eae63bbfe95d87f5dde1e4d090cad", - "Author": "Badal Kumar Prusty ", - "Date": "2025-04-08T07:28:12Z", - "Message": "added react project\n", - "Changes": null, - "WebhookData": { - "id": 0, - "eventActionType": "", - "data": null - }, - "CiConfigureSourceValue": "main", - "GitRepoUrl": "https://github.com/badal773/test.git", - "GitRepoName": "test", - "CiConfigureSourceType": "SOURCE_TYPE_BRANCH_FIXED" - }, - "76": { - "Commit": "a61f69d7889eae63bbfe95d87f5dde1e4d090cad", - "Author": "Badal Kumar Prusty ", - "Date": "2025-04-08T07:28:12Z", - "Message": "added react project\n", - "Changes": null, - "WebhookData": { - "id": 0, - "eventActionType": "", - "data": null - }, - "CiConfigureSourceValue": "main", - "GitRepoUrl": "https://github.com/badal773/test.git", - "GitRepoName": "test", - "CiConfigureSourceType": "SOURCE_TYPE_BRANCH_FIXED" - } - }, - "ciMaterials": [ - { - "id": 76, - "gitMaterialId": 62, - "gitMaterialUrl": "", - "gitMaterialName": "test", - "type": "SOURCE_TYPE_BRANCH_FIXED", - "value": "main", - "active": true, - "lastFetchTime": "0001-01-01T00:00:00Z", - "isRepoError": false, - "repoErrorMsg": "", - "isBranchError": false, - "branchErrorMsg": "", - "url": "https://github.com/badal773/test.git" - }, - { - "id": 101, - "gitMaterialId": 80, - "gitMaterialUrl": "", - "gitMaterialName": "test", - "type": "SOURCE_TYPE_BRANCH_FIXED", - "value": "main", - "active": true, - "lastFetchTime": "0001-01-01T00:00:00Z", - "isRepoError": false, - "repoErrorMsg": "", - "isBranchError": false, - "branchErrorMsg": "", - "url": "https://github.com/badal773/test.git" - } - ] -}; - -// Get the list of commits -const commits = getCommitsFromGitTriggers(material); -console.log('Commits:', commits); diff --git a/src/routes/healthRoutes.ts b/src/routes/healthRoutes.ts index 9ca4489..44b0432 100644 --- a/src/routes/healthRoutes.ts +++ b/src/routes/healthRoutes.ts @@ -15,7 +15,7 @@ */ import { Router } from 'express'; -import { send } from '../tests/sendSlackNotification'; +import { send } from '../tests/sendSlackNotification.test'; const router = Router(); diff --git a/src/templateService.ts b/src/templateService.ts deleted file mode 100644 index c054130..0000000 --- a/src/templateService.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// import * as mustache from 'mustache'; -// import { readFileSync, writeFileSync } from 'fs'; -// import { EventType, Event } from './notificationService'; - -// class TemplateService { -// slackTemplateMap = new Map(); -// constructor() { -// const template = readFileSync('./template/slack.ci_success.template.mustache', 'utf-8'); -// this.slackTemplateMap.set(EventType.CI_SUCCESS, template) -// } - -// getNotificationPayload(event: Event) { -// if (this.slackTemplateMap.has(event.type)) { -// let template = this.slackTemplateMap.get(event.type) - -// } else { -// //err not supported -// } - -// } - -// //const result = Mustache.render(template, hash); - -// } \ No newline at end of file diff --git a/test/common/getCommitsFromGitTriggers.test.ts b/src/tests/getCommitsFromGitTriggers.test.ts similarity index 98% rename from test/common/getCommitsFromGitTriggers.test.ts rename to src/tests/getCommitsFromGitTriggers.test.ts index cdc6cd7..42fcb56 100644 --- a/test/common/getCommitsFromGitTriggers.test.ts +++ b/src/tests/getCommitsFromGitTriggers.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getCommitsFromGitTriggers } from '../../src/common/getCommitsFromGitTriggers'; +import { getCommitsFromGitTriggers } from '../common/getCommitsFromGitTriggers'; import { expect } from 'chai'; describe('getCommitsFromGitTriggers', () => { diff --git a/src/tests/getMustacheTemplate.ts b/src/tests/getMustacheTemplate.test.ts similarity index 96% rename from src/tests/getMustacheTemplate.ts rename to src/tests/getMustacheTemplate.test.ts index 094e0f0..6b72936 100644 --- a/src/tests/getMustacheTemplate.ts +++ b/src/tests/getMustacheTemplate.test.ts @@ -17,7 +17,7 @@ import fs from 'fs'; import { Event } from '../notification/service/notificationService'; -export function getMustacheTemplate(event: Event) { +export function getMustacheTemplateTest(event: Event) { if (event.pipelineType === "CI") { switch (event.eventTypeId) { case 1: return fs.readFileSync("src/tests/mustacheTemplate/CITrigger.mustache").toString(); diff --git a/src/tests/notificationTest.ts b/src/tests/notification.test.ts similarity index 100% rename from src/tests/notificationTest.ts rename to src/tests/notification.test.ts diff --git a/src/tests/sendSlackNotification.ts b/src/tests/sendSlackNotification.test.ts similarity index 91% rename from src/tests/sendSlackNotification.ts rename to src/tests/sendSlackNotification.test.ts index 2b069bf..cf56d5d 100644 --- a/src/tests/sendSlackNotification.ts +++ b/src/tests/sendSlackNotification.test.ts @@ -18,7 +18,7 @@ const fetch = require('node-fetch'); import Mustache from 'mustache'; import event from './data/cd.json'; import { MustacheHelper } from '../common/mustacheHelper'; -import { getMustacheTemplate } from './getMustacheTemplate'; +import { getMustacheTemplateTest } from './getMustacheTemplate.test'; import { Event } from '../notification/service/notificationService'; // Used for sending notification on slack. triggers on /test export function send() { @@ -26,7 +26,7 @@ export function send() { let mh = new MustacheHelper(); let parsedEvent = mh.parseEvent(event); - let mustacheHash = getMustacheTemplate(event as Event); + let mustacheHash = getMustacheTemplateTest(event as Event); let json = Mustache.render(JSON.stringify(mustacheHash), parsedEvent); json = JSON.parse(json); fetch(webhookURL, { body: json, method: "POST" }).then((r) => { From feba36adbdb2cd39143644948ddbff54da6b1c01 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Wed, 23 Apr 2025 15:20:38 +0530 Subject: [PATCH 6/8] v2 support in nats client --- src/config/nats.ts | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/config/nats.ts b/src/config/nats.ts index f2f8f55..89076fd 100644 --- a/src/config/nats.ts +++ b/src/config/nats.ts @@ -24,9 +24,33 @@ import { successNotificationMetricsCounter, failedNotificationMetricsCounter } f export const natsEventHandler = (notificationService: NotificationService) => async (msg: string) => { const eventAsString = JSON.parse(msg); - const event = JSON.parse(eventAsString) as Event; - logger.info({ natsEventBody: event }); - const response = await notificationService.sendNotification(event); + const parsedData = JSON.parse(eventAsString); + let response; + + try { + // First try to parse as V2 format (which includes both event and notificationSettings) + logger.info('Attempting to parse as V2 payload'); + if (parsedData.event && parsedData.notificationSettings) { + // This is a V2 payload + const { event, notificationSettings } = parsedData; + logger.info({ natsEventBodyV2: { event, notificationSettingsCount: notificationSettings.length } }); + response = await notificationService.sendNotificationV2(event, notificationSettings); + } else { + // Fall back to V1 format (which only includes the event) + logger.info('Falling back to V1 payload format'); + const event = parsedData as Event; + logger.info({ natsEventBodyV1: event }); + response = await notificationService.sendNotification(event); + } + } catch (error: any) { + { + logger.error(`Failed to process message in both V1 and V2 formats: ${error.message || 'Unknown error'}`); + failedNotificationMetricsCounter.inc(); + throw error; + } + } + + // Handle response metrics if (response.status != 0) { successNotificationMetricsCounter.inc(); } else { @@ -36,12 +60,12 @@ export const natsEventHandler = (notificationService: NotificationService) => as export const connectToNats = async (notificationService: NotificationService) => { const natsUrl = process.env.NATS_URL; - + if (!natsUrl) { logger.info("NATS_URL not provided, skipping NATS connection"); return; } - + try { logger.info("Connecting to NATS server..."); const conn: NatsConnection = await connect({ servers: natsUrl }); From 52ee334663b98dfe8201edf74d38c4fe36f891ef Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Mon, 28 Apr 2025 15:02:47 +0530 Subject: [PATCH 7/8] send notif with always nats --- src/app.ts | 5 +-- src/routes/index.ts | 5 +-- src/routes/notificationRoutes.ts | 65 -------------------------------- src/server.ts | 2 +- 4 files changed, 3 insertions(+), 74 deletions(-) delete mode 100644 src/routes/notificationRoutes.ts diff --git a/src/app.ts b/src/app.ts index d82661c..7fa104a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,11 +16,9 @@ import express from 'express'; import bodyParser from 'body-parser'; -import { createRouter } from './routes'; import { metricsMiddleware } from './middleware/metrics'; -import { NotificationService } from './notification/service/notificationService'; -export const createApp = (notificationService: NotificationService) => { +export const createApp = () => { const app = express(); // Middleware @@ -29,7 +27,6 @@ export const createApp = (notificationService: NotificationService) => { app.use(metricsMiddleware); // Routes - app.use(createRouter(notificationService)); return app; }; diff --git a/src/routes/index.ts b/src/routes/index.ts index 5acdc09..3b9972a 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -16,16 +16,13 @@ import { Router } from 'express'; import healthRoutes from './healthRoutes'; -import createNotificationRouter from './notificationRoutes'; import metricsRoutes from './metricsRoutes'; -import { NotificationService } from '../notification/service/notificationService'; -export const createRouter = (notificationService: NotificationService) => { +export const createRouter = () => { const router = Router(); // Mount routes router.use('/', healthRoutes); - router.use('/', createNotificationRouter(notificationService)); router.use('/', metricsRoutes); return router; diff --git a/src/routes/notificationRoutes.ts b/src/routes/notificationRoutes.ts deleted file mode 100644 index 2863092..0000000 --- a/src/routes/notificationRoutes.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Router } from 'express'; -import { NotificationService } from '../notification/service/notificationService'; -import { logger } from '../config/logger'; -import { successNotificationMetricsCounter, failedNotificationMetricsCounter } from '../common/metrics'; - -export const createNotificationRouter = (notificationService: NotificationService) => { - const router = Router(); - - router.post('/notify', async(req, res) => { - logger.info("notifications Received"); - const response = await notificationService.sendNotification(req.body); - if (response.status != 0) { - res.status(response.status).json({message: response.message}).send(); - successNotificationMetricsCounter.inc(); - } else { - res.status(response.error.statusCode).json({message: response.error.message}).send(); - failedNotificationMetricsCounter.inc(); - } - }); - - router.post('/notify/v2', async(req, res) => { - logger.info("notifications V2 Received"); - const { event, notificationSettings } = req.body; - - // log the event and notificationSettings - logger.info("event: ", event); - logger.info("notificationSettings: ", notificationSettings); - - if (!event || !notificationSettings) { - logger.error("Missing required fields: event or notificationSettings"); - res.status(400).json({message: "Missing required fields: event or notificationSettings"}).send(); - failedNotificationMetricsCounter.inc(); - return; - } - - const response = await notificationService.sendNotificationV2(event, notificationSettings); - if (response.status != 0) { - res.status(response.status).json({message: response.message}).send(); - successNotificationMetricsCounter.inc(); - } else { - res.status(response.error.statusCode).json({message: response.error.message}).send(); - failedNotificationMetricsCounter.inc(); - } - }); - - return router; -}; - -export default createNotificationRouter; diff --git a/src/server.ts b/src/server.ts index a4ace74..237266e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -35,7 +35,7 @@ const startServer = async () => { await connectToNats(notificationService); // Create and start the Express app - const app = createApp(notificationService); + const app = createApp(); app.listen(PORT, () => { logger.info(`Notifier app listening on port ${PORT}!`); From caccc8a348d7fb898f4a3e3ba4e5ea1971de9eec Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Mon, 28 Apr 2025 18:09:39 +0530 Subject: [PATCH 8/8] Revert "send notif with always nats" This reverts commit 52ee334663b98dfe8201edf74d38c4fe36f891ef. --- src/app.ts | 5 ++- src/routes/index.ts | 5 ++- src/routes/notificationRoutes.ts | 65 ++++++++++++++++++++++++++++++++ src/server.ts | 2 +- 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/routes/notificationRoutes.ts diff --git a/src/app.ts b/src/app.ts index 7fa104a..d82661c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,9 +16,11 @@ import express from 'express'; import bodyParser from 'body-parser'; +import { createRouter } from './routes'; import { metricsMiddleware } from './middleware/metrics'; +import { NotificationService } from './notification/service/notificationService'; -export const createApp = () => { +export const createApp = (notificationService: NotificationService) => { const app = express(); // Middleware @@ -27,6 +29,7 @@ export const createApp = () => { app.use(metricsMiddleware); // Routes + app.use(createRouter(notificationService)); return app; }; diff --git a/src/routes/index.ts b/src/routes/index.ts index 3b9972a..5acdc09 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -16,13 +16,16 @@ import { Router } from 'express'; import healthRoutes from './healthRoutes'; +import createNotificationRouter from './notificationRoutes'; import metricsRoutes from './metricsRoutes'; +import { NotificationService } from '../notification/service/notificationService'; -export const createRouter = () => { +export const createRouter = (notificationService: NotificationService) => { const router = Router(); // Mount routes router.use('/', healthRoutes); + router.use('/', createNotificationRouter(notificationService)); router.use('/', metricsRoutes); return router; diff --git a/src/routes/notificationRoutes.ts b/src/routes/notificationRoutes.ts new file mode 100644 index 0000000..2863092 --- /dev/null +++ b/src/routes/notificationRoutes.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Router } from 'express'; +import { NotificationService } from '../notification/service/notificationService'; +import { logger } from '../config/logger'; +import { successNotificationMetricsCounter, failedNotificationMetricsCounter } from '../common/metrics'; + +export const createNotificationRouter = (notificationService: NotificationService) => { + const router = Router(); + + router.post('/notify', async(req, res) => { + logger.info("notifications Received"); + const response = await notificationService.sendNotification(req.body); + if (response.status != 0) { + res.status(response.status).json({message: response.message}).send(); + successNotificationMetricsCounter.inc(); + } else { + res.status(response.error.statusCode).json({message: response.error.message}).send(); + failedNotificationMetricsCounter.inc(); + } + }); + + router.post('/notify/v2', async(req, res) => { + logger.info("notifications V2 Received"); + const { event, notificationSettings } = req.body; + + // log the event and notificationSettings + logger.info("event: ", event); + logger.info("notificationSettings: ", notificationSettings); + + if (!event || !notificationSettings) { + logger.error("Missing required fields: event or notificationSettings"); + res.status(400).json({message: "Missing required fields: event or notificationSettings"}).send(); + failedNotificationMetricsCounter.inc(); + return; + } + + const response = await notificationService.sendNotificationV2(event, notificationSettings); + if (response.status != 0) { + res.status(response.status).json({message: response.message}).send(); + successNotificationMetricsCounter.inc(); + } else { + res.status(response.error.statusCode).json({message: response.error.message}).send(); + failedNotificationMetricsCounter.inc(); + } + }); + + return router; +}; + +export default createNotificationRouter; diff --git a/src/server.ts b/src/server.ts index 237266e..a4ace74 100644 --- a/src/server.ts +++ b/src/server.ts @@ -35,7 +35,7 @@ const startServer = async () => { await connectToNats(notificationService); // Create and start the Express app - const app = createApp(); + const app = createApp(notificationService); app.listen(PORT, () => { logger.info(`Notifier app listening on port ${PORT}!`);