From e1e9fa9dc61ae538682d2f50839fea5301911d53 Mon Sep 17 00:00:00 2001 From: FORESTIER Fabien <fabien.forestier@soprasteria.com> Date: Fri, 14 Jun 2019 15:15:36 +0200 Subject: [PATCH] Update log system, simplify var env export for local deploy, remove heavy email bodies from the logs --- README.md | 78 +++++++++++------------------ docker-compose.yml | 10 ++-- package.json | 4 +- src/app-logger.ts | 19 +++++++ src/app.module.ts | 2 + src/configuration/config.service.ts | 21 +++----- src/configuration/config.ts | 8 +-- src/configuration/template.env | 8 --- src/email/email.controller.ts | 15 +++--- src/email/email.service.ts | 27 +++++----- src/guards/groups.guards.ts | 17 +++++-- src/main.ts | 7 ++- template.env | 19 +++++++ 13 files changed, 129 insertions(+), 106 deletions(-) create mode 100644 src/app-logger.ts delete mode 100644 src/configuration/template.env create mode 100644 template.env diff --git a/README.md b/README.md index b73cc61..83a4338 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,23 @@ -<p align="center"> - <a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a> -</p> - -[travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master -[travis-url]: https://travis-ci.org/nestjs/nest -[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux -[linux-url]: https://travis-ci.org/nestjs/nest - - <p align="center">A progressive <a href="http://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.io" target="blank">Angular</a>.</p> - <p align="center"> -<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a> -<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a> -<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/dm/@nestjs/core.svg" alt="NPM Downloads" /></a> -<a href="https://travis-ci.org/nestjs/nest"><img src="https://api.travis-ci.org/nestjs/nest.svg?branch=master" alt="Travis" /></a> -<a href="https://travis-ci.org/nestjs/nest"><img src="https://img.shields.io/travis/nestjs/nest/master.svg?label=linux" alt="Linux" /></a> -<a href="https://coveralls.io/github/nestjs/nest?branch=master"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#5" alt="Coverage" /></a> -<a href="https://gitter.im/nestjs/nestjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge"><img src="https://badges.gitter.im/nestjs/nestjs.svg" alt="Gitter" /></a> -<a href="https://opencollective.com/nest#backer"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a> -<a href="https://opencollective.com/nest#sponsor"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a> - <a href="https://paypal.me/kamilmysliwiec"><img src="https://img.shields.io/badge/Donate-PayPal-dc3d53.svg"/></a> - <a href="https://twitter.com/nestframework"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a> -</p> - <!--[](https://opencollective.com/nest#backer) - [](https://opencollective.com/nest#sponsor)--> - -## Description - -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. - ## Installation ```bash $ npm install ``` -## Running the app +## Environment variables + +In order to run the code, some environment variables are needed. They are specified in the `template.env` file at the root of the project. + +For a local deployment: + +1. `cp template.env .env` +2. Edit .env according to the chosen configuration + +The values will be read from the file by default, but you can override any of those by exporting manually the variable before launching the service. + +## Running the app without docker + +You will need to provide a healthy connection to a database in order for the service to start. ```bash # development @@ -43,15 +26,24 @@ $ npm run start # watch mode $ npm run start:dev -# incremental rebuild (webpack) -$ npm run webpack -$ npm run start:hmr - # production mode $ npm run start:prod ``` -## Test +## Running the app with docker + +```bash +# build +$ docker-compose build + +# deploy +$ docker-compose up [-d] + +# build and deploy +$ docker-compose up --build [-d] +``` + +<!-- ## Test ```bash # unit tests @@ -62,18 +54,6 @@ $ npm run test:e2e # test coverage $ npm run test:cov -``` - -## Support - -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). - -## Stay in touch - -- Author - [Kamil MyĆliwiec](https://kamilmysliwiec.com) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) +``` --> -## License - Nest is [MIT licensed](LICENSE). diff --git a/docker-compose.yml b/docker-compose.yml index eae8767..8dba4d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,14 +8,18 @@ services: ports: - ${MAIL_SERVICE_BIND_PORT}:3000 environment: - - USER_SUPPORT_MAILBOX=${USER_SUPPORT_MAILBOX} - RABBITMQ_USER=rabbitmq-user - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - - GROUP_HEADER=x-consumer-groups - - EMAIL_WRITER_GROUP_NAME=email-writer + - RABBITMQ_HOST=rabbitmq + - RABBITMQ_LISTENING_PORT=5672 + - MAILER_QUEUE=portail-data-send-email - MAIL_SUBJECT_PREFIX=${MAIL_SUBJECT_PREFIX} + - USER_SUPPORT_MAILBOX=${USER_SUPPORT_MAILBOX} - NO_REPLY_MAIL_ADDRESS=${NO_REPLY_MAIL_ADDRESS} - ADDITIONAL_FEEDBACK_EMAILS=${ADDITIONAL_FEEDBACK_EMAILS} + - GROUP_HEADER=x-consumer-groups + - EMAIL_WRITER_GROUP_NAME=email-writer + - IMAGE_HOST=https://minio.alpha.grandlyon.com/email-template-assets restart: unless-stopped rabbitmq: diff --git a/package.json b/package.json index dcf5eb4..6d970e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "service-email", - "version": "1.2.0", + "version": "1.2.1", "description": "description", "author": "", "license": "MIT", @@ -72,4 +72,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/src/app-logger.ts b/src/app-logger.ts new file mode 100644 index 0000000..5c9db3a --- /dev/null +++ b/src/app-logger.ts @@ -0,0 +1,19 @@ +import { Logger } from '@nestjs/common'; + +export class AppLogger extends Logger { + log(message: string, context?: string) { + // add your tailored logic here + super.log(`[log] ${message}`, context); + } + + warn(message: string, context?: string) { + // add your tailored logic here + super.warn(`[warn] ${message}`, context); + } + + error(message: string, trace?: string, context?: string) { + // add your tailored logic here + super.error(`[error] ${message}`, trace, context); + } + +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 93874d9..7a1593d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { ConfigModule } from './configuration/config.module'; import { APP_GUARD } from '@nestjs/core'; import { GroupsGuard } from './guards/groups.guards'; import { HealthModule } from './health/health.module'; +import { AppLogger } from './app-logger'; @Module({ imports: [ @@ -13,6 +14,7 @@ import { HealthModule } from './health/health.module'; ], controllers: [], providers: [ + AppLogger, { provide: APP_GUARD, useClass: GroupsGuard, diff --git a/src/configuration/config.service.ts b/src/configuration/config.service.ts index 193cb34..67c9ba1 100644 --- a/src/configuration/config.service.ts +++ b/src/configuration/config.service.ts @@ -1,31 +1,24 @@ -import * as dotenv from 'dotenv'; -import * as fs from 'fs'; import { config } from './config'; export class ConfigService { private _config = config; constructor() { - // Only for development purpose, set environment variable based on the env file provided - if (process.env.NODE_ENV === 'LOCAL') { - const envConfig = dotenv.parse(fs.readFileSync(`./src/configuration/${process.env.NODE_ENV}.env`)); - for (const k in envConfig) { - if (envConfig.hasOwnProperty(k)) { - process.env[k] = envConfig[k]; - } - } - } // Initializing conf with values from var env this._config.rabbitMQ.user = process.env.RABBITMQ_USER; this._config.rabbitMQ.password = process.env.RABBITMQ_PASSWORD; - this._config.userSupportMailbox = process.env.USER_SUPPORT_MAILBOX; - this._config.groupNames.emailWriter = process.env.EMAIL_WRITER_GROUP_NAME; - this._config.groupHeader = process.env.GROUP_HEADER; + this._config.rabbitMQ.host = process.env.RABBITMQ_HOST; + this._config.rabbitMQ.port = process.env.RABBITMQ_LISTENING_PORT; + this._config.mailerQueue = process.env.MAILER_QUEUE; this._config.mailSubjectPrefix = process.env.MAIL_SUBJECT_PREFIX; + this._config.userSupportMailbox = process.env.USER_SUPPORT_MAILBOX; this._config.noReplyMailAddress = process.env.NO_REPLY_MAIL_ADDRESS; this._config.additionalFeedbackEmails = process.env.ADDITIONAL_FEEDBACK_EMAILS; + this._config.groupHeader = process.env.GROUP_HEADER; + this._config.groupNames.emailWriter = process.env.EMAIL_WRITER_GROUP_NAME; + this._config.imageHost = process.env.IMAGE_HOST; } get config() { diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 7f1ddb7..07512a4 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -2,12 +2,12 @@ export const config = { rabbitMQ: { user: null, password: null, - host: 'rabbitmq', - port: '5672', + host: null, + port: null, }, - mailerQueue: 'portail-data-send-email', + mailerQueue: '', userSupportMailbox: '', - imageHost: 'https://highway-to-data.alpha.grandlyon.com/email-template-assets', + imageHost: '', groupNames: { emailWriter: '', }, diff --git a/src/configuration/template.env b/src/configuration/template.env deleted file mode 100644 index ae1b2eb..0000000 --- a/src/configuration/template.env +++ /dev/null @@ -1,8 +0,0 @@ -RABBITMQ_USER= -RABBITMQ_PASSWORD= -USER_SUPPORT_MAILBOX= -MAIL_SUBJECT_PREFIX= -NO_REPLY_MAIL_ADDRESS= -ADDITIONAL_FEEDBACK_EMAILS= -GROUP_HEADER= -EMAIL_WRITER_GROUP_NAME= \ No newline at end of file diff --git a/src/email/email.controller.ts b/src/email/email.controller.ts index b1b33e7..dec2cd7 100644 --- a/src/email/email.controller.ts +++ b/src/email/email.controller.ts @@ -1,7 +1,7 @@ -import { Controller, Post, Body, InternalServerErrorException, Logger, HttpCode, Req } from '@nestjs/common'; +import { Controller, Post, Body, InternalServerErrorException, HttpCode, Req } from '@nestjs/common'; import { ContactForm, EmailWithoutFrom, FeedbackForm } from './email'; import { EmailService } from './email.service'; -import { ApiBadRequestResponse, ApiOkResponse, ApiUseTags, ApiOperation, ApiInternalServerErrorResponse } from '@nestjs/swagger'; +import { ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiInternalServerErrorResponse } from '@nestjs/swagger'; import { Groups } from '../decorators/groups.decorators'; @Controller() @@ -9,20 +9,19 @@ export class EmailController { constructor( private emailService: EmailService, - ) {} + ) { } @Post('contact') // tslint:disable-next-line:max-line-length @ApiOperation({ title: 'Send email to admin (emails defined as var env of the project, see docker-compose.yml file) and recap email to user email.' }) @ApiOkResponse({ description: 'OK' }) @ApiBadRequestResponse({ description: 'Missing fields' }) - @ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)'}) + @ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)' }) @HttpCode(200) async create(@Body() contactForm: ContactForm) { try { return await this.emailService.sendContactEmails(contactForm); } catch (error) { - Logger.log(error); throw new InternalServerErrorException(); } } @@ -32,13 +31,12 @@ export class EmailController { @ApiOperation({ title: 'Send email to admin with the user feedback' }) @ApiOkResponse({ description: 'OK' }) @ApiBadRequestResponse({ description: 'Missing fields' }) - @ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)'}) + @ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)' }) @HttpCode(200) async sendFeedback(@Body() feedbackForm: FeedbackForm, @Req() req) { try { return await this.emailService.sendFeedback(feedbackForm, req.headers['user-agent']); } catch (error) { - Logger.log(error); throw new InternalServerErrorException(); } } @@ -48,13 +46,12 @@ export class EmailController { @ApiOperation({ title: 'Send email.' }) @ApiOkResponse({ description: 'OK' }) @ApiBadRequestResponse({ description: 'Missing fields' }) - @ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)'}) + @ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)' }) @HttpCode(200) async createEmail(@Body() email: EmailWithoutFrom) { try { return await this.emailService.send(email); } catch (error) { - Logger.log(error); throw new InternalServerErrorException(); } } diff --git a/src/email/email.service.ts b/src/email/email.service.ts index e78709f..d295e3f 100644 --- a/src/email/email.service.ts +++ b/src/email/email.service.ts @@ -10,14 +10,17 @@ moment.tz.setDefault('Europe/Paris'); @Injectable() export class EmailService { + + private logger: Logger; config: any = {}; constructor(private configService: ConfigService) { + this.logger = new Logger(EmailService.name); this.config = this.configService.config; } async sendContactEmails(contactForm: ContactForm) { - Logger.log('[-] sendContactEmails method'); + this.logger.log('Entering function', `${EmailService.name} - ${this.sendContactEmails.name}`); const adminEmailBody = buildContactAdminEmail({ subject: contactForm.subject, @@ -55,7 +58,7 @@ export class EmailService { } async sendFeedback(feedbackForm: FeedbackForm, userAgent) { - Logger.log('[-] sendFeedback method'); + this.logger.log('Entering function', `${EmailService.name} - ${this.sendFeedback.name}`); const userAgentParsed = useragent.parse(userAgent); let userAgentString = ''; @@ -90,6 +93,8 @@ export class EmailService { } async send(emailInfo: EmailWithoutFrom) { + this.logger.log('Entering function', `${EmailService.name} - ${this.send.name}`); + let conn, ch; // tslint:disable-next-line:max-line-length const rabbitmqUrl = `amqp://${this.config.rabbitMQ.user}:${this.config.rabbitMQ.password}@${this.config.rabbitMQ.host}:${this.config.rabbitMQ.port}`; @@ -103,14 +108,11 @@ export class EmailService { email.subject = `[${this.config.mailSubjectPrefix}] ${email.subject}`; } - Logger.log('[-] send method'); - Logger.log(email); - // Connect to rabbitmq try { conn = await amqp.connect(rabbitmqUrl); } catch (error) { - Logger.error(' [x] Error connecting to RabbitMQ: ', JSON.stringify(error)); + Logger.error('Error connecting to RabbitMQ', error, `${EmailService.name} - ${this.send.name}`); throw new InternalServerErrorException('Could not connect to rabbitMQ.'); } @@ -118,7 +120,7 @@ export class EmailService { // Create a communication channel ch = await conn.createChannel(); } catch (error) { - Logger.error(' [x] Error creating channel: ', JSON.stringify(error)); + Logger.error('Error creating channel', error, `${EmailService.name} - ${this.send.name}`); throw new InternalServerErrorException('Could not create channel.'); } @@ -128,21 +130,22 @@ export class EmailService { try { await ch.assertQueue(mailerQueue, { durable: true }); } catch (error) { - Logger.error(' [x] Error creating channel: ', JSON.stringify(error)); - throw new InternalServerErrorException('Could not assert channel.'); + Logger.error('Error asserting queue', error, `${EmailService.name} - ${this.send.name}`); + throw new InternalServerErrorException('Could not assert queue.'); } try { await ch.sendToQueue(mailerQueue, buffer, { persistent: true }); } catch (error) { - Logger.error(' [x] Error sending to queue: ', JSON.stringify(error)); + Logger.error('Error sending to queue', error, `${EmailService.name} - ${this.send.name}`); throw new InternalServerErrorException('Could not send to queue.'); } - Logger.log(`Sent to queue ${mailerQueue}: ${JSON.stringify(email)}`); + Logger.log( + `Sent to queue ${mailerQueue},{ from: ${email.from}, to: ${email.to}, subject: ${email.subject}}`, `${EmailService.name} - ${this.send.name}`, + ); setTimeout(() => { conn.close(); }, 500); - return; } diff --git a/src/guards/groups.guards.ts b/src/guards/groups.guards.ts index a56a946..a5ada74 100644 --- a/src/guards/groups.guards.ts +++ b/src/guards/groups.guards.ts @@ -4,10 +4,19 @@ import { ConfigService } from '../configuration/config.service'; @Injectable() export class GroupsGuard implements CanActivate { - constructor(private readonly reflector: Reflector, private configService: ConfigService) {} + + private logger: Logger; + + constructor( + private readonly reflector: Reflector, + private configService: ConfigService, + ) { + this.logger = new Logger(GroupsGuard.name); + } canActivate(context: ExecutionContext): boolean { - Logger.log('[+] GroupMiddleware'); + + this.logger.log('Entering function', `${GroupsGuard.name} - ${this.canActivate.name}`); // We get the groups specified on the endpoint with the @Groups annotation // We need then to get the real group names associated to those keys from the config file let groups = this.reflector.get<string[]>('groups', context.getHandler()); @@ -31,13 +40,13 @@ export class GroupsGuard implements CanActivate { const groupHeader = request.headers[this.configService.config.groupHeader]; if (!groupHeader) { - Logger.log(' [-] no group header'); + this.logger.log('No group header'); return false; } const consumerGroups = groupHeader.split(',').map(e => e.trim()); - Logger.log(` [-] consumer groups: ${consumerGroups}`); + this.logger.log(`Consumer groups: ${consumerGroups}`); const hasGroup = () => consumerGroups.some((group) => groups.includes(group)); diff --git a/src/main.ts b/src/main.ts index c6fe329..5b81436 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,10 +5,13 @@ import { ValidationPipe } from '@nestjs/common'; import * as swStats from 'swagger-stats'; import * as favicon from 'serve-favicon'; import * as path from 'path'; +import { AppLogger } from './app-logger'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + logger: false, + }); app.enableCors({ credentials: true, origin: true }); @@ -30,6 +33,8 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); + app.useLogger(app.get(AppLogger)); + await app.listen(3000); } bootstrap(); diff --git a/template.env b/template.env new file mode 100644 index 0000000..eb388f2 --- /dev/null +++ b/template.env @@ -0,0 +1,19 @@ +TAG=<version of the service to deploy> + +RABBITMQ_USER=<rabbitMQ user> +RABBITMQ_PASSWORD=<rabbitMQ password> +RABBITMQ_HOST=<rabbitMQ host> +RABBITMQ_LISTENING_PORT=<rabiitMQ listening port, default 5672> +RABBITMQ_GUI_PORT=<RabbitMQ interface, default 15672> +MAILER_QUEUE=<sueue where to write the emails to be sent> + +MAIL_SERVICE_BIND_PORT=<listening port of the service> +MAIL_SUBJECT_PREFIX=<prefix used in email subject> +USER_SUPPORT_MAILBOX=<email address of the support> +NO_REPLY_MAIL_ADDRESS=<no-reply email address> +ADDITIONAL_FEEDBACK_EMAILS=<any other email address that should receive the feedback> + +GROUP_HEADER=<header name that contains the user group set by the api gateway> +EMAIL_WRITER_GROUP_NAME=<name of the group allowed to send email> + +IMAGE_HOST=<host of the images present in the emails body> \ No newline at end of file -- GitLab