Skip to content

Commit

Permalink
Fix #4 add error notify to discord webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
boly38 committed Jun 8, 2024
1 parent d2fd987 commit 46c1f01
Show file tree
Hide file tree
Showing 10 changed files with 740 additions and 94 deletions.
469 changes: 459 additions & 10 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@
"@logtail/winston": "^0.4.21",
"axios": "^1.6.8",
"dayjs": "^1.11.11",
"discord.js": "^14.15.3",
"dotenv-flow": "^4.1.0",
"ejs": "^3.1.10",
"express": "^4.19.2",
"http-errors": "^2.0.0",
"http-status-codes": "^2.3.0",
"node-dependency-injection": "^3.1.2",
"superagent": "^9.0.2",
Expand Down
47 changes: 34 additions & 13 deletions src/config/ApplicationConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import LogtailService from "../services/LogtailService.js";
import UnMute from "../plugins/UnMute.js";
import AskPlantnet from "../plugins/AskPlantnet.js";
import PlantnetCommonService from "../services/PlantnetCommonService.js";
import {nowHuman} from "../lib/Common.js";
import DiscordSendService from "../servicesExternal/DiscordSendService.js";
import AuditLogsService from "../services/AuditLogsService.js";

export default class ApplicationConfig {
constructor() {
Expand All @@ -35,13 +38,18 @@ export default class ApplicationConfig {
container.register('newsService', NewsService)
.addArgument(container.get('loggerService'))
.addArgument(container.get('logtailService'));
container.register('discordService', DiscordSendService)
.addArgument(container.get('config'));
container.register('auditLogsService', AuditLogsService)
.addArgument(container.get('discordService'));

container.register('blueskyService', BlueSkyService)
.addArgument(container.get('config'))
.addArgument(container.get('loggerService'));

container.register('plantnetCommonService', PlantnetCommonService)
.addArgument(container.get('loggerService'))
.addArgument(container.get('auditLogsService'))
.addArgument(container.get('blueskyService'));

container.register('plantnetApiService', PlantnetApiService)
Expand Down Expand Up @@ -84,6 +92,7 @@ export default class ApplicationConfig {
container.register('botService', BotService)
.addArgument(container.get('config'))
.addArgument(container.get('loggerService'))
.addArgument(container.get('auditLogsService'))
.addArgument(container.get('newsService'))
.addArgument(this.plugins)
;
Expand All @@ -93,27 +102,22 @@ export default class ApplicationConfig {
return this.container.get(beanName);
}

initExpressServer() {
const {container, logger} = this;
async initExpressServer() {
const {container} = this;
container
.register('expressServer', ExpressServer)
.addArgument({
config: container.get('config'),
loggerService: container.get('loggerService'),
botService: container.get('botService'),
blueskyService: container.get('blueskyService'),
newsService: container.get('newsService')
newsService: container.get('newsService'),
auditLogsService: container.get('auditLogsService')
});

const expressServer = container.get('expressServer');
return new Promise((resolve, reject) => {
ApplicationConfig.listeningServer = expressServer.init()
.then(resolve)
.catch(errInitServer => {
logger.error("Error, unable to init express server:" + errInitServer);
reject(new Error("Init failed"));
});
});
ApplicationConfig.listeningServer = await expressServer.init();
return ApplicationConfig.listeningServer;
}
}

Expand All @@ -125,9 +129,26 @@ ApplicationConfig.getInstance = function getInstance() {
}
return ApplicationConfig.singleton;
};
ApplicationConfig.startServerMode = () => ApplicationConfig.getInstance().initExpressServer();
ApplicationConfig.stopServerMode = () => {
ApplicationConfig.sendAuditLogs = async () => {
await ApplicationConfig.getInstance().get("auditLogsService").notifyLogs();
}
ApplicationConfig.startServerMode = async () => {
// https://nodejs.org/api/process.html#process_process_kill_pid_signal
process.on('exit', () => console.log(`exit les pointes sèches`));
process.on('SIGINT', () => ApplicationConfig.stopServerMode("SIGINT"));
// render.com sequence of events : https://docs.render.com/deploys#sequence-of-events
process.on('SIGTERM', () => ApplicationConfig.stopServerMode("SIGTERM"));

// adding handler for SIGKILL produces Error: uv_signal_start EINVAL issue
// - https://stackoverflow.com/questions/16311347/node-script-throws-uv-signal-start-einval
// - https://github.com/nodejs/node-v0.x-archive/issues/6339
// process.on('SIGKILL', () => ApplicationConfig.stopServerMode("SIGKILL"));
return ApplicationConfig.getInstance().initExpressServer();
}
ApplicationConfig.stopServerMode = async (origin = "unknown") => {
console.log(`${nowHuman()} stopServerMode (origin:${origin})`);
if (ApplicationConfig.listeningServer !== undefined) {
ApplicationConfig.listeningServer.close();
}
await ApplicationConfig.sendAuditLogs();
};
1 change: 1 addition & 0 deletions src/config/ApplicationProperties.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default class ApplicationProperties {
this.port = getEnvInt("PORT", 5000);
this.isProd = this.nodeEnv === 'production';

this.discordWebhookUrl = getEnv("BOT_DISCORD_WEBHOOK_URL", null);
this.log = {
logtailToken: getEnv("LOG_LOGTAIL_TOKEN", null),
logtailApiV1: "https://logs.betterstack.com/api/v1",
Expand Down
9 changes: 5 additions & 4 deletions src/lib/Common.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import axios from "axios"; // dependent on utc plugin
dayjs.extend(utc)
dayjs.extend(timezone)

export const DEFAULT_TZ = 'Europe/Paris'
export const BES_DATE_FORMAT = "YYYY-MM-DD HH:mm:ss";
const __dirname = path.resolve();

Expand Down Expand Up @@ -51,14 +52,14 @@ export const assumePropertyIsSet = (expectedValue, name) => {
export const nowISO8601 = () => dayjs().toISOString(); // '2019-01-25T02:00:00.000Z'
// dayjs doc: https://day.js.org/docs/en/manipulate/subtract
export const nowMinusHoursUTCISO = (nbHours = 1) => dayjs.utc().subtract(nbHours, 'hour').toISOString()
export const nowHuman = tz => dayjs().tz(tz).format(BES_DATE_FORMAT);
export const toHuman = (utcDateTimeString, tz) => {
export const nowHuman = (tz = DEFAULT_TZ) => dayjs().tz(tz).format(BES_DATE_FORMAT);
export const toHuman = (utcDateTimeString, tz= DEFAULT_TZ) => {
return dayjs.utc(utcDateTimeString).tz(tz).format(BES_DATE_FORMAT);
};
export const toHumanDay = (utcDateTimeString, tz) => {
export const toHumanDay = (utcDateTimeString, tz= DEFAULT_TZ) => {
return dayjs.utc(utcDateTimeString).tz(tz).format("YYYY-MM-DD");
};
export const toHumanTime = (utcDateTimeString, tz) => {
export const toHumanTime = (utcDateTimeString, tz= DEFAULT_TZ) => {
return dayjs.utc(utcDateTimeString).tz(tz).format("HH:mm:ss");
};
/*
Expand Down
62 changes: 62 additions & 0 deletions src/services/AuditLogsService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import dayjs from 'dayjs';
import {DEFAULT_TZ} from "../lib/Common.js";

class AuditLogsSingleton {
constructor() {
this.auditLogs = [];// in memory audit logs
}

static get() {
if (!AuditLogsSingleton.instance) {
AuditLogsSingleton.instance = new AuditLogsSingleton();
}
return AuditLogsSingleton.instance;
}
}

export default class AuditLogsService {

constructor(discordService) {
this.discordService = discordService;
}

createAuditLog(log) {
const instant = dayjs(new Date()).tz(DEFAULT_TZ).format("DD/MM/YYYY à HH:mm");
AuditLogsSingleton.get().auditLogs.push({instant, log})
}

retrieveAuditLogs(cleanup = false) {
const logs = AuditLogsSingleton.get().auditLogs;
const clone = [].concat(logs);
if (true === cleanup) {
AuditLogsSingleton.get().auditLogs = [];
}
return clone;
}

async notifyLogs() {
const logs = this.retrieveAuditLogs(true);
console.log(`*${logs?.length}`);// debug
if (logs === undefined || logs.length < 1) {
return;
}
let markdown = this.formatMarkdownMessageFromLogs(logs);
return await this.discordService.sendMessage(markdown);
}

formatMarkdownMessageFromLogs(logs) {
const count = logs.length;
const nbMsg = count > 1 ? `${count} messages` : `${count} message`;
let markdown = `${nbMsg} :\n` + "```\n";
markdown += logs.map(entry => `${entry.instant}|${entry.log}`).join(" \n");
markdown += "\n```";

//~ prevent discord limit
const MAX_DISCORD_MSG_LENGTH = 2000;
if (markdown.length > MAX_DISCORD_MSG_LENGTH) {
markdown = markdown.substring(0, MAX_DISCORD_MSG_LENGTH - 12);
markdown += "\n(...)\n```";
}
return markdown;
}
}
72 changes: 40 additions & 32 deletions src/services/BotService.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export default class BotService {

constructor(config, loggerService, newsService, plugins) {
this.logger = loggerService.getLogger().child({ label: 'BotService' });
constructor(config, loggerService, auditLogsService, newsService, plugins) {
this.logger = loggerService.getLogger().child({label: 'BotService'});
this.intervalMs = config.bot.engineMinIntervalMs;
this.auditLogsService = auditLogsService;
this.newsService = newsService;
this.plugins = plugins;
}
Expand All @@ -28,38 +29,45 @@ export default class BotService {
return `${this.plugins.length} plugin(s) : ${pluginsNames}`;
}

process(remoteAddress, doSimulate, pluginName) {
assumeProcessRateLimit(remoteAddress) {
const bot = this;
const nowMs = (new Date()).getTime();
const allowedTs = bot.lastProcess + bot.intervalMs;
const needToWaitSec = Math.floor((allowedTs - nowMs) / 1000);
if (bot.lastProcess && allowedTs > nowMs) {
bot.logger.info(`${remoteAddress} | need to wait ${needToWaitSec} sec`);
throw {"message": "Demande trop rapprochée, retentez plus tard", "status": 429};
}
bot.lastProcess = nowMs;
}

assumeBotReadyPlugin(pluginName, remoteAddress) {
const bot = this;
const plugin = bot.getPluginByName(pluginName);
if (!plugin || !plugin.isReady()) {
bot.logger.info(`${remoteAddress} | no plugin '${pluginName}' available`);
throw {"message": "je suis actuellement en maintenance, retentez plus tard", "status": 503};
}
return plugin;
}

async process(remoteAddress, doSimulate, pluginName) {
const bot = this;
const context = {remoteAddress, pluginName};
return new Promise((resolve, reject) => {
const nowMs = (new Date()).getTime();
const allowedTs = bot.lastProcess + bot.intervalMs;
const needToWaitSec = Math.floor((allowedTs - nowMs) / 1000);
if (bot.lastProcess && allowedTs > nowMs) {
bot.logger.info(remoteAddress + " | need to wait " + needToWaitSec + " sec");
reject({"message": "Demande trop rapprochée, retentez plus tard", "status": 429});
return;
}
bot.lastProcess = nowMs;
const plugin = bot.getPluginByName(pluginName);
if (!plugin || !plugin.isReady()) {
bot.logger.info(remoteAddress + ` | no plugin '${pluginName}' available`);
reject({"message": "je suis actuellement en maintenance, retentez plus tard", "status": 503});
return;
}
bot.logger.info( `${(doSimulate ? "Simulation" : "Exécution")} du plugin - ${pluginName}`, context);
plugin.process({"doSimulate": doSimulate, context})
.then(result => {
bot.logger.info(`plugin result ${result.text}`, context);
bot.newsService.add(result.html);
resolve(result);
})
.catch(err => {
bot.logger.warn(`plugin error: ${err.message}`, context);
bot.newsService.add(err.html ? err.html : err.message);
reject(err);
});
});
this.assumeProcessRateLimit(bot, remoteAddress);
const plugin = this.assumeBotReadyPlugin(pluginName, remoteAddress);
bot.logger.info(`${(doSimulate ? "Simulation" : "Exécution")} du plugin - ${pluginName}`, context);
try {
const result = await plugin.process({"doSimulate": doSimulate, context});
bot.logger.info(`plugin result ${result.text}`, context);
bot.newsService.add(result.html);
return result;
} catch (err) {
bot.logger.warn(`plugin error: ${err.message}`, context);
bot.auditLogsService.createAuditLog(`${err.message} ${JSON.stringify(context)}`);
bot.newsService.add(err.html ? err.html : err.message);
throw err;
}
}

getState() {
Expand Down
Loading

0 comments on commit 46c1f01

Please sign in to comment.