Skip to content

Commit 304e19a

Browse files
authored
feat: ledger id null trigger via email (#1526)
* feat: alert via email while ledgerId gets set to null Signed-off-by: Krishna Waske <[email protected]> * feat: alert via email while ledgerId gets set to null Signed-off-by: Krishna Waske <[email protected]> * fix: add pg query listen Signed-off-by: Krishna Waske <[email protected]> * fix: multiple event listner handle Signed-off-by: Krishna Waske <[email protected]> * fix: log enabling of alerts Signed-off-by: Krishna Waske <[email protected]> * fix: add env demo and sample events Signed-off-by: Krishna Waske <[email protected]> * fix: remove unwanted imports Signed-off-by: Krishna Waske <[email protected]> * fix: change toLocaleLowercase to toLowerCase Signed-off-by: Krishna Waske <[email protected]> * fix: gracefully handle pg connect Signed-off-by: Krishna Waske <[email protected]> * fix: increase try catch scope Signed-off-by: Krishna Waske <[email protected]> * fix: handle missing env variables gracefully Signed-off-by: Krishna Waske <[email protected]> * fix: handle positive scenario properly Signed-off-by: Krishna Waske <[email protected]> * fix: handle positive scenario properly Signed-off-by: Krishna Waske <[email protected]> * fix: handle empty org_agents table in alerts Signed-off-by: Krishna Waske <[email protected]> * fix: handle retry logic and multiple notifications for send email Signed-off-by: Krishna Waske <[email protected]> * fix: make pg obj readonly Signed-off-by: Krishna Waske <[email protected]> * finally reset flag Signed-off-by: Krishna Waske <[email protected]> * fix: Only initialize PgClient when DB_ALERT_ENABLE is true Signed-off-by: Krishna Waske <[email protected]> * fix: toLowerLocaleCase to toLowerCase Signed-off-by: Krishna Waske <[email protected]> * fix: TODOs regarding email from platformconfig Signed-off-by: Krishna Waske <[email protected]> * fix: cosmetic changes and missing env variable add in sample and demo Signed-off-by: Krishna Waske <[email protected]> * fix: minor code rabbit changes Signed-off-by: Krishna Waske <[email protected]> * fix: percentage threshold from common constants Signed-off-by: Krishna Waske <[email protected]> --------- Signed-off-by: Krishna Waske <[email protected]>
1 parent 3ab4ac6 commit 304e19a

File tree

17 files changed

+380
-127
lines changed

17 files changed

+380
-127
lines changed

.env.demo

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,7 @@ RESEND_API_KEY=re_xxxxxxxxxx
223223

224224
# Prisma log type. Default set to error
225225
PRISMA_LOGS = error
226+
# HIDE_EXPERIMENTAL_OIDC_CONTROLLERS=true
227+
228+
# DB_ALERT_ENABLE=
229+
# DB_ALERT_EMAILS=

.env.sample

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,11 @@ RESEND_API_KEY=re_xxxxxxxxxx
242242

243243
# Prisma log type. Ideally should have only error or warn enabled. Having query enabled can add a lot of unwanted logging for all types of queries being run
244244
# PRISMA_LOGS = error,warn,query
245+
246+
# Default is true too, if nothing is passed
247+
# HIDE_EXPERIMENTAL_OIDC_CONTROLLERS=
248+
249+
# Comma separated emails that need to be alerted in case the 'ledgerId' is set to null
250+
# DB_ALERT_EMAILS=
251+
# Boolean: to enable/disable db alerts. This needs the 'utility' microservice
252+
# DB_ALERT_ENABLE=

apps/api-gateway/src/main.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ async function bootstrap(): Promise<void> {
119119
);
120120
app.useGlobalInterceptors(new NatsInterceptor());
121121
await app.listen(process.env.API_GATEWAY_PORT, `${process.env.API_GATEWAY_HOST}`);
122-
Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`);
122+
Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`, 'Success');
123+
124+
if ('true' === process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase()) {
125+
// in case it is enabled, log that
126+
Logger.log(
127+
"We have enabled DB alert for 'ledger_null' instances. This would send email in case the 'ledger_id' column in 'org_agents' table is set to null",
128+
'DB alert enabled'
129+
);
130+
}
123131
}
124132
bootstrap();

apps/api-gateway/src/utilities/utilities.service.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,121 @@ import { BaseService } from 'libs/service/base.service';
33
import { StoreObjectDto, UtilitiesDto } from './dtos/shortening-url.dto';
44
import { NATSClient } from '@credebl/common/NATSClient';
55
import { ClientProxy } from '@nestjs/microservices';
6+
import { Client as PgClient } from 'pg';
7+
import { CommonConstants } from '@credebl/common/common.constant';
68

79
@Injectable()
810
export class UtilitiesService extends BaseService {
11+
private readonly pg: PgClient;
12+
private isSendingNatsAlert = false;
13+
914
constructor(
1015
@Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy,
1116
private readonly natsClient: NATSClient
1217
) {
13-
super('OrganizationService');
18+
super('UtilitiesService');
19+
if ('true' === process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase()) {
20+
if (!process.env.DATABASE_URL) {
21+
throw new Error('DATABASE_URL environment variable is required');
22+
} else {
23+
this.pg = new PgClient({
24+
connectionString: process.env.DATABASE_URL
25+
});
26+
}
27+
}
28+
}
29+
30+
// TODO: I think it would be better, if we add all the event listening and email sending logic in a common library instead of it being scattered across here and the utility microservice
31+
async onModuleInit(): Promise<void> {
32+
try {
33+
if ('true' !== process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase()) {
34+
// in case it is not enabled, return
35+
return;
36+
}
37+
await this.pg.connect();
38+
await this.pg.query('LISTEN ledger_null');
39+
this.logger.log('PostgreSQL notification listener connected');
40+
} catch (err) {
41+
this.logger.error(`Failed to connect PostgreSQL listener: ${err?.message}`);
42+
throw err;
43+
}
44+
45+
this.pg.on('notification', async (msg) => {
46+
if ('true' !== process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase()) {
47+
// in case it is not enabled, return
48+
return;
49+
}
50+
51+
if ('ledger_null' === msg.channel) {
52+
try {
53+
if (this.isSendingNatsAlert) {
54+
this.logger.warn('Skipping duplicate NATS alert send...');
55+
return;
56+
}
57+
58+
this.isSendingNatsAlert = true;
59+
60+
// Step 1: Count total records
61+
const totalRes = await this.pg.query('SELECT COUNT(*) FROM org_agents');
62+
const total = Number(totalRes.rows[0].count);
63+
64+
// If the org_agents table has no records, total will be 0, causing a division by zero resulting in Infinity or NaN
65+
if (0 === total) {
66+
this.logger.debug('No org_agents records found, skipping alert check');
67+
return;
68+
}
69+
70+
// Step 2: Count NULL ledgerId records
71+
const nullRes = await this.pg.query('SELECT COUNT(*) FROM org_agents WHERE "ledgerId" IS NULL');
72+
const nullCount = Number(nullRes.rows[0].count);
73+
74+
// Step 3: Calculate %
75+
const percent = (nullCount / total) * 100;
76+
77+
// Condition: > 30% for now
78+
if (CommonConstants.AFFECTED_RECORDS_THRESHOLD_PERCENTAGE_FOR_DB_ALERT >= percent) {
79+
return;
80+
}
81+
82+
const alertEmails =
83+
process.env.DB_ALERT_EMAILS?.split(',')
84+
.map((e) => e.trim())
85+
.filter((e) => 0 < e.length) || [];
86+
87+
if (0 === alertEmails.length) {
88+
this.logger.error(
89+
`DB_ALERT_EMAILS is empty, skipping alert. There is a ${percent}% records are set to null for 'ledgerId' in 'org_agents' table`,
90+
'DB alert'
91+
);
92+
return;
93+
}
94+
95+
const emailDto = {
96+
emailFrom: '',
97+
emailTo: alertEmails,
98+
emailSubject: '[ALERT] More than 30% org_agents ledgerId is NULL',
99+
emailText: `ALERT: ${percent.toFixed(2)}% of org_agents records currently have ledgerId = NULL.`,
100+
emailHtml: `<p><strong>ALERT:</strong> ${percent.toFixed(
101+
2
102+
)}% of <code>org_agents</code> have <code>ledgerId</code> = NULL.</p>`
103+
};
104+
105+
const result = await this.natsClient.sendNatsMessage(this.serviceProxy, 'alert-db-ledgerId-null', {
106+
emailDto
107+
});
108+
this.logger.debug('Received result', JSON.stringify(result, null, 2));
109+
} catch (err) {
110+
this.logger.error(err?.message ?? 'Error in ledgerId alert handler');
111+
} finally {
112+
// Once its done, reset the flag
113+
this.isSendingNatsAlert = false;
114+
}
115+
}
116+
});
117+
}
118+
119+
async onModuleDestroy(): Promise<void> {
120+
await this.pg?.end();
14121
}
15122

16123
async createShorteningUrl(shorteningUrlDto: UtilitiesDto): Promise<string> {

apps/ledger/src/ledger.module.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Logger, Module } from '@nestjs/common';
22
import { LedgerController } from './ledger.controller';
33
import { LedgerService } from './ledger.service';
44
import { SchemaModule } from './schema/schema.module';
5-
import { PrismaService } from '@credebl/prisma-service';
5+
import { PrismaServiceModule } from '@credebl/prisma-service';
66
import { CredentialDefinitionModule } from './credential-definition/credential-definition.module';
77
import { ClientsModule, Transport } from '@nestjs/microservices';
88
import { LedgerRepository } from './repositories/ledger.repository';
@@ -16,23 +16,21 @@ import { ContextInterceptorModule } from '@credebl/context/contextInterceptorMod
1616
@Module({
1717
imports: [
1818
GlobalConfigModule,
19-
LoggerModule, PlatformConfig, ContextInterceptorModule,
19+
LoggerModule,
20+
PlatformConfig,
21+
ContextInterceptorModule,
2022
ClientsModule.register([
2123
{
2224
name: 'NATS_CLIENT',
2325
transport: Transport.NATS,
2426
options: getNatsOptions(CommonConstants.LEDGER_SERVICE, process.env.LEDGER_NKEY_SEED)
25-
2627
}
2728
]),
28-
SchemaModule, CredentialDefinitionModule
29+
SchemaModule,
30+
CredentialDefinitionModule,
31+
PrismaServiceModule
2932
],
3033
controllers: [LedgerController],
31-
providers: [
32-
LedgerService,
33-
PrismaService,
34-
LedgerRepository,
35-
Logger
36-
]
34+
providers: [LedgerService, LedgerRepository, Logger]
3735
})
38-
export class LedgerModule { }
36+
export class LedgerModule {}

apps/utility/src/utilities.controller.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Controller, Logger } from '@nestjs/common';
22
import { MessagePattern } from '@nestjs/microservices';
33
import { UtilitiesService } from './utilities.service';
44
import { IShorteningUrlData } from '../interfaces/shortening-url.interface';
5+
import { EmailDto } from '@credebl/common/dtos/email.dto';
56

67
@Controller()
78
export class UtilitiesController {
@@ -30,4 +31,17 @@ export class UtilitiesController {
3031
throw new Error('Error occured in Utility Microservices Controller');
3132
}
3233
}
34+
35+
@MessagePattern({ cmd: 'alert-db-ledgerId-null' })
36+
async handleLedgerAlert(payload: { emailDto: EmailDto }): Promise<void> {
37+
try {
38+
this.logger.debug('Received msg in alert-db-service');
39+
const result = await this.utilitiesService.handleLedgerAlert(payload.emailDto);
40+
this.logger.debug('Received result in alert-db-service');
41+
return result;
42+
} catch (error) {
43+
this.logger.error(error);
44+
throw error;
45+
}
46+
}
3347
}
Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,60 @@
1-
import { PrismaService } from "@credebl/prisma-service";
2-
import { Injectable, Logger } from "@nestjs/common";
1+
import { PrismaService } from '@credebl/prisma-service';
2+
import { Injectable, Logger } from '@nestjs/common';
33
// eslint-disable-next-line camelcase
4-
import { shortening_url } from "@prisma/client";
4+
import { platform_config, shortening_url } from '@prisma/client';
55

66
@Injectable()
77
export class UtilitiesRepository {
8-
constructor(
9-
private readonly prisma: PrismaService,
10-
private readonly logger: Logger
11-
) { }
12-
13-
async saveShorteningUrl(
14-
payload
15-
): Promise<object> {
16-
17-
try {
18-
19-
const { referenceId, invitationPayload } = payload;
20-
const storeShorteningUrl = await this.prisma.shortening_url.upsert({
21-
where: { referenceId },
22-
update: { invitationPayload },
23-
create: { referenceId, invitationPayload }
24-
});
25-
26-
this.logger.log(`[saveShorteningUrl] - shortening url details ${referenceId}`);
27-
return storeShorteningUrl;
28-
} catch (error) {
29-
this.logger.error(`Error in saveShorteningUrl: ${error} `);
30-
throw error;
31-
}
8+
constructor(
9+
private readonly prisma: PrismaService,
10+
private readonly logger: Logger
11+
) {}
12+
13+
async saveShorteningUrl(payload): Promise<object> {
14+
try {
15+
const { referenceId, invitationPayload } = payload;
16+
const storeShorteningUrl = await this.prisma.shortening_url.upsert({
17+
where: { referenceId },
18+
update: { invitationPayload },
19+
create: { referenceId, invitationPayload }
20+
});
21+
22+
this.logger.log(`[saveShorteningUrl] - shortening url details ${referenceId}`);
23+
return storeShorteningUrl;
24+
} catch (error) {
25+
this.logger.error(`Error in saveShorteningUrl: ${error} `);
26+
throw error;
3227
}
33-
34-
// eslint-disable-next-line camelcase
35-
async getShorteningUrl(referenceId): Promise<shortening_url> {
36-
try {
37-
38-
const storeShorteningUrl = await this.prisma.shortening_url.findUnique({
39-
where: {
40-
referenceId
41-
}
42-
});
43-
44-
this.logger.log(`[getShorteningUrl] - shortening url details ${referenceId}`);
45-
return storeShorteningUrl;
46-
} catch (error) {
47-
this.logger.error(`Error in getShorteningUrl: ${error} `);
48-
throw error;
28+
}
29+
30+
// eslint-disable-next-line camelcase
31+
async getShorteningUrl(referenceId): Promise<shortening_url> {
32+
try {
33+
const storeShorteningUrl = await this.prisma.shortening_url.findUnique({
34+
where: {
35+
referenceId
4936
}
37+
});
38+
39+
this.logger.log(`[getShorteningUrl] - shortening url details ${referenceId}`);
40+
return storeShorteningUrl;
41+
} catch (error) {
42+
this.logger.error(`Error in getShorteningUrl: ${error} `);
43+
throw error;
44+
}
45+
}
46+
47+
/**
48+
* Get platform config details
49+
* @returns
50+
*/
51+
// eslint-disable-next-line camelcase
52+
async getPlatformConfigDetails(): Promise<platform_config> {
53+
try {
54+
return this.prisma.platform_config.findFirst();
55+
} catch (error) {
56+
this.logger.error(`[getPlatformConfigDetails] - error: ${JSON.stringify(error)}`);
57+
throw error;
5058
}
51-
}
59+
}
60+
}

0 commit comments

Comments
 (0)