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>
-  <!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
-  [![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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