Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add debug logs in api #294

Merged
12 commits merged into from
Jul 30, 2024
3 changes: 3 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ NODE_ENV= # Use "development" for graphql playground to work
# Notification configuration
MAX_RETRY_COUNT=3 # Max retry count, default is 3

# Log Level
LOG_LEVEL=info # Log level, default is info

# Database configuration
DB_TYPE=
DB_HOST=
Expand Down
12 changes: 10 additions & 2 deletions apps/api/src/common/decorators/is-data-valid.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ChannelType } from 'src/common/constants/notifications';
import { SMTPDataDto } from 'src/modules/notifications/dtos/providers/smtp-data.dto';
import { MailgunDataDto } from 'src/modules/notifications/dtos/providers/mailgun-data.dto';
import { Wa360DialogDataDto } from 'src/modules/notifications/dtos/providers/wa360Dialog-data.dto';
import { BadRequestException } from '@nestjs/common';
import { BadRequestException, Logger } from '@nestjs/common';
import { CreateNotificationDto } from 'src/modules/notifications/dtos/create-notification.dto';
import { WaTwilioDataDto } from 'src/modules/notifications/dtos/providers/waTwilio-data.dto';
import { SmsTwilioDataDto } from 'src/modules/notifications/dtos/providers/smsTwilio-data.dto';
Expand All @@ -26,20 +26,28 @@ import { VcTwilioDataDto } from 'src/modules/notifications/dtos/providers/vcTwil
@ValidatorConstraint({ name: 'isDataValidConstraint', async: true })
@Injectable()
export class IsDataValidConstraint implements ValidatorConstraintInterface {
constructor(private readonly providersService: ProvidersService) {}
constructor(
private readonly providersService: ProvidersService,
private logger: Logger,
) {}

async validate(value: object, args: ValidationArguments): Promise<boolean> {
this.logger.debug('Request data validation started');
const object = args.object as { providerId: number; data: object };
let channelTypeFromProviderId = null;

try {
channelTypeFromProviderId = (await this.providersService.getById(object.providerId))
.channelType;
this.logger.debug(
`Fetched channel type: ${channelTypeFromProviderId} from provider Id: ${object.providerId}`,
);
} catch (error) {
throw new Error(`Error while fetching channelType from ProviderId: ${error}`);
}

const validateAndThrowError = async (validationData: object): Promise<void> => {
this.logger.debug('Awaiting Validation of request data as per request channel type');
const errors: ValidationError[] = await validate(validationData);

if (errors.length > 0) {
Expand Down
18 changes: 18 additions & 0 deletions apps/api/src/common/guards/api-key/api-key.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@ export class ApiKeyGuard implements CanActivate {

async validateRequest(execContext: ExecutionContext): Promise<boolean> {
const request = execContext.switchToHttp().getRequest();
this.logger.debug(
`Request validation started for request body: ${JSON.stringify(request.body)}`,
);

// Get api key header incase of http request
if (request && request.headers) {
this.logger.debug(`Fetching request header and provider ID: ${request.body.providerId}`);
const serverApiKeyHeader = request.headers['x-api-key'];
this.logger.debug('Fetching provider Id');
This conversation was marked as resolved.
Show resolved Hide resolved
const requestProviderId = request.body.providerId;
const validationResult = await this.validateApiKeyHeader(
serverApiKeyHeader,
Expand All @@ -44,7 +49,11 @@ export class ApiKeyGuard implements CanActivate {
// Get api key header incase of graphql request
const ctx = GqlExecutionContext.create(execContext);
const req = ctx.getContext().req;
this.logger.debug(
`Fetching request header and provider ID for GraphQL: ${req.body.providerId}`,
);
const serverApiKeyHeader = req.headers['x-api-key'];
this.logger.debug('Fetching provider Id for GraphQL');
This conversation was marked as resolved.
Show resolved Hide resolved
This conversation was marked as resolved.
Show resolved Hide resolved
const requestProviderId = request.body.providerId;
const validationResult = await this.validateApiKeyHeader(serverApiKeyHeader, requestProviderId);

Expand All @@ -59,6 +68,7 @@ export class ApiKeyGuard implements CanActivate {
serverApiKeyHeader: string,
requestProviderId: number,
): Promise<boolean> {
this.logger.debug('validateApiKeyHeader started');
let apiKeyToken = null;

if (serverApiKeyHeader) {
Expand All @@ -69,6 +79,7 @@ export class ApiKeyGuard implements CanActivate {
}

const apiKeyEntry = await this.serverApiKeysService.findByServerApiKey(apiKeyToken);
this.logger.debug(`Fetching Server API Key from request token: ${JSON.stringify(apiKeyEntry)}`);

if (!apiKeyEntry) {
//this.logger.error('Invalid x-api-key');
Expand All @@ -77,6 +88,7 @@ export class ApiKeyGuard implements CanActivate {

// Get channel type from providerId & Set the channelType based on providerEntry
const providerEntry = await this.providersService.getById(requestProviderId);
this.logger.debug(`Fetching provider by request providerId: ${JSON.stringify(providerEntry)}`);

if (!providerEntry) {
this.logger.error('Provider does not exist');
Expand All @@ -91,13 +103,19 @@ export class ApiKeyGuard implements CanActivate {

// Set correct ApplicationId after verifying
const inputApplicationId = await this.getApplicationIdFromApiKey(apiKeyToken);
this.logger.debug(
`Set correct ApplicationId for request: ${JSON.stringify(inputApplicationId)}`,
);

if (inputApplicationId != providerEntry.applicationId) {
this.logger.error('The applicationId for Server Key and Provider do not match.');
throw new BadRequestException('The applicationId for Server Key and Provider do not match.');
}

if (apiKeyToken && apiKeyToken === apiKeyEntry.apiKey) {
this.logger.debug(
'Requested providerId is valid. The applicationId for Server Key and Provider match. Valid Request.',
);
return true;
}

Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/config/logger.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { transports, format } from 'winston';
import { join } from 'path';
import { v4 as uuidv4 } from 'uuid';
import 'winston-daily-rotate-file';
import { ConfigService } from '@nestjs/config';

const configService = new ConfigService();

const logDir = 'logs';
const logFormat = format.combine(
Expand Down Expand Up @@ -57,5 +60,6 @@ const transportsConfig = [
];

export const loggerConfig = WinstonModule.createLogger({
level: configService.get('LOG_LEVEL', 'info'),
transports: transportsConfig,
});
17 changes: 17 additions & 0 deletions apps/api/src/jobs/consumers/notifications/notification.consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,31 @@ export abstract class NotificationConsumer {

try {
this.logger.log(`Sending notification with id: ${id}`);
this.logger.debug(
`Processing notification queue for channel type: ${notification.channelType}`,
);
const result = await sendNotification();

if (SkipProviderConfirmationChannels.includes(notification.channelType)) {
this.logger.debug(
`Channel type: ${notification.channelType} is included in skip queue. Provider confirmation skipped for notification id ${notification.id}`,
);
notification.deliveryStatus = DeliveryStatus.SUCCESS;
this.webhookService.triggerWebhook(notification);
} else {
this.logger.debug(
`Notification id ${notification.id} is awaiting confirmation from provider`,
);
notification.deliveryStatus = DeliveryStatus.AWAITING_CONFIRMATION;
}

this.logger.debug(`Updating result of notification with id ${notification.id}`);
notification.result = { result };
} catch (error) {
if (notification.retryCount < this.maxRetryCount) {
this.logger.debug(
`Some error occured while sendiing Notification with ID ${notification.id}. Retry Count ${notification.retryCount}/${this.maxRetryCount}. Sending notification again`,
);
notification.deliveryStatus = DeliveryStatus.PENDING;
notification.retryCount++;
} else {
Expand Down Expand Up @@ -73,6 +86,9 @@ export abstract class NotificationConsumer {

try {
this.logger.log(`Checking delivery status from provider for notification with id: ${id}`);
this.logger.debug(
`Processing awaiting confirmation notification queue for channel type: ${notification.channelType}`,
);
const response = await getNotificationStatus();
notification.result = { result: response.result as Record<string, unknown> };
notification.deliveryStatus = response.deliveryStatus;
Expand Down Expand Up @@ -113,6 +129,7 @@ export abstract class NotificationConsumer {
);
this.logger.error(JSON.stringify(error, ['message', 'stack'], 2));
} finally {
this.logger.debug('Saving notification data in DB');
await this.notificationRepository.save(notification);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ export class NotificationQueueProducer {

async addNotificationToQueue(queueType: string, notification: Notification): Promise<void> {
const provider = await this.providersService.getById(notification.providerId);
this.logger.debug(
`Fetched provider ${provider.providerId} from notification ${notification.id}`,
);
const queue = this.queueService.getOrCreateQueue(
queueType,
provider.channelType.toString(),
notification.providerId.toString(),
);
this.logger.debug(`Adding notification with id ${notification.id} to queue`);
await queue.add(notification.id.toString(), {
id: notification.id,
providerId: notification.providerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class NotificationsController {
): Promise<Record<string, unknown>> {
try {
// ApiKeyGuard checks if requested providerId is valid, correct channelType and applicationId present
this.logger.debug(`Notification Request Data: ${JSON.stringify(notificationData)}`);
const createdNotification =
await this.notificationService.createNotification(notificationData);
this.logger.log('Notification created successfully.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export class NotificationsService extends CoreService<Notification> {

// Get the applicationId currently being used for filtering data based on api key
const filterApplicationId = await this.getApplicationIdFromApiKey(authorizationHeader);
this.logger.debug(`Fetch notifications with applicationId: ${filterApplicationId}`);

const baseConditions = [
{ field: 'status', value: Status.ACTIVE },
Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/modules/notifications/queues/queue.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class QueueService {

getOrCreateQueue(action: string, providerType: string, providerId: string): Queue {
const queueName = `${action}-${providerType}-${providerId}`;
this.logger.debug(`Started process getOrCreateQueue for ${queueName}`);

if (!this.queues.has(queueName)) {
this.logger.log(`Creating new queue and worker for ${queueName}`);
Expand All @@ -84,6 +85,10 @@ export class QueueService {

private createWorker(action: string, providerType: string, queueName: string): void {
const processJob = async (job): Promise<void> => {
this.logger.debug(
`Processing notification ${JSON.stringify(job.data)}. Searching for action ${action} for channel type ${providerType}`,
);

switch (`${action}-${providerType}`) {
// SMTP cases
case `${QueueAction.SEND}-${ChannelType.SMTP}`:
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/modules/providers/mailgun/mailgun.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { Logger, Module } from '@nestjs/common';
import { MailgunService } from './mailgun.service';
import { ConfigModule } from '@nestjs/config';
import { ProvidersModule } from '../providers.module';
import { ProvidersService } from '../providers.service';

@Module({
imports: [ConfigModule, ProvidersModule],
providers: [MailgunService, ProvidersService],
providers: [MailgunService, ProvidersService, Logger],
exports: [MailgunService],
})
export class MailgunModule {}
10 changes: 9 additions & 1 deletion apps/api/src/modules/providers/mailgun/mailgun.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
import * as FormData from 'form-data';
import Mailgun, {
MailgunClientOptions,
Expand All @@ -18,12 +18,14 @@ export class MailgunService {
private mailgun: Mailgun;
private mailgunClient: ReturnType<Mailgun['client']>;
private mailgunDomain: string;
private logger: Logger;

constructor(private readonly providersService: ProvidersService) {
this.mailgun = new Mailgun(FormData);
}

async assignClient(providerId: number): Promise<void> {
this.logger.debug('Started assigning mailgun client');
const mailgunConfig = await this.providersService.getConfigById(providerId);
this.mailgunClient = this.mailgun.client({
username: 'api',
Expand All @@ -39,6 +41,7 @@ export class MailgunService {
): Promise<MessagesSendResult> {
try {
await this.assignClient(providerId);
this.logger.debug('Sending Mailgun email');
return this.mailgunClient.messages.create(this.mailgunDomain, mailgunNotificationData);
} catch (error) {
throw new Error(`Failed to send message: ${error.message}`);
Expand All @@ -48,6 +51,8 @@ export class MailgunService {
async formatNotificationData(
notificationData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
this.logger.debug('Formatting notification data for mailgun');

if (notificationData.attachments) {
const formattedNotificationData = { ...notificationData };

Expand All @@ -65,6 +70,7 @@ export class MailgunService {
private async formatAttachments(
attachments: CreateNotificationAttachmentDto[],
): Promise<{ filename: string; data: Buffer; contentType: string }[]> {
this.logger.debug('Formatting attachments for mailgun');
return Promise.all(
attachments.map(async (attachment) => {
let data: Buffer | string | Stream = attachment.content;
Expand Down Expand Up @@ -92,10 +98,12 @@ export class MailgunService {

async getDeliveryStatus(messageId: string, providerId: number): Promise<DomainEvent> {
try {
this.logger.debug('Fetching delivery status from mailgun');
await this.assignClient(providerId);
const response = await this.mailgunClient.events.get(this.mailgunDomain, {
'message-id': messageId,
});
this.logger.debug(`Delivery status: ${response.items[0]}`);
return response.items[0];
} catch (error) {
throw new Error(`Failed to fetch delivery status: ${error.message}`);
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/modules/providers/providers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ export class ProvidersService extends CoreService<Provider> {
}

async getConfigById(providerId: number): Promise<Record<string, unknown> | null> {
this.logger.debug(`Fetching config for provider with id: ${providerId}`);
const configEntity = await this.providerRepository.findOne({
where: { providerId, status: Status.ACTIVE },
});

if (configEntity) {
this.logger.debug('config entry fetched successfully');
return configEntity.configuration as unknown as Record<string, unknown>;
}

Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/modules/providers/push-sns/push-sns.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { Logger, Module } from '@nestjs/common';
import { PushSnsService } from './push-sns.service';
import { ConfigModule } from '@nestjs/config';
import { ProvidersModule } from '../providers.module';
import { ProvidersService } from '../providers.service';

@Module({
imports: [ConfigModule, ProvidersModule],
providers: [PushSnsService, ProvidersService],
providers: [PushSnsService, ProvidersService, Logger],
exports: [PushSnsService],
})
export class PushSnsModule {}
5 changes: 4 additions & 1 deletion apps/api/src/modules/providers/push-sns/push-sns.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { SNS } from 'aws-sdk';
import { ProvidersService } from '../providers.service';

Expand All @@ -10,10 +10,12 @@ export interface PushSnsData {
@Injectable()
export class PushSnsService {
private sns: SNS;
private logger: Logger;

constructor(private readonly providersService: ProvidersService) {}

async assignSnsConfig(providerId: number): Promise<void> {
this.logger.debug('Started assigning SNS client');
const snsConfig = await this.providersService.getConfigById(providerId);

this.sns = new SNS({
Expand All @@ -33,6 +35,7 @@ export class PushSnsService {
TargetArn: data.target,
};

this.logger.debug('Sending SNS push notification');
return this.sns.publish(params).promise();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Logger, Module } from '@nestjs/common';
import { SmsKapsystemService } from './sms-kapsystem.service';
import { ConfigModule } from '@nestjs/config';
import { ProvidersModule } from '../providers.module';
Expand All @@ -7,7 +7,7 @@ import { HttpModule } from '@nestjs/axios';

@Module({
imports: [HttpModule, ConfigModule, ProvidersModule],
providers: [SmsKapsystemService, ProvidersService],
providers: [SmsKapsystemService, ProvidersService, Logger],
exports: [SmsKapsystemService],
})
export class SmsKapsystemModule {}
Loading
Loading