From ddf259dc1a8e2e7e4c38a67de09edd791629161e Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Mon, 7 Oct 2024 09:10:59 -0400 Subject: [PATCH 01/12] Initial send email routines (new) send email routines via notifyEmail (new) support for multiple recipients (new) support for html body with text fallback (new) support for embedded image/icon from message type (new) robust server response handling (fix) convertToIOResults not handling errors correctly (fix) log 'data' is optional --- server/http/routes/sandbox.ts | 9 +- server/records/logger/log.ts | 10 +- server/records/notify/notify.ts | 2 + server/records/notify/notifyEmail.ts | 283 ++++++++++++++++++++++++++ server/records/notify/notifyShared.ts | 68 +++++++ server/records/recordKeeper.ts | 117 ++++++++++- server/records/utils/rateManager.ts | 2 +- server/records/utils/utils.ts | 73 +++++++ 8 files changed, 548 insertions(+), 16 deletions(-) create mode 100644 server/records/notify/notify.ts create mode 100644 server/records/notify/notifyEmail.ts create mode 100644 server/records/notify/notifyShared.ts create mode 100644 server/records/utils/utils.ts diff --git a/server/http/routes/sandbox.ts b/server/http/routes/sandbox.ts index 51275d17..ff9f5b3b 100644 --- a/server/http/routes/sandbox.ts +++ b/server/http/routes/sandbox.ts @@ -4,11 +4,14 @@ import { RecordKeeper as RK } from '../../records/recordKeeper'; export const play = async (_req: Request, res: Response): Promise => { - RK.configure(); // 'D:\\Temp\\PackratTemp\\Logs' + await RK.configure(); // test our logging - const numLogs: number = 1000; - const result = await RK.logTest(numLogs); + // const numLogs: number = 1000; + // const result = await RK.logTest(numLogs); + + // test email notifications + const result = await RK.emailTest(); // return our results res.status(200).send(H.Helpers.JSONStringify({ message: result.error })); diff --git a/server/records/logger/log.ts b/server/records/logger/log.ts index 2fafa833..fa0add11 100644 --- a/server/records/logger/log.ts +++ b/server/records/logger/log.ts @@ -481,35 +481,35 @@ export class Logger { } // wrappers for each level of log - public static critical(section: LogSection, message: string, data: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static critical(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { if(this.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; this.postLog(this.getLogEntry('crit', message, data, audit, { section, caller, idUser, idRequest })); return { success: true, message: 'posted log message' }; } - public static error(section: LogSection, message: string, data: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static error(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { if(this.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; this.postLog(this.getLogEntry('error', message, data, audit, { section, caller, idUser, idRequest })); return { success: true, message: 'posted log message' }; } - public static warning(section: LogSection, message: string, data: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static warning(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { if(this.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; this.postLog(this.getLogEntry('warn', message, data, audit, { section, caller, idUser, idRequest })); return { success: true, message: 'posted log message' }; } - public static info(section: LogSection, message: string, data: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static info(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { if(this.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; this.postLog(this.getLogEntry('info', message, data, audit, { section, caller, idUser, idRequest })); return { success: true, message: 'posted log message' }; } - public static debug(section: LogSection, message: string, data: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static debug(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { if(this.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; diff --git a/server/records/notify/notify.ts b/server/records/notify/notify.ts new file mode 100644 index 00000000..c32ea9b1 --- /dev/null +++ b/server/records/notify/notify.ts @@ -0,0 +1,2 @@ +export { NotifyChannel, NotifyType, NotifyPackage } from './notifyShared'; +export { sendMessage as sendEmailMessage, sendMessageRaw as sendEmailMessageRaw } from './notifyEmail'; \ No newline at end of file diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts new file mode 100644 index 00000000..db5c65c7 --- /dev/null +++ b/server/records/notify/notifyEmail.ts @@ -0,0 +1,283 @@ +import * as NET from 'net'; +import * as SHARED from './notifyShared'; +import * as UTIL from '../utils/utils'; + +//#region UTILS +const storeServerResponse = (response: string): { statusCode: number, message: string}[] => { + + // see if we need to split it + const pieces: string[] = response.split('\n'); + + // split it up as needed + const result: { statusCode: number, message: string}[] = pieces.map(line => { + const firstSpaceIndex = line.indexOf(' '); + + // Extract status code (everything before the first space) + const statusCode = line.substring(0, firstSpaceIndex); + + // Extract message (everything after the first space) + const message = line.substring(firstSpaceIndex + 1).trim(); + + return { + statusCode: parseInt(statusCode), + message + }; + }).filter(item => !isNaN(item.statusCode) && item.message.length > 0); // Filter out invalid entries + return result; +}; +const verifyServerResponses = (responses): boolean => { + // first should always be 220 saying we connected to the server + if(responses[0].statusCode!=220) + return false; + + // next we check to see if the next one is a 250, accepting the command + if(responses[1].statusCode!=250) + return false; + + // finally, see if the connect closed with code 221 + if(responses[responses.length-1].statusCode!=221) + return false; + + return true; +}; +const extractErrorFromResponse = (responses): string => { + const errorMessages = responses + .filter(item => item.statusCode >= 400 && item.statusCode < 600) + .map(item => item.message); + + return errorMessages.join(' | '); +}; + + +//#endregion + +//#region FORMATING +export const getMessageIconBase64 = (type: SHARED.NotifyType): string => { + + // pre-converted base64 strings for each icon type + switch(type) { + case SHARED.NotifyType.SYSTEM_ERROR: + return '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'; + case SHARED.NotifyType.SYSTEM_NOTICE: + return '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'; + + case SHARED.NotifyType.JOB_FAILED: + return 'iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAAD6VJREFUeF7tnYHR7LQOhZdKHlTCoxKgkveoBKgEqASoBO5h/sPoaiRLTuzE2nhnGO7863Uc68uxLMvOF6/92T0woQe+mFDnrnL3wGuDtSGY0gMbrCnduivdYG0GpvTABmtKt+5KN1ibgSk9sMGa0q270g3WZmBKD2ywpnTrrnSDtRmY0gMbrCnduivdYG0GpvTABmtKt+5KN1ibgSk9sMGa0q270g3WZmBKD2ywpnTrrnSDtRmY0gMbrCnduivdYG0GpvTABmtKt+5KnwbWd6/X69fX6/XHxab/8tN1//t6vX66+Lq3Xe5pYP3vE1SA65sL4QJUv7xerx82WLdxPv3CAOv/H6r1/UVwASqqFa75iM/TFItGhnEBGFRk5kdeD0MwlPIRn6eB9fun4QhDEz8z4aI68lrw6756BFWfntingfWXYVgMT6Odagx9UCv9eUx/P+ZGP5QKimV9oCSjZopQxCuus7T4PQksT0VgIEA1YqbYggrXwTXga73950lgIczwY8OiI+CSzrp1qRnD7pKQPgks7UxbBoEz//PBYTGCCtebOVlYCrAngQW1gmpFnyPG96CFCspZKCYJj4hlPQksrSgwOgwNkPSnBy5viEUd+E6C9ZhY1pPA0qEGqoenZJmZojchIEC67sfEsp4MllQlHTjNzBS9GaCEx4I2A2w0XC///VPAsiCQYHGhWA5bLbhQDtBAseRHK5I1TG6wln8s8g20hiwdU/LgwpCJNUUZQPVmgLrOzHXzd1Go5EqKRbUYFQGXZrBmbZZyeLM7qW6eT2YFPy2wZsaycL0lArArgSVTWqgQoyCzgOG96+S/Flx/OkFWPazKpD5v0jBKf9D+rz9moD2z2VHXN+tZCSxrKMLTh/+OBi15063ZmZX8lwmmsm4dm8IwiTYzJUdPDEbEsgDTt4aPt4w9l2nIh5VaBiVkvx2Qe+0TyXgSodNPuzVT1E+njkux/RKe1rWzqkFVghLqCQbrWEat0KDVwPIcaMugGCapZNGQ2RqOvOS/qC2tGaAFrryHTL9nYJJ1LjXbzNxg9qkaVS679CKvx6EHxtaQRaEGrSjSQC24pLOuryHByk4ccD/SX+rpz6XUakXFQpta6S2ZzqZPxt04FlhyZqaHPJ3lAGMDDjkEaagAp/xeqlkUy/L8pcy9ssxSarUqWGjXEdWyDEG49OKzBMPKKtUKIFVHhwu8mBZHA+tBQbsAoucv9UC15PrjikOhp1oc7ji1PmMU3rdUMzjc0jm24EJ5mZ0ggYNKoY2EmCoSJf9lIGKQ1or2L6dWKysW2mbNymQnAgJARhh6QLPAYi6WHNZaGZ9aiVAW7WG2hGxrZoYpAQNInP3SZ7QAXXZRe1XFaqmWt4UKhqYv1IJMGkPCQYXSKmSlLGsjE0DpT0koM2BZMEnYLPdg2VTnlcHKqJY3jAAYBhA1ZNIn8UCI4PJCFLI+6Yt5fhhhinYJlVKr1YdCTr91nroMPsKQDDF4sSwOmUy6k76TBEg//VJlvLVC7ThLBZRgSbVB+xF/a63p0U8jcJZazVxzzPh9zTKrK1akWtKQGYMxzYVGlWBpJ1jHsACXXCu0/Bs9GaCjn1lg1wHRlvO/rG9F2iqAZQUYW9kGPUs/UgmsvsgGSOXTy/BFZk3QG7KjAOtyAVEtXxXAilSrZfxoEZu+T0sBIrB1n3IIbcWXuPDtTTJKq1UFH4tGi4ybCahakBGCaGiR14+UyAPLiuBbfornA7Ls8mpVCawe1QIkSFnBrNCLbnPZhxODTPSa2+ajgz3kDBA+lpXeQkjoF6IdVK/yalUNrEi1vGWXTHwrUiH0VcYBzyxH6RCDbPdbqFU1sHpVywpsepCNHF6sYbkVr5JhDWtFgOoWDdenQwQjK6jivGd9Le/pt/pMZhWMBEsm++llGd2Ot1SrioplzQBlmov8PnvIB1TMyuM6+gBnh0zULzMr5ENuZVyUEoEqjY2yKT3fZKQSHQXN+12PWqEOGZ8b+SCMvq9/6lsZrJ5sSq1O0m9ZMa1ERui175RZsCZoXBqKUrOnwNOqdCWwIlWKOkeunfX4WlG9M77vVauoDcup2Z1gcaG1FeeJOrSVajJStbj0gvbAIY+yEVrtbqkVfncmz4zX7VnWivr40PdXg3VWlXCT2VSTHtXiDBFDi4ZGJ/S1sgqYbcGhyjJKT7sImpcClDV6a7NJto6ucrPBGgkS89ejG6QSMoWZ5S1fC+oh030tZ19vhrCS63S+lBdzsnbz4L6gghmHfLSaZa4Z9bf5/Syw0AFymaKncVSMKGeJdcLw//kYQvTpL/K6FjTa0FYEXoOVOfPBm41GO6x7fSWda9bTz60H7kg9n/1mFli4iFaDVmOjtFz52x4VJKStQKX0xSyw9FBo9ZkGptWvParTAxrXRTPp2Ryqp732ZSZYhCGjXpHfIjdNRIBC3pGQl3Ww5aJxBJY3xMk6epdeRoNm7WOUfYb2ob9bGaxLK5ZuHH0ZLwcJNyqfoIziAQR01JHzHNg+qTZelgMj4RmwzgZls6Dph4DroC13ILPYfhoqVHCFYsmGZtSL27AY9LN+w9SYrCq1OksOdR5YUfKetzRz1kiW+nCGJxUn47shlWiqSsmbvRosqRLc3GB1viXXVqKcVrmjhowS/vi9p0aRovW2y1Mf7TJkHtRbNl3cBRade6boeh3PHcByycIaUq1yPcZkqks01EUzy7PDoDf86yEs4yaMeuh6+vHfsneCdca5bz3R2XiXnmkym9TqE4JnPf1yKD3Tn9ZwZsGxhHMe0XamI6K6e7/vde5Rv+d/HZn1cLhrgWUFR2noTHqz1SfeEK99ooxzflYxe23mll8JLA8U3Xjt3OP7Ef4XQwZWAJTwtGJYvUbN+lG4v+Wc84jA1cBie6MdLV4s5oz/RXiOgIXfRpsspC289GX9np1lnfOqYJ1x7nWkPBuw5O+8Y7UBgwUP/s7wR9Tf/F7nXGnfLeOcn52wZNt6qNyqiiVvpvXUWtCciYLD4JZ/RkNbJ93gelZWRGQQCZflpHsJf5dEzqPGR99XAMs6aQX3NRoq1An18fKt0A4rUxNgHXk7vc7ft1Ye9BGUtOeKWbGfsbY6WC2otLKcUSp2igdP9IAe/d46eESeae8dH5DdKHK0Xad/tzpY2XfWaKiOhBtanQkDY0gesYSkr/OWcK0M1h1QwcjI1tTZEdaSDReLAQrfQnH0SY/g8g4RuTW63rrZVcHyoNKzJzltH+HUyvp4LT3LpH8jnesRRzZacElgvVhWb/zsKPxdv1sRrGwH6ljQCOPqAz049FExYGj+TWY0jLg2QyzSYW8dCy4NvRxcq4HlrYPpRVgN30jDou5MXAoKky3b87S/hXKtBJYecmgMvQan4WNaM8oj951Pvpzp6b8xuNhj8KNl2V6GKmTIwvo3/Du9EUQrknce2C0pMlbHrAKWB5WOVUUr+1njj1K4zPW8e8v8tjXceQHUJWJcK4CVDYCegYrKwLToq+9b+mO6LT2ASeXyYlyo73a4ru5gK4bDk/Lkd9YMj/lSNAyGDH6iYQblMueN9hi5p2wrtVnuAbD+zeGdw7l8P/WyAdS7wcrGqnqM6JWlahzNmzrTBukTjVaTJeG6E6wWVNaanDSst9NHG5+bB+Rwm5mawy/K7BLmXr5ok4IcxvX7gHgv0T3j3rwyCOryHT6yD24LoN4FlgfVGVXQv82+gUL/jqGMzAyLZa3kQ1mvdOC9N1aMvHft9J99p3Z32+4AK3N0dveNGD/wwPLuWedAZYbMKPVFNst6scCZCUlPH2VUuqe+sOzVYF3VkbhxGVKIHHcvJNDyh7zZrKd0lgM/KhQRGvpjqDy7ppm5zj9lrgTryk7U99Zy3PWwDD+Gfk/rSW/loVu+jefAW+eNpg3YWfAy5boSLKbwtvpChhBa5bQTC+eVxxfRyWUasee4W5mp7Pjo0DZZJ4O4OgNBh0wkiK2XZHKns3f/0cRFhiesOi5RrSvB6ny4uopbqkOwrBcLaLVpQWANbd7haR6scJ4ZFsCNtd5lmPHtujrnjsLvApYeTjzHHbDp9yp7B2W0gprWof/SfnqCQhXCA4CPvKY1mSlvl/I38KEEOnovwWqFNlprhp5PJIfB1uktrS1sUpWsCc3oIOrlovUOYFmTAgmM5RxbzjWGN9TFjRGyXk8BdUwKQ6rOWdcKSSOz76P2Xw7FiAu+A1jW7Kz1Bi09M9I7kj3Hmj4b1VFmXkgV86DVp+u02pgJzo6w/7Q63hUs3pccZqyFbQvKljphxsWlE68cjRWd8yXh0Skwlx2QNousdwArMyPsPf1OqglThaFEXBuUcTIvUIoyrXiWbJMGq/zM8B3A0j6UNBig0Ke2ZJaUpBpF5yxEB3YAMK1ePKCXZzW83cywOliWWuhkOAZTMwdscGSQG0It51oOY9nIubXTmW2zwCo9M3xHsCzHN1IpGB0AyY/nA0mn3VKrKEPUO4aJiYxsw5Xp08NdrepgWTEgaZBIpejQ4//6nATp53iRdivvHErDTAlv+UVPJCJVHG742RVWB8tSDN5TpFKZnS+WE8/6rWFYO93eDmZr5tjyFWdzMLz+6mBpeKgE+HtWLdiplmropReUbTncrbO1ovboNpeeGVYHq5XyYj2FUdqIN7QBWIAHOACbpVZ6q5q+fqReunxU33CVGVlhdbCyMzIrOGr1o+WzWTBGgVXPRpHPp39XdmZYGaxWYFIaKFIpbcyWaqHsEbU6ql4brJEymqwryki1Xg2SqTpSo+j7zDVQJqNeZdcMKytWK+Ldq1KRasmAqaVoZ/qx5XttsLKP6cBy3pscRryMyApV8M1kTNaTIYOz6b6eepVdjD7zpA1k5FBVekZ4VqVkI6xhVi9Cs/xIP0irV9mQQ2Ww5M6bESql6c5sqp0REtDqVdJGJRstZmYjVUqDFU0OGCydceAt6qZ6Ieia2X5/SPZn/agyWJj2R2cmnO037wwq1DtDrSy4M2dInL3P4b+vCtbwjnAqnDnzvOoebrnOBivudku1rlCruGULl9hgxcYZFRCNr/RGJTZYsTGtg812vwX9tjsoBgslZMB05kw015oCpTZYOSPJ0MPIgGju6gVLbbDyRuNG1Z43qeZrf7OSG6y8QXlM0qyAaL4lBUpusAoYqWITN1gVrVagzRusAkaq2MQNVkWrFWjzBquAkSo2cYNV0WoF2rzBKmCkik3cYFW0WoE2b7AKGKliEzdYFa1WoM0brAJGqtjEDVZFqxVo8wargJEqNnGDVdFqBdq8wSpgpIpN3GBVtFqBNm+wChipYhM3WBWtVqDNG6wCRqrYxA1WRasVaPMGq4CRKjZxg1XRagXa/DeH3+DTjRG4SAAAAABJRU5ErkJggg=='; + case SHARED.NotifyType.JOB_PASSED: + return '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'; + case SHARED.NotifyType.JOB_STARTED: + return 'iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAAFBxJREFUeF7t3QXQPM1RBvAObgGCu3uwAMHd3d3dCa4BAiS4u1twd3d3t+Du7i73q9qpNFN7d7v33urNVP3re7+72b2dnmd7erqf7rlHtNYkMIEE7jHBPdstmwSiAauBYBIJNGBNItZ20washoFJJNCANYlY200bsBoGJpFAA9YkYm03bcBqGJhEAg1Yk4i13bQBq2FgEgk0YE0i1nbTBqyGgUkk0IA1iVjbTRuwGgYmkUAD1iRibTdtwGoYmEQCDViTiLXdtAGrYWASCTRgTSLWdtMGrIaBSSTQgDWJWNtNG7AaBiaRQAPWJGJtN23AahiYRAINWJOItd20AathYBIJ3CqwHiMinjwini4ifikifn0S6d7wTW8RWI8aEe8cEfeLiMePiO/r/v7lG8bB1Yd+a8B6uIh4+4j4xEqSrxkRX3N16d7wDW8NWG8TEZ9RzfefRcTzRsTv3zAOrj70WwIWm+pzI+IlkhT/PCJePyK+94xkHyYi/ufq0t/xDW8JWO8XER+a5pKmep+I+MIT8/vwEfF8EfFOEfFNEfFdEfGnO8bD1YZ2K8B6ms6GetYkua+KiNc+IUmyeZPD7vHzUx///+CrSX/HN7oVYL1xpZksgS8QEb99Ym6fLSK+ICKePfVh9L/LjvFwtaFtHVhsn+fopPGHEQEwdXuUiPiiiHj19MWXH3xXr3dCin3X/EVEvHJE/MTVpL/jG20dWDTRu0UE7fIVEfG+h+Xrd6v5eoqD1vn6rk/5Chh/7oy2+raIeMLU5wMi4kE7xsJVh7ZlYL1sRLxnRLx4ksjzRMRPVhK6z0HT/Gz6jFYDrD85IclPjoh3TN9/96H/W/eA9qqTsaebbRlYvOefUAHmRXrCMwx02qw0nnZL2j8dmUje+I+vlkr+r8/a08RPPZatAsvy9tERwWNe2tce4n6v0SOwN4iIL06f/2BEvFRE/McR4T5LtxN8zu57Gu65I+IPpp6MPd1/q8DiPvjqZDdZ1mihn+mZnBft4oHlq1/pgPIvRybSbvGH03e/1v3Of+5p4qcey1aBRYPk3Zkd2wtGxG/2COy5IuIbDkvZE3Xf/eLBDnv5iPjjI8K1nH5/+u6HDp75F556IvZ2/60Ci3YCltJMPs3UF3Z5qoj41Ihg7Jf2qtX1eV6fPyK+MiKeuPvwVztj/9/3NvlTjmerwHqdiOCLKs1yxZHZZzfxSTHG7epK+7rOOH/Eg4sCYP43Ih4hIiyPz3wISn97cjWwsew2W5B6BBK3CqwX63Z6j9uN1VL4ahHxjxFxz4h4koh4tG7544t6pk6jFdHo/5AOUH/VfQhcDxsRjxQRls9yb19/8OGzb+6A98/djtJvHdsAjJiCfXbdGrAeMyIe+6CtnqFjKnANlCaoDBzZqXntWROABsq/PoR2/q5jn/5WB1KfASnA3XxbO7AeubN1GN6ve9BSz9iB6glWOHM/320eMFHZZZyyf3SrWm2NwLIcMbhfpXMhPO2BnLdGIJ3D9i90nnosih+7Na/9moCFi26Je6uO0Sn+d0mzVP10RFii+J44NrkW/ruzve5fAfV7OqqyHaWl9W8i4sk6G8uz5OXW89BGjxMRjzfi4YBMvFK4Canw30Zcu8muawGWeB4u+isMtJF4zwGIsxPHiuGe21t2Nlg9KTjvPPaZ+sJuElz+tJ4ZFNSWdJE1JmB+S0e7sVQjAlqiH2uAZvVbCIPcH/xpu21LA4vt9OYR8Q5nJgWQTAQGJzDRKn/bzcq9u4ni2CztOw8T/UkdAOrJs2MU4sn9JVIg8dnx5Waz8DlVqIjH3/MCdmm0Fy133y4o7plce2wjwR77lG6n2Uf12TzglgTW0x98Rx9W8aSyQHm/AUkM0CQC07HGz8RhmpctSRNvd+SCN+w0V9FEDG7Bav6wuuFt8YPle9OQlrZjDaBoMI5cscp7Jc9/vgZoPzwifmfzSKoGsBSwxPo+u/ItlUczYXjobJ8xW3caCje9NFqOjfQbPZMmsYJfijO0tBeqYoTlc+DjjM0a7gER8SEDwUBzARen7lP3AIy2fPe9OWCXANajH5yYH3vYirODcqM1aBlv8SXhE07Nn0o3tGQClh1ZX0OfEQYq7c06KnJfX/ZX1n4oNO/d+bIG4itsTt7isKFgo9WGPzvOi7Ybo34JYKG62ILnhk1gebpL2IQ2+NFq0qR2ARgjmw30l53dY9z4XILZpQE1LYkpYYf4pN0OkN0FVO+V+iL+AcklVBraWhpaDmwz6nG+GPa7aEsAi82Ud3G27y99gm1wTNB2eLb9bBk2DFvIDq1w4F3HMOZmKMyGMZPGruPgxKsHTDZhaWjN79953nncj1Fwjv0ePx2bMC/FwkYfNOYB19x3bmABga02L3ppNAf7aEgDJCDBdVfY4ym7OOCUYZxTz2Wnyl+GB6a4iP+eojzne31MZ1uVz2hxy3G9Mx0il9X1mRtYvOgoKTmliu/qW09IRtKo63jiaTaaYykgnZtA4OKOsKQB3KnNh/HknSVGBQ7/LoqTzA0sS8CPVD4rTsiPODJjHI92U+yPLYV1xAktl5Y7L43luG5yFBntpfFtoVbvwvUwN7DQVxjTuWEl4Eplw1W80Paf8HP28jmNsbbvgcvSz64sDl3PWGsrnzHg0YF2UatrbmDVqVgFCIxs6VZ8OqgvbLAvWxtK7vA8b9u5MsQu7YrVkeiLhUrkaEvhBYKWjXyqDpUtP9fDy3VOxQt+YpWXlHikHMi+TKLy0OcSaVc5uL6HmltjDdFEbI1s3G9GmGce1JJ/zk60RH7jHgY8N7Dsej4qCY7a50I4J/A9yLoeA6D9a+cyKd/tprLg3MCqa1RJarAjBDYe6ZIZs0cg5TExB4yZL8smpTT1uoSl8PXZW+QhyQOfjH0m5GPXaAPEgZs3BKuS2dzAErzN3mV+HF54zAEhHT4tlYz3CjDxSbFQISHA4PPKNteQ5RKA7DYF10Ut/P0Dh+Io/7AmZM0NLGGLD0wCoLFyeSFLIvqKbbe6oHtYItmMPPIcoPIfM/+qDm9dgg0bA3YZsKqQswrP/dzAqlkCNbCKYMXmUFtesWNqCt1Ix7ok5nfJZN31GkRD8UMaCU1acL2PuSCMk+tP3PV3sS4wR/qoQne996jr1wqsPAhJpUI4lkuFadFj2GOM/rWEdgp37Pc6/xutRJOcyztka2WNbdyWNVrOPbAxAFKMVWjLC6dYiWD7seC6ZxF7XXR3OTew0HHRektjb+BMjWkETND+IdExfmU789CXvEN8J45W/+6q5QCkTDJGxd93gWa1I2gG+YWMaCyH/xozkAOV5/O6wDNSIlnYJYs3nroPtgUTAeXnXTubtDYZPK8wWC5DMPLR7tZ9i8A6NmJhIGQ6IPNmy2im7eyqcLF8Dxi0nHHnsQMgAOFh0QT+4VrROEiHtAbXgOWNDdMX+7tkJvj1LJMANYYtW34LyOwevbCKouT2JR3tuQ6hXfKco6+ZG1h1pbxLNNboQW70AjXAuBi8JIrEAbedX1/hE9rbpui1Kg3NhrUi0KaztrmBVfPSjxnvswphRT+GHkQDKQwnx5LGpGG5ISy3qjxbevH1Bauz5qSZVXmui/Zy49Bes7a5gYV1+cA0QsbrNXdFswrvij/GRuLPkluJAXKuMe7Rm7kw8L5KY2Oy2zJDVyKIAsCzHnwwN7B4lqU7lWa7jZc+1ug9J/gtfc8h7MSMS14wtVVdy+AvDd1Z2lw26HHaECxna3MDS0LCR6bR8USrrreb7JSRM9eXWFHf4lw8FWDe45Dcoc59fmEzUGUAoe7Mdh7Q3MCikjnwSlPHQHW9S3ZEI+dwdd1x9m1eak3FdwUs6ENcGrQ5QEibU5VQ2ly9XNYlBSTw/ngaMQ2GT3+sPObVhTM3sOxaskoWhqCmT2U5X33QK7khh68XK4NE7O+Nqrr09eOKQOCtZccqUqSk12JHKSPA45+zt1GRFCeZpc0NrPpNkuUiQWKX9QvOzKBdn81LAZbsHjs6ztJzTS4AUBY7Cog4RMtBCXx431HlLvKZ5Xr3537jTt/PDSx0XDua0vytvkG2D+40oA1dLCE2J7yO4bwDjoBzzuSu3Qo1c+JUpvfVxTY3sKRuMdhLjI8w1UToK6N99cGu7IYoQvxR+SXjJhBvHNJotszlkpnN1VBaHT6b9SyguYHlLZWNkxMJXiYisAFurTG47dZKIwNaZWjCq/4cqaU54Uwib2lKNSlGUppsoXw+0KTynhtYCtNa5/PxuZbC3dQsGDFbgvG0Smnk4rOh4ZeacqP6DSJlaQz6nHEu5JOd0yMedXzXuYHVZ1SOSbEfP8L1XvGlVfjFTo+fb4jrheGPXpOr1rxkV9SkjFi4jCuntHqpnFQycwPLYD6zKub/cVUNg0kHvKKb1xqHNsns2mOPitEgdyBrHzRnhnwpHMfksBTmijZs2SE7zquIaAlg1QkVs679V5Ha3W8CHDROprq86ZmDz/2qEgXsprrom9wBRedUx9E4Uu0aS7NJ4taZLRl2CWApLKv0Yml44Dju1+I43X3ap7+DYLEaFtk5KplVhg6PPHqMTB10GX39bZPDHiWr3GgreQKAWlq9zLLfGO7lFI7JR7gEsGT7MtYLs9NbRE3fkvcdtViRuNyU6kYkpM28ZOYGAVE4R2C5LgvuWuEf/it05PJioi7TVvnIFrUx8g50l8BSeU8cLEff2Qf5jZt84Av/QH0m4iWP44UU0KedcnCZdqLBSsPlYtjPyiRdQmNxOYgX5rOcz1UhvkTwa76mPvV1zLMCFPOBO8F/c+srwynljqE/q6mxBLD8pjSlXNzW2c4SA26lYXqqKz+0iVZYIsX/kPtEKuozrdV9UN48222C/Hxjs6eDLQEswqwJf5ZGRuktEP4kdViqLF/+BhLcdtlHwFLmBE0ZO7RkGzG82VR141rAGsl0JH3sBDlIZ3Mx5AdbClhSlzjwcmoWAZXt8tA3+Zb7yThyGqzjWnLMsMhEpURgW+Qs66WAJctZzluOGe6mhM/EaEdltqsUV8yHGpSfFWvkG5S4Ui+XEz/aQ2+/FLBklCD5Z9K/uFk+WWI2Icz8Q2TOFcBfJQ+Si8GyyFeFJepzTWjH0idPkpkgHYz95Ppjh58j8iH88ZEtSvdeClgE18d/ZysMDcLOjIer/By6kKWLB734q+QEApa5YBoU/9WYY+u4FFBw1Gydzbt+SiJLAovTD/Uj1194pYqjdJXZXNFNeL8l7V6rMdCFc2h/4Jp993dsIEsCi9pXuCLbCXuPG/Lf0cp3aeWcRssdBi5XxOqiFksCi3AddJRrvMuPkw62x92hVC9hnBxqwfk3B/5xPbCpuBdK87fPec39wy5lnwr/rFpGSwMLVdm5hDm8I4F1T6W4C0hqb7sEEnwpafNS6dlZCpCwvcxLMezVbuDfU+VmM21pYPHFWB7y7lDmilNXV1X68I4ziqEg2pCXQdX8TpXmvuNPLnv50sAyeiUhgSk7S73Ji9V2mmBK6vr2jG5H1e1pjP9PbGsAlu02VmnOCLbTkWO3qC/mSgDjh8LmzFpZ1EFi6irqhV5pnKsDlgcCIqfL52rJe0myqM/N4Rm3JNZ8rCnmd7F7rkFjGTzjHa8osyNpLYa8SnpbbbzpD+6xIRHvVuciuKaQ1wIsY7JrQlzLWotxy8jdauvjRx071HyrY+x97jUBi9biZshp49LEFSPro4usfSJQiXnE83i8JHhoqz1R4lpCXROwjAlPSaJF3iEOyV65ljyueR/sg5zybifIgFeaaPdtbcAS2ceuzImWIvYC1ltKw1caHNszO365VOwEt2wzDn4h1gYsD66OkyyTPCmLVf8dLMmHdsRK+PSqfhUvu41JSSi94LbbumSNwCLBOqnVZ/fv4oqzlTu8YCpFEjA364RSx+k56etm2lqBRVthOtTHgaw9jtiXfcNtIkS1e4M9vzVrBZZnVO9c7cycqIlzhGVqt7i2ZuOBaJeXcGwNztBdHCA+RuBrBpZxmBSTlcmAwCVVTPB6Dcui5e9+PVkyntMm5CZ2gTXo1g4sz6eYmHy5+rAlTEw142ctjF8JkDPXMlfbVJ5JiWzRhJtsaweWSVFTSyERS2ANLuQ3J7Yq9DpniEQ2N3tKaObeFXKASmkm/9agURcB9haARTBO8VK2G9u074xCZEHOSLXNMSun8BVhKch9dOiRMkE1oDwnqrAsGc9zs6AiiK0Aq7x1EjRVvmPY9zUTi5GpLPVDOjov5qVUKmlWmfZ77E0u7M17dqlYaD1Azb8mp+/YccKyYyyLShHdfNsasEwY7jgPNs1x7oRVy5I4o2XSdp8WYVT7b0ldL5zzcsCk/97rUCtVLXX9zv0GMIsBYjE4e7C1DWqsMml2YrSWovn4TkscSg6g6MZcH/mQpAasDQOrTJ4MFsUzgOu+3cHkU4Ks5O45A4c9d4v16Qe9OFtcCo8NDKnOrpFhLZh9n27ZlG5l2Tu3pNX3tcTJypbUYUOgaovPMEDZa62dkMCegFUPU/4dgDHCyznRipH4nB1VzoYu50ErTKYyCzvJrhIfndHvRNNbKK901Rdlz8C6qqDazcZJoAFrnLxa74ESaMAaKKjWbZwEGrDGyav1HiiBBqyBgmrdxkmgAWucvFrvgRJowBooqNZtnAQasMbJq/UeKIEGrIGCat3GSaABa5y8Wu+BEmjAGiio1m2cBBqwxsmr9R4ogQasgYJq3cZJoAFrnLxa74ESaMAaKKjWbZwEGrDGyav1HiiBBqyBgmrdxkmgAWucvFrvgRJowBooqNZtnAQasMbJq/UeKIEGrIGCat3GSaABa5y8Wu+BEmjAGiio1m2cBBqwxsmr9R4ogQasgYJq3cZJoAFrnLxa74ESaMAaKKjWbZwE/g8BwXjEyuIx8AAAAABJRU5ErkJggg=='; + + case SHARED.NotifyType.SECURITY_NOTICE: + return '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'; + + default: + return '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'; + } +}; +const formatTextBody = (type: SHARED.NotifyType, params: SHARED.NotifyPackage): string => { + const msgPrefix: string = SHARED.getMessagePrefixByType(type); + const startDate: Date = params.startDate; + const duration: string | undefined = (params.endDate) ? UTIL.getDurationString(params.startDate,params.endDate) : undefined; + + let result: string = `${msgPrefix.toUpperCase()}\n${params.message}\n`; + result += (params.detailsMessage) ? '\n'+params.detailsMessage : ''; + result += `\n\nStarted: ${UTIL.getFormattedDate(startDate)}`; + result += (duration) ? '\nDuration: '+duration : ''; + result += '\nWho: '+(params.sendTo?.join(', ') ?? 'NA'); + result += (params.detailsLink) ? `\n${UTIL.toTitleCase(params.detailsLink.label)}: ${params.detailsLink.url}` : ''; + + return result; +}; +const formatHtmlBody = (type: SHARED.NotifyType, params: SHARED.NotifyPackage): string => { + + const msgPrefix: string = SHARED.getMessagePrefixByType(type); + const startDate: Date = params.startDate; + const duration: string | undefined = (params.endDate) ? UTIL.getDurationString(params.startDate,params.endDate) : undefined; + + let result: string = ''; + result += ''; + + // head + result += ''; + result += ''; + result += ''; + result += `[${msgPrefix}] ${params.message}`; + result += ''; + + // body + result += ''; + result += '
'; + + // banner + result += '
'; + result += ''; // image references specific attachment by CID + result += '
'; + + // header and subtitle + result += `

[${msgPrefix}]

`; + result += `

${params.message}

`; + + // details paragraph + if(params.detailsMessage) { + result += '

'; + result += params.detailsMessage; + result += '

'; + } + result += '

'; + result += `Started: ${UTIL.getFormattedDate(startDate)}`; + result += '

'; + if(duration) { + result += '

'; + result += `Duration: ${duration}`; + result += '

'; + } + result += '

'; + result += `Who: ${params.sendTo?.join(', ') ?? 'NA'}`; + result += '

'; + + // more info button + if(params.detailsLink) { + result += '
'; + result += ``; + result += UTIL.toTitleCase(params.detailsLink.label); + result += ''; + result += '
'; + } + + // close body + result += '
'; + result += ''; + + // close html + result += ''; + + return result; +}; +//#endregion + +//#region PUBLIC +export const sendMessageRaw = async (type: SHARED.NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise => { + + // get our email addresses if needed + if(sendTo.length<=0) + return { success: false, message: 'failed to send message', data: { error: 'no addresses provided' } }; + + // get our SMTP parameters from config + // NOTE: currently unencrypted and insecure. do not send anything sensitive! + const smtpHost: string = 'smtp.si.edu'; + const smtpPort: number = 25; + + const from: string = 'maslowskiec@si.edu'; + const boundary: string = '----=_Packrat_Ops_Msg_001'; + + return new Promise((resolve) => { + const serverResponses: { statusCode: number, message: string}[] = []; + const client = NET.createConnection(smtpPort,smtpHost, () => { + // SMTP dialog + client.write('HELO si.edu\r\n'); + client.write(`MAIL FROM:<${from}>\r\n`); + for(const recipient of sendTo) + client.write(`RCPT TO:<${recipient}>\r\n`); + client.write('DATA\r\n'); + + // MIME email body with plain text and HTML parts + client.write(`From: ${from}\r\n`); + client.write(`To: ${sendTo.join(', ')}\r\n`); + client.write(`Subject: ${UTIL.truncateString(subject,60)}\r\n`); + client.write('MIME-Version: 1.0\r\n'); + client.write(`Content-Type: multipart/alternative; boundary="${boundary}"\r\n`); + client.write('\r\n'); + + // Plain-text part + client.write(`--${boundary}\r\n`); + client.write('Content-Type: text/plain; charset="UTF-8"\r\n'); + client.write('Content-Transfer-Encoding: 7bit\r\n'); + client.write('\r\n'); + client.write(`${textBody}\r\n`); + + // HTML part + if(htmlBody) { + client.write(`--${boundary}\r\n`); + client.write('Content-Type: text/html; charset="UTF-8"\r\n'); + client.write('Content-Transfer-Encoding: 7bit\r\n'); + client.write('\r\n'); + client.write(`${htmlBody}\r\n`); + } + + // attachments + // NOTE: we need to put all images as attachments and then reference by CID + // for compatability since GMail removes any base64 embedded images + client.write(`--${boundary}\r\n`); + client.write('Content-Type: image/png; name="header.png"\r\n'); + client.write('Content-Disposition: inline; filename="header.png"\r\n'); + client.write('Content-Transfer-Encoding: base64\r\n'); + client.write('Content-ID: <0123456789>\r\n'); + client.write('Content-Location: header.png\r\n'); + client.write('\r\n'); + + // add our base64 icon from the type + const base64Icon: string = getMessageIconBase64(type); + client.write(`${base64Icon}\r\n`); + + // End of message + client.write(`--${boundary}--\r\n`); + client.write('.\r\n'); + client.write('QUIT\r\n'); + }); + + client.on('data', (data) => { + // get our data and make sure it's not an error + const response = data.toString(); + serverResponses.push(...storeServerResponse(response)); + + // see if we have an errors in the mix + const errors = extractErrorFromResponse(serverResponses); + + // Handle server responses + if (errors.length > 0) { + resolve({ + success: false, + message: 'failed to send email.', + data: { error: errors } + }); + } + }); + + client.on('end', () => { + // console.log('Connection closed.'); + // go through our responses and see if it makes sense + if(verifyServerResponses(serverResponses)===true) + resolve({ success: true, message: 'email sent' }); + else + resolve({ success: false, message: 'failed to send email', data: { error: extractErrorFromResponse(serverResponses) } }); + }); + + client.on('error', (err) => { + // console.error('Error:', err); + resolve({ + success: false, + message: 'failed to send email.', + data: { error: UTIL.getErrorString(err) } + }); + }); + }); +}; +export const sendMessage = async (notifyType: SHARED.NotifyType, params: SHARED.NotifyPackage): Promise => { + + // if we have sendTo address(es) then we ignore the channel + if(!params.sendTo) + return { success: false, message: 'failed to send message', data: { error: 'no email address provided.' } }; + + // build our text and html bodies + const textBody: string = formatTextBody(notifyType, params); + const htmlBody: string = formatHtmlBody(notifyType, params); + + try { + const subject: string = `[Packrat:${SHARED.getMessagePrefixByType(notifyType)}] ${params.message}`; + return await sendMessageRaw(notifyType,params.sendTo,subject,textBody,htmlBody); + } catch (error) { + return { success: false, message: 'failed to send message', data: { error: UTIL.getErrorString(error) } }; + } +}; +//#endregion \ No newline at end of file diff --git a/server/records/notify/notifyShared.ts b/server/records/notify/notifyShared.ts new file mode 100644 index 00000000..2a51a6dc --- /dev/null +++ b/server/records/notify/notifyShared.ts @@ -0,0 +1,68 @@ +export enum NotifyType { + UNDEFINED = -1, + SYSTEM_ERROR = 1, + SYSTEM_NOTICE = 2, + JOB_FAILED = 10, + JOB_PASSED = 11, + JOB_STARTED = 12, + SECURITY_NOTICE = 20, +} +export enum NotifyChannel { + SLACK_OPS = 'slack-ops', + SLACK_DEV = 'slack-dev', + EMAIL_ADMIN = 'email-admin', + EMAIL_USER = 'email-user', + EMAIL_ALL = 'email-all' +} +export interface NotifyResult { + success: boolean, + message: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any +} +export interface NotifyPackage { + message: string, // main message for the user + startDate: Date, // when did this job/event happen or start + endDate?: Date, // when did this job/event end (optional) + detailsLink?: { url: string, label: string }, // is there somewhere the user can get more info + detailsMessage?: string, // error message, scene name, full message, etc. + sendTo?: string[], // where are we sending the message. for Slack it is a user slack ID. for email it is the 'to' email address +} + +export const getMessageIconUrlByType = (type: NotifyType): string => { + + switch(type) { + case NotifyType.SYSTEM_ERROR: + return 'https://egofarms.com/packrat/fire-solid.png'; + case NotifyType.SYSTEM_NOTICE: + return 'https://egofarms.com/packrat/alarm-on.png'; + + case NotifyType.JOB_FAILED: + return 'https://egofarms.com/packrat/attack.png'; + case NotifyType.JOB_PASSED: + return 'https://egofarms.com/packrat/award-ribbon.png'; + case NotifyType.JOB_STARTED: + return 'https://egofarms.com/packrat/coffee.png'; + + case NotifyType.SECURITY_NOTICE: + return 'https://egofarms.com/packrat/privacy-shield-solid.png'; + + default: + return 'https://egofarms.com/packrat/gear-solid.png'; + } +}; +export const getMessagePrefixByType = (type: NotifyType): string => { + + switch(type) { + case NotifyType.SYSTEM_ERROR: return 'Error'; + case NotifyType.SYSTEM_NOTICE: return 'Notice'; + + case NotifyType.JOB_FAILED: return 'Failed'; + case NotifyType.JOB_PASSED: return 'Success'; + case NotifyType.JOB_STARTED: return 'Start'; + + case NotifyType.SECURITY_NOTICE: return 'Notice'; + + default: return 'N/A'; + } +}; diff --git a/server/records/recordKeeper.ts b/server/records/recordKeeper.ts index 32e8c882..4d60c9ef 100644 --- a/server/records/recordKeeper.ts +++ b/server/records/recordKeeper.ts @@ -4,30 +4,58 @@ import { Config } from '../config'; import { ASL, LocalStore } from '../utils/localStore'; import * as H from '../utils/helpers'; import { Logger as LOG, LogSection } from './logger/log'; +import { sendEmailMessage, sendEmailMessageRaw, NotifyChannel, NotifyType, NotifyPackage } from './notify/notify'; /** TODO: * - change H.IOResults.error to message and make a requirement */ // utils -const convertToIOResults = ( result: { success: boolean, message: string }): H.IOResults => { +const convertToIOResults = ( result: { success: boolean, message: string, data?: any }): H.IOResults => { // converts standard response from logging and notification systems into Packrat's current // 'results' structure, which has 'error' instead of message. - return { success: result.success, error: result.message }; + let error: string | undefined = undefined; + if(result.success===false && result.data) { + if(result.data.error) + error = result.data.error; + else + error = JSON.stringify(result.data); + } + + const msg: string = result.message + ((error!=undefined) ? ` ${error}` : ''); + return { success: result.success, error: msg }; +}; + +type ChannelConfig = { + [NotifyChannel.SLACK_DEV]: string, + [NotifyChannel.SLACK_OPS]: string, + [NotifyChannel.EMAIL_ADMIN]: string[], + [NotifyChannel.EMAIL_ALL]: string[], }; export class RecordKeeper { static LogSection = LogSection; + static NotifyChannel = NotifyChannel; + static NotifyType = NotifyType; - static configure(): H.IOResults { + private static defaultEmail: string[] = ['maslowskiec@si.edu']; + private static notifyChannelConfig: ChannelConfig = { + [NotifyChannel.EMAIL_ADMIN]: [], + [NotifyChannel.EMAIL_ALL]: [], + [NotifyChannel.SLACK_DEV]: 'C07MKBKGNTZ', // packrat-dev + [NotifyChannel.SLACK_OPS]: 'C07NCJE9FJM', // packrat-ops + }; + static async configure(): Promise { + + //#region CONFIG:LOGGER // get our log path from the config const logPath: string = Config.log.root ? Config.log.root : /* istanbul ignore next */ './var/logs'; // our default Logger configuration options // base it on the environment variable for our base/target rate - const useRateManager: boolean = true; // do we manage our log output for greater consistency + const useRateManager: boolean = true; // do we manage our log output for greater consistency const targetRate: number = Config.log.targetRate; // targeted logs per second (100-500) const burstRate: number = targetRate * 5; // target rate when in burst mode and playing catchup const burstThreshold: number = burstRate * 5; // when queue is bigger than this size, trigger 'burst mode' @@ -38,16 +66,20 @@ export class RecordKeeper { if(logResults.success===false) return convertToIOResults(logResults); this.logInfo(LogSection.eSYS, logResults.message,{ path: logPath, useRateManager, targetRate, burstRate, burstThreshold, staggerLogs }); + //#endregion - // initialize notify sub-system - // ... + // region CONFIG:NOTIFY + // get our email addresses from the system. these can be cached because they will be + // the same for all users and sessions. + this.notifyChannelConfig[NotifyChannel.EMAIL_ADMIN] = await this.getEmailsFromChannel(NotifyChannel.EMAIL_ADMIN) ?? this.defaultEmail; + this.notifyChannelConfig[NotifyChannel.EMAIL_ALL] = await this.getEmailsFromChannel(NotifyChannel.EMAIL_ALL) ?? this.defaultEmail; + //#endregion return { success: true }; } static cleanup(): H.IOResults { return { success: true }; } - private static getContext(): { idUser: number, idRequest: number } { // get our user and request ids from the local store // TEST: does it maintain store context since static and not async @@ -102,4 +134,75 @@ export class RecordKeeper { return convertToIOResults(result); } //#endregion + + //#region NOTIFY + // emails + private static async getEmailsFromChannel(channel: NotifyChannel, forceUpdate: boolean = false): Promise { + + switch(channel) { + + case NotifyChannel.EMAIL_ALL: { + // see if we already initialized this + if(this.notifyChannelConfig[NotifyChannel.EMAIL_ALL].length>0 && forceUpdate===true) + return this.notifyChannelConfig[NotifyChannel.EMAIL_ALL]; + + // otherwise, grab all active Users from DB and their emails + return ['eric@egofarms.com']; + } + + case NotifyChannel.EMAIL_ADMIN: { + // see if we already initialized this + if(this.notifyChannelConfig[NotifyChannel.EMAIL_ADMIN].length>0 && forceUpdate===true) + return this.notifyChannelConfig[NotifyChannel.EMAIL_ADMIN]; + + // get ids from Config and then get their emails + return ['maslowskiec@si.edu','ericmaslowski@gmail.com']; + } + + case NotifyChannel.EMAIL_USER: { + // const { idUser } = this.getContext(); + // TODO: from current user id + return ['ericmaslowski@gmail.com']; + } + } + + return undefined; + } + static async sendEmail(type: NotifyType, channel: NotifyChannel, subject: string, body: string, startDate?: Date): Promise { + + // build our package + const params: NotifyPackage = { + message: subject, + detailsMessage: body, + startDate: startDate ?? new Date(), + sendTo: await this.getEmailsFromChannel(channel) + }; + + // send our message out + this.logInfo(LogSection.eSYS,'sending email',{ sendTo: params.sendTo },'RecordKeeper.sendEmail',true); + const emailResult = await sendEmailMessage(type,params); + this.logInfo(LogSection.eSYS,emailResult.message,emailResult.data,'RecordKeeper.sendEmail',true); + + // convert and return the results + return convertToIOResults(emailResult); + } + static async sendEmailRaw(type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise { + const emailResult = await sendEmailMessageRaw(type, sendTo, subject, textBody, htmlBody); + return convertToIOResults(emailResult); + } + static async emailTest(): Promise { + const result = await this.sendEmail(NotifyType.JOB_FAILED, NotifyChannel.EMAIL_ADMIN, 'test message', 'test body for email...'); + + if(result.success===true) + this.logInfo(LogSection.eTEST,result.error ?? 'NA', undefined, 'RecordKeeper.emailTest',false); + else + this.logError(LogSection.eTEST,result.error ?? 'Unknown error', undefined, 'RecordKeeper.emailTest'); + + return result; + } + + // slack + // sendSlackMessage(...) + // sendSlackMessageRaw(...) + //#endregion } diff --git a/server/records/utils/rateManager.ts b/server/records/utils/rateManager.ts index aab05cdb..78cb89dd 100644 --- a/server/records/utils/rateManager.ts +++ b/server/records/utils/rateManager.ts @@ -92,7 +92,7 @@ export class RateManager { if(ellapsedTimedelay) { - (this.config.onMessage) && this.config.onMessage(true,'log took longer than delay',{ ellapsedTime, delay }); + // (this.config.onMessage) && this.config.onMessage(true,'log took longer than delay',{ ellapsedTime, delay }); continue; // already took too long. just keep moving } else this.delay(waitTime); diff --git a/server/records/utils/utils.ts b/server/records/utils/utils.ts new file mode 100644 index 00000000..9de49ba9 --- /dev/null +++ b/server/records/utils/utils.ts @@ -0,0 +1,73 @@ +export const getDurationString = (startDate: Date, endDate: Date): string => { + // Calculate the difference in milliseconds + const diffMs = Math.abs(endDate.getTime() - startDate.getTime()); + + // Convert milliseconds to total seconds + const diffSeconds = Math.floor(diffMs / 1000); + + // Calculate hours, minutes, and remaining seconds + const hours = Math.floor(diffSeconds / 3600); + const minutes = Math.floor((diffSeconds % 3600) / 60); + const seconds = diffSeconds % 60; + + // Build the result string + let result = ''; + if (hours > 0) { + result += `${hours}h `; + } + if (minutes > 0) { + result += `${minutes}min `; + } + if (hours === 0 && minutes === 0) { + result += `${seconds}s`; + } + + return result.trim(); +}; +export const getFormattedDate = (date: Date): string => { + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Months are 0-indexed + const day = date.getDate().toString().padStart(2, '0'); + + let hours = date.getHours(); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const ampm = hours >= 12 ? 'pm' : 'am'; + + hours = hours % 12; + hours = hours ? hours : 12; // The hour '0' should be '12' + + return `${year}-${month}-${day} @ ${hours}:${minutes}${ampm}`; +}; +export const getRandomWhitespace = (): string => { + return ' '.repeat(Math.floor(Math.random() * 30)); +}; +export const toCamelCase = (str: string): string => { + return str + .toLowerCase() // Convert the entire string to lowercase + .split(/[\s-_]+/) // Split by space, dash, or underscore + .map((word, index) => { + if (index === 0) { + return word; // Keep the first word lowercase + } + return word.charAt(0).toUpperCase() + word.slice(1); // Capitalize the first letter of each subsequent word + }) + .join(''); // Join the words back into a single string +}; +export const toTitleCase = (str: string): string => { + return str + .toLowerCase() // Convert the entire string to lowercase first + .split(/[\s-_]+/) // Split by spaces, dashes, or underscores + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) // Capitalize the first letter of each word + .join(' '); // Join the words with a space +}; +export const truncateString = (str: string, maxLength = 32): string => { + if (str.length > maxLength) { + return str.slice(0, maxLength - 3) + '...'; + } + return str; +}; + +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types */ +export const getErrorString = (error: any): string => { + return error instanceof Error ? error.message : String(error); +}; \ No newline at end of file From a05813ddd2e9783fa7a47e67b0b9c061907789a3 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Mon, 7 Oct 2024 10:42:15 -0400 Subject: [PATCH 02/12] Email placed in class (new) place Notify email functionality in class and cleanup exports --- server/records/notify/notify.ts | 9 +- server/records/notify/notifyEmail.ts | 520 +++++++++++++-------------- 2 files changed, 268 insertions(+), 261 deletions(-) diff --git a/server/records/notify/notify.ts b/server/records/notify/notify.ts index c32ea9b1..3f80c7b2 100644 --- a/server/records/notify/notify.ts +++ b/server/records/notify/notify.ts @@ -1,2 +1,9 @@ export { NotifyChannel, NotifyType, NotifyPackage } from './notifyShared'; -export { sendMessage as sendEmailMessage, sendMessageRaw as sendEmailMessageRaw } from './notifyEmail'; \ No newline at end of file + +// Email +import { NotifyEmail } from './notifyEmail'; + +export const sendEmailMessage = NotifyEmail.sendMessage; +export const sendEmailMessageRaw = NotifyEmail.sendMessageRaw; + +// slack diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index db5c65c7..b3133555 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -2,282 +2,282 @@ import * as NET from 'net'; import * as SHARED from './notifyShared'; import * as UTIL from '../utils/utils'; -//#region UTILS -const storeServerResponse = (response: string): { statusCode: number, message: string}[] => { - - // see if we need to split it - const pieces: string[] = response.split('\n'); - - // split it up as needed - const result: { statusCode: number, message: string}[] = pieces.map(line => { - const firstSpaceIndex = line.indexOf(' '); - - // Extract status code (everything before the first space) - const statusCode = line.substring(0, firstSpaceIndex); - - // Extract message (everything after the first space) - const message = line.substring(firstSpaceIndex + 1).trim(); - - return { - statusCode: parseInt(statusCode), - message - }; - }).filter(item => !isNaN(item.statusCode) && item.message.length > 0); // Filter out invalid entries - return result; -}; -const verifyServerResponses = (responses): boolean => { - // first should always be 220 saying we connected to the server - if(responses[0].statusCode!=220) - return false; - - // next we check to see if the next one is a 250, accepting the command - if(responses[1].statusCode!=250) - return false; - - // finally, see if the connect closed with code 221 - if(responses[responses.length-1].statusCode!=221) - return false; - - return true; -}; -const extractErrorFromResponse = (responses): string => { - const errorMessages = responses - .filter(item => item.statusCode >= 400 && item.statusCode < 600) - .map(item => item.message); - - return errorMessages.join(' | '); -}; - - -//#endregion - -//#region FORMATING -export const getMessageIconBase64 = (type: SHARED.NotifyType): string => { - - // pre-converted base64 strings for each icon type - switch(type) { - case SHARED.NotifyType.SYSTEM_ERROR: - return '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'; - case SHARED.NotifyType.SYSTEM_NOTICE: - return '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'; - - case SHARED.NotifyType.JOB_FAILED: - return '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'; - case SHARED.NotifyType.JOB_PASSED: - return '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'; - case SHARED.NotifyType.JOB_STARTED: - return 'iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAAFBxJREFUeF7t3QXQPM1RBvAObgGCu3uwAMHd3d3dCa4BAiS4u1twd3d3t+Du7i73q9qpNFN7d7v33urNVP3re7+72b2dnmd7erqf7rlHtNYkMIEE7jHBPdstmwSiAauBYBIJNGBNItZ20washoFJJNCANYlY200bsBoGJpFAA9YkYm03bcBqGJhEAg1Yk4i13bQBq2FgEgk0YE0i1nbTBqyGgUkk0IA1iVjbTRuwGgYmkUAD1iRibTdtwGoYmEQCDViTiLXdtAGrYWASCTRgTSLWdtMGrIaBSSTQgDWJWNtNG7AaBiaRQAPWJGJtN23AahiYRAINWJOItd20AathYBIJ3CqwHiMinjwini4ifikifn0S6d7wTW8RWI8aEe8cEfeLiMePiO/r/v7lG8bB1Yd+a8B6uIh4+4j4xEqSrxkRX3N16d7wDW8NWG8TEZ9RzfefRcTzRsTv3zAOrj70WwIWm+pzI+IlkhT/PCJePyK+94xkHyYi/ufq0t/xDW8JWO8XER+a5pKmep+I+MIT8/vwEfF8EfFOEfFNEfFdEfGnO8bD1YZ2K8B6ms6GetYkua+KiNc+IUmyeZPD7vHzUx///+CrSX/HN7oVYL1xpZksgS8QEb99Ym6fLSK+ICKePfVh9L/LjvFwtaFtHVhsn+fopPGHEQEwdXuUiPiiiHj19MWXH3xXr3dCin3X/EVEvHJE/MTVpL/jG20dWDTRu0UE7fIVEfG+h+Xrd6v5eoqD1vn6rk/5Chh/7oy2+raIeMLU5wMi4kE7xsJVh7ZlYL1sRLxnRLx4ksjzRMRPVhK6z0HT/Gz6jFYDrD85IclPjoh3TN9/96H/W/eA9qqTsaebbRlYvOefUAHmRXrCMwx02qw0nnZL2j8dmUje+I+vlkr+r8/a08RPPZatAsvy9tERwWNe2tce4n6v0SOwN4iIL06f/2BEvFRE/McR4T5LtxN8zu57Gu65I+IPpp6MPd1/q8DiPvjqZDdZ1mihn+mZnBft4oHlq1/pgPIvRybSbvGH03e/1v3Of+5p4qcey1aBRYPk3Zkd2wtGxG/2COy5IuIbDkvZE3Xf/eLBDnv5iPjjI8K1nH5/+u6HDp75F556IvZ2/60Ci3YCltJMPs3UF3Z5qoj41Ihg7Jf2qtX1eV6fPyK+MiKeuPvwVztj/9/3NvlTjmerwHqdiOCLKs1yxZHZZzfxSTHG7epK+7rOOH/Eg4sCYP43Ih4hIiyPz3wISn97cjWwsew2W5B6BBK3CqwX63Z6j9uN1VL4ahHxjxFxz4h4koh4tG7544t6pk6jFdHo/5AOUH/VfQhcDxsRjxQRls9yb19/8OGzb+6A98/djtJvHdsAjJiCfXbdGrAeMyIe+6CtnqFjKnANlCaoDBzZqXntWROABsq/PoR2/q5jn/5WB1KfASnA3XxbO7AeubN1GN6ve9BSz9iB6glWOHM/320eMFHZZZyyf3SrWm2NwLIcMbhfpXMhPO2BnLdGIJ3D9i90nnosih+7Na/9moCFi26Je6uO0Sn+d0mzVP10RFii+J44NrkW/ruzve5fAfV7OqqyHaWl9W8i4sk6G8uz5OXW89BGjxMRjzfi4YBMvFK4Canw30Zcu8muawGWeB4u+isMtJF4zwGIsxPHiuGe21t2Nlg9KTjvPPaZ+sJuElz+tJ4ZFNSWdJE1JmB+S0e7sVQjAlqiH2uAZvVbCIPcH/xpu21LA4vt9OYR8Q5nJgWQTAQGJzDRKn/bzcq9u4ni2CztOw8T/UkdAOrJs2MU4sn9JVIg8dnx5Waz8DlVqIjH3/MCdmm0Fy133y4o7plce2wjwR77lG6n2Uf12TzglgTW0x98Rx9W8aSyQHm/AUkM0CQC07HGz8RhmpctSRNvd+SCN+w0V9FEDG7Bav6wuuFt8YPle9OQlrZjDaBoMI5cscp7Jc9/vgZoPzwifmfzSKoGsBSwxPo+u/ItlUczYXjobJ8xW3caCje9NFqOjfQbPZMmsYJfijO0tBeqYoTlc+DjjM0a7gER8SEDwUBzARen7lP3AIy2fPe9OWCXANajH5yYH3vYirODcqM1aBlv8SXhE07Nn0o3tGQClh1ZX0OfEQYq7c06KnJfX/ZX1n4oNO/d+bIG4itsTt7isKFgo9WGPzvOi7Ybo34JYKG62ILnhk1gebpL2IQ2+NFq0qR2ARgjmw30l53dY9z4XILZpQE1LYkpYYf4pN0OkN0FVO+V+iL+AcklVBraWhpaDmwz6nG+GPa7aEsAi82Ud3G27y99gm1wTNB2eLb9bBk2DFvIDq1w4F3HMOZmKMyGMZPGruPgxKsHTDZhaWjN79953nncj1Fwjv0ePx2bMC/FwkYfNOYB19x3bmABga02L3ppNAf7aEgDJCDBdVfY4ym7OOCUYZxTz2Wnyl+GB6a4iP+eojzne31MZ1uVz2hxy3G9Mx0il9X1mRtYvOgoKTmliu/qW09IRtKo63jiaTaaYykgnZtA4OKOsKQB3KnNh/HknSVGBQ7/LoqTzA0sS8CPVD4rTsiPODJjHI92U+yPLYV1xAktl5Y7L43luG5yFBntpfFtoVbvwvUwN7DQVxjTuWEl4Eplw1W80Paf8HP28jmNsbbvgcvSz64sDl3PWGsrnzHg0YF2UatrbmDVqVgFCIxs6VZ8OqgvbLAvWxtK7vA8b9u5MsQu7YrVkeiLhUrkaEvhBYKWjXyqDpUtP9fDy3VOxQt+YpWXlHikHMi+TKLy0OcSaVc5uL6HmltjDdFEbI1s3G9GmGce1JJ/zk60RH7jHgY8N7Dsej4qCY7a50I4J/A9yLoeA6D9a+cyKd/tprLg3MCqa1RJarAjBDYe6ZIZs0cg5TExB4yZL8smpTT1uoSl8PXZW+QhyQOfjH0m5GPXaAPEgZs3BKuS2dzAErzN3mV+HF54zAEhHT4tlYz3CjDxSbFQISHA4PPKNteQ5RKA7DYF10Ut/P0Dh+Io/7AmZM0NLGGLD0wCoLFyeSFLIvqKbbe6oHtYItmMPPIcoPIfM/+qDm9dgg0bA3YZsKqQswrP/dzAqlkCNbCKYMXmUFtesWNqCt1Ix7ok5nfJZN31GkRD8UMaCU1acL2PuSCMk+tP3PV3sS4wR/qoQne996jr1wqsPAhJpUI4lkuFadFj2GOM/rWEdgp37Pc6/xutRJOcyztka2WNbdyWNVrOPbAxAFKMVWjLC6dYiWD7seC6ZxF7XXR3OTew0HHRektjb+BMjWkETND+IdExfmU789CXvEN8J45W/+6q5QCkTDJGxd93gWa1I2gG+YWMaCyH/xozkAOV5/O6wDNSIlnYJYs3nroPtgUTAeXnXTubtDYZPK8wWC5DMPLR7tZ9i8A6NmJhIGQ6IPNmy2im7eyqcLF8Dxi0nHHnsQMgAOFh0QT+4VrROEiHtAbXgOWNDdMX+7tkJvj1LJMANYYtW34LyOwevbCKouT2JR3tuQ6hXfKco6+ZG1h1pbxLNNboQW70AjXAuBi8JIrEAbedX1/hE9rbpui1Kg3NhrUi0KaztrmBVfPSjxnvswphRT+GHkQDKQwnx5LGpGG5ISy3qjxbevH1Bauz5qSZVXmui/Zy49Bes7a5gYV1+cA0QsbrNXdFswrvij/GRuLPkluJAXKuMe7Rm7kw8L5KY2Oy2zJDVyKIAsCzHnwwN7B4lqU7lWa7jZc+1ug9J/gtfc8h7MSMS14wtVVdy+AvDd1Z2lw26HHaECxna3MDS0LCR6bR8USrrreb7JSRM9eXWFHf4lw8FWDe45Dcoc59fmEzUGUAoe7Mdh7Q3MCikjnwSlPHQHW9S3ZEI+dwdd1x9m1eak3FdwUs6ENcGrQ5QEibU5VQ2ly9XNYlBSTw/ngaMQ2GT3+sPObVhTM3sOxaskoWhqCmT2U5X33QK7khh68XK4NE7O+Nqrr09eOKQOCtZccqUqSk12JHKSPA45+zt1GRFCeZpc0NrPpNkuUiQWKX9QvOzKBdn81LAZbsHjs6ztJzTS4AUBY7Cog4RMtBCXx431HlLvKZ5Xr3537jTt/PDSx0XDua0vytvkG2D+40oA1dLCE2J7yO4bwDjoBzzuSu3Qo1c+JUpvfVxTY3sKRuMdhLjI8w1UToK6N99cGu7IYoQvxR+SXjJhBvHNJotszlkpnN1VBaHT6b9SyguYHlLZWNkxMJXiYisAFurTG47dZKIwNaZWjCq/4cqaU54Uwib2lKNSlGUppsoXw+0KTynhtYCtNa5/PxuZbC3dQsGDFbgvG0Smnk4rOh4ZeacqP6DSJlaQz6nHEu5JOd0yMedXzXuYHVZ1SOSbEfP8L1XvGlVfjFTo+fb4jrheGPXpOr1rxkV9SkjFi4jCuntHqpnFQycwPLYD6zKub/cVUNg0kHvKKb1xqHNsns2mOPitEgdyBrHzRnhnwpHMfksBTmijZs2SE7zquIaAlg1QkVs679V5Ha3W8CHDROprq86ZmDz/2qEgXsprrom9wBRedUx9E4Uu0aS7NJ4taZLRl2CWApLKv0Yml44Dju1+I43X3ap7+DYLEaFtk5KplVhg6PPHqMTB10GX39bZPDHiWr3GgreQKAWlq9zLLfGO7lFI7JR7gEsGT7MtYLs9NbRE3fkvcdtViRuNyU6kYkpM28ZOYGAVE4R2C5LgvuWuEf/it05PJioi7TVvnIFrUx8g50l8BSeU8cLEff2Qf5jZt84Av/QH0m4iWP44UU0KedcnCZdqLBSsPlYtjPyiRdQmNxOYgX5rOcz1UhvkTwa76mPvV1zLMCFPOBO8F/c+srwynljqE/q6mxBLD8pjSlXNzW2c4SA26lYXqqKz+0iVZYIsX/kPtEKuozrdV9UN48222C/Hxjs6eDLQEswqwJf5ZGRuktEP4kdViqLF/+BhLcdtlHwFLmBE0ZO7RkGzG82VR141rAGsl0JH3sBDlIZ3Mx5AdbClhSlzjwcmoWAZXt8tA3+Zb7yThyGqzjWnLMsMhEpURgW+Qs66WAJctZzluOGe6mhM/EaEdltqsUV8yHGpSfFWvkG5S4Ui+XEz/aQ2+/FLBklCD5Z9K/uFk+WWI2Icz8Q2TOFcBfJQ+Si8GyyFeFJepzTWjH0idPkpkgHYz95Ppjh58j8iH88ZEtSvdeClgE18d/ZysMDcLOjIer/By6kKWLB734q+QEApa5YBoU/9WYY+u4FFBw1Gydzbt+SiJLAovTD/Uj1194pYqjdJXZXNFNeL8l7V6rMdCFc2h/4Jp993dsIEsCi9pXuCLbCXuPG/Lf0cp3aeWcRssdBi5XxOqiFksCi3AddJRrvMuPkw62x92hVC9hnBxqwfk3B/5xPbCpuBdK87fPec39wy5lnwr/rFpGSwMLVdm5hDm8I4F1T6W4C0hqb7sEEnwpafNS6dlZCpCwvcxLMezVbuDfU+VmM21pYPHFWB7y7lDmilNXV1X68I4ziqEg2pCXQdX8TpXmvuNPLnv50sAyeiUhgSk7S73Ji9V2mmBK6vr2jG5H1e1pjP9PbGsAlu02VmnOCLbTkWO3qC/mSgDjh8LmzFpZ1EFi6irqhV5pnKsDlgcCIqfL52rJe0myqM/N4Rm3JNZ8rCnmd7F7rkFjGTzjHa8osyNpLYa8SnpbbbzpD+6xIRHvVuciuKaQ1wIsY7JrQlzLWotxy8jdauvjRx071HyrY+x97jUBi9biZshp49LEFSPro4usfSJQiXnE83i8JHhoqz1R4lpCXROwjAlPSaJF3iEOyV65ljyueR/sg5zybifIgFeaaPdtbcAS2ceuzImWIvYC1ltKw1caHNszO365VOwEt2wzDn4h1gYsD66OkyyTPCmLVf8dLMmHdsRK+PSqfhUvu41JSSi94LbbumSNwCLBOqnVZ/fv4oqzlTu8YCpFEjA364RSx+k56etm2lqBRVthOtTHgaw9jtiXfcNtIkS1e4M9vzVrBZZnVO9c7cycqIlzhGVqt7i2ZuOBaJeXcGwNztBdHCA+RuBrBpZxmBSTlcmAwCVVTPB6Dcui5e9+PVkyntMm5CZ2gTXo1g4sz6eYmHy5+rAlTEw142ctjF8JkDPXMlfbVJ5JiWzRhJtsaweWSVFTSyERS2ANLuQ3J7Yq9DpniEQ2N3tKaObeFXKASmkm/9agURcB9haARTBO8VK2G9u074xCZEHOSLXNMSun8BVhKch9dOiRMkE1oDwnqrAsGc9zs6AiiK0Aq7x1EjRVvmPY9zUTi5GpLPVDOjov5qVUKmlWmfZ77E0u7M17dqlYaD1Azb8mp+/YccKyYyyLShHdfNsasEwY7jgPNs1x7oRVy5I4o2XSdp8WYVT7b0ldL5zzcsCk/97rUCtVLXX9zv0GMIsBYjE4e7C1DWqsMml2YrSWovn4TkscSg6g6MZcH/mQpAasDQOrTJ4MFsUzgOu+3cHkU4Ks5O45A4c9d4v16Qe9OFtcCo8NDKnOrpFhLZh9n27ZlG5l2Tu3pNX3tcTJypbUYUOgaovPMEDZa62dkMCegFUPU/4dgDHCyznRipH4nB1VzoYu50ErTKYyCzvJrhIfndHvRNNbKK901Rdlz8C6qqDazcZJoAFrnLxa74ESaMAaKKjWbZwEGrDGyav1HiiBBqyBgmrdxkmgAWucvFrvgRJowBooqNZtnAQasMbJq/UeKIEGrIGCat3GSaABa5y8Wu+BEmjAGiio1m2cBBqwxsmr9R4ogQasgYJq3cZJoAFrnLxa74ESaMAaKKjWbZwEGrDGyav1HiiBBqyBgmrdxkmgAWucvFrvgRJowBooqNZtnAQasMbJq/UeKIEGrIGCat3GSaABa5y8Wu+BEmjAGiio1m2cBBqwxsmr9R4ogQasgYJq3cZJoAFrnLxa74ESaMAaKKjWbZwE/g8BwXjEyuIx8AAAAABJRU5ErkJggg=='; - - case SHARED.NotifyType.SECURITY_NOTICE: - return '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'; - - default: - return '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'; +export class NotifyEmail { + //#region UTILS + private static storeServerResponse(response: string): { statusCode: number, message: string}[] { + + // see if we need to split it + const pieces: string[] = response.split('\n'); + + // split it up as needed + const result: { statusCode: number, message: string}[] = pieces.map(line => { + const firstSpaceIndex = line.indexOf(' '); + + // Extract status code (everything before the first space) + const statusCode = line.substring(0, firstSpaceIndex); + + // Extract message (everything after the first space) + const message = line.substring(firstSpaceIndex + 1).trim(); + + return { + statusCode: parseInt(statusCode), + message + }; + }).filter(item => !isNaN(item.statusCode) && item.message.length > 0); // Filter out invalid entries + return result; } -}; -const formatTextBody = (type: SHARED.NotifyType, params: SHARED.NotifyPackage): string => { - const msgPrefix: string = SHARED.getMessagePrefixByType(type); - const startDate: Date = params.startDate; - const duration: string | undefined = (params.endDate) ? UTIL.getDurationString(params.startDate,params.endDate) : undefined; - - let result: string = `${msgPrefix.toUpperCase()}\n${params.message}\n`; - result += (params.detailsMessage) ? '\n'+params.detailsMessage : ''; - result += `\n\nStarted: ${UTIL.getFormattedDate(startDate)}`; - result += (duration) ? '\nDuration: '+duration : ''; - result += '\nWho: '+(params.sendTo?.join(', ') ?? 'NA'); - result += (params.detailsLink) ? `\n${UTIL.toTitleCase(params.detailsLink.label)}: ${params.detailsLink.url}` : ''; - - return result; -}; -const formatHtmlBody = (type: SHARED.NotifyType, params: SHARED.NotifyPackage): string => { - - const msgPrefix: string = SHARED.getMessagePrefixByType(type); - const startDate: Date = params.startDate; - const duration: string | undefined = (params.endDate) ? UTIL.getDurationString(params.startDate,params.endDate) : undefined; - - let result: string = ''; - result += ''; - - // head - result += ''; - result += ''; - result += ''; - result += `[${msgPrefix}] ${params.message}`; - result += ''; - - // body - result += ''; - result += '
'; - - // banner - result += '
'; - result += ''; // image references specific attachment by CID - result += '
'; - - // header and subtitle - result += `

[${msgPrefix}]

`; - result += `

${params.message}

`; - - // details paragraph - if(params.detailsMessage) { - result += '

'; - result += params.detailsMessage; - result += '

'; + private static verifyServerResponses(responses): boolean { + // first should always be 220 saying we connected to the server + if(responses[0].statusCode!=220) + return false; + + // next we check to see if the next one is a 250, accepting the command + if(responses[1].statusCode!=250) + return false; + + // finally, see if the connect closed with code 221 + if(responses[responses.length-1].statusCode!=221) + return false; + + return true; } - result += '

'; - result += `Started: ${UTIL.getFormattedDate(startDate)}`; - result += '

'; - if(duration) { - result += '

'; - result += `Duration: ${duration}`; - result += '

'; + private static extractErrorFromResponse(responses): string { + const errorMessages = responses + .filter(item => item.statusCode >= 400 && item.statusCode < 600) + .map(item => item.message); + + return errorMessages.join(' | '); + } + //#endregion + + //#region FORMATING + private static getMessageIconBase64(type: SHARED.NotifyType): string { + + // pre-converted base64 strings for each icon type + switch(type) { + case SHARED.NotifyType.SYSTEM_ERROR: + return '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'; + case SHARED.NotifyType.SYSTEM_NOTICE: + return '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'; + + case SHARED.NotifyType.JOB_FAILED: + return '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'; + case SHARED.NotifyType.JOB_PASSED: + return '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'; + case SHARED.NotifyType.JOB_STARTED: + return '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'; + + case SHARED.NotifyType.SECURITY_NOTICE: + return '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'; + + default: + return 'iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAACxVJREFUeF7tnV/oZVMUx9e8U4R4mAeKUMooioaYPODJTJPwoGYa0/AgFKGmJMSE8ORlhEYhhCIUUYiSeEMRDx4USvGouN+5Z/c7c917z/6z1tnfs3/r1C9p9t5377U+e5111l577y3ij0vAQAJbDNr0Jl0C4mA5BCYScLBMxOqNOljOgIkEHCwTsXqjDpYzYCIBB8tErN6og+UMmEjAwTIRqzfqYDkDJhJwsEzE6o06WM6AiQQcLBOxeqMOljNgIgEHy0Ss3qiD5QyYSMDBMhGrN+pgOQMmEnCwTMTqjTpYzoCJBBys/4v19AJJ/1xQt6mqDtax6nxURK4XkeMytfypiLwiIi9n1m+mmoO1ocrnRGSPgma/EZFds7Y2tfVysDZI+k1ETlYAC00cEpF7ldqaZDMO1lxtV4jIR4oa/FhEdii2N7mmHKy5yu4SkccUtQdf6zLF9ibXlIM1Vxmc9nsUtffdzIk/V7G9yTXlYM1VdlhE9ilq71cRuWQzO/AO1pwmrS/CPpvwseBrbcrHwZqr/RMRuVSZgBs3czxrimAhMo4/TWvw7QyCc5TBel5E9iq1GVYDJhMbmxpYcLIPiMgJnf/ypojcqaA8zRhW6I5WLKvv/+Gj4D2lMSuIbXUTUwLrDRHZuWQoz4rIzQVSQrQdPpb2A+gRgc99YKUw5m0LDeDDAEtGGhMqt2+D9aYC1iqowgChxCdEBPGj2AeKOzjzr67sXq2x9WLL4bX1oYjAyjweW6krd4OIPCkip62oRw/XFMAagirIHgrcPwAXYLpFRLYbOOvr2MH64fszi4v/Di1Qx8bUqOFiBysWqqDUX0TktSWvCQAFC4ClG/hnNR9Y1R+WOPbo45FE4GnhYgYLa3cAIecJfhfq31/QTs5vx9aBhX2rW6xGqANQ5eSCUcLFCtbXS5zWWIX1X43aIYTUPsSUB2Cl/aSDixEsDahiFNpaGSq42MByqMpwp4GLCSyHqgyqUJsCLhawShx1HXW01QrgQgB1KLRhNmoGsPBFhEVgf3QlUDWLlQGs2ICgrtjbb61qsiEDWIjdfL5m+aJ9BGxGuOktFsTqPpYuXPCxkA+mmVqU1EMGi4UOw2p9qbj9KkkIDRbWzAXLEg8LWOg8nHisDWrt7csSSK8SshOw9viPiPwoIr93fTupVwZ9xR/WH1dlIpT2I7V+abpO6u8tLc8EFjqIdBFsw9qqMrr0RrBA/FkHUUqqCywucsUu7yZIrcmBxe2z0oetX4MNLIwwNaOhVCrwR94RkReVfJIA2e7uFT/WJPmzSyys5lf1FcEIVkhxWZYtWgpRv34A6iGjbVpjAlbdWV9UDCNYYzjzSKuxAmpRxgEwpO9Y5YIhD/4azVlX2hYrWMGZf1XZKcbMfqHSgR2rcthLdVg1XrWq88xgaZ+ngEj0rUp+VC4MIc/+KsUPFAcrURuaTjxyzS9I/H3L4pg0OCtC6+vx7owNG5bjo73FXtNasUEVFDq0EydF8XRjZH0ValmrqguxEWRgM6rWa5HKajGChZn8UoRShorQfYKv6LDWJKKyWoxgaSxITwWqEFrBTuzcHUl9XmkOImEE698hUxTx7xTrZRH9DEW0Uodoxs0GlobTjqWNExOUylJUw1LT+JRsYGn4G1RObAK1sFrYUFIanad4HbKBVfoapJmxCUD1i97RHQWQWf1oNYrX4dhghUPTLuzt/j27k+J5CrMVO1OeKtFK5bqQD16JOVvt+13HBMMT/vv97DAUfNCErWHmwxwDLGyWOF9EABOS5qwS4pCId4q5xOx/QMMdGOolQhNIZDS7nsUarDGEFIRI8QoY0mjEvyPsgDie1QTsdwGT8TaL/YeWYGkFOiN0cbTIVJ32xfFpvQ5j5WayiG0JlkboIFY4mHkXGSXsxfZBs9yYlt4knbkVsFq7YmTMTbwO1hqTUH27k6a5Mrg0al33HKw10ik9OVmZi+LmtJZ4YjriYK2RktaZ6jGKGKvMTwrxrJi+OlhrpESxjBGjxYQyFtewLPt5B8vBSsAyvqiD5WDF05JQ0sFysBJwiS/qYG0y532sM1kdrE0GlsVVd+68x1vyoyWpNhIk9n1ZccSxEG4Y43GLtUbKU0/wWxzamOusDtbA9G0pljWWfwWRTg6sMZclIKCWwNLYWBH7Gp1c2gwGNmb6h4mAYrWjXK409z+2O0g3us7ioBTLtBkMLlw8iW3kyIi0zIqsfhtDrDYHyuFeRWyqsHrgjwIoPDdZ5bBZg9UXTtggcHEPslN7B8Ni21Pp9Wot5GVphBkghwDPHyLyV3c3IvQxylGSY4I1NAM1fLKpg6XxNUjhEjCBBfA0nFZs/6K+4X3NDNP4Gtw7c0GQ+Fj1YQNLY8MmgqXYeZJyo31VJXQ/joNB9hR25G8ROb6wDZXqbGBp3VBB8TpI0JCGG4Cfo3EF2MDSDFFM6ZWo4QLgMBQch4mNqNUfRrC0Zu9Uwg9aO3KorDQjWJhtWmm52B0NR55iFi8xI1pQmQU6c00fK1gaTnyQCStcmjvF6bI7WMHSPmyfDS5NqDCBdowV+Iy1YKxgof+aVit8Md1HEIbQev0FHVP5VqFTzGBpRKEXJxh8EdzyVSOACiusdYhtf1x0r0F0jhUs6xNXEO8Z03oFoEoPVFv1JqKzWqxgacR1htwB62vl8PvwpXDrV+ni+tBY8O9UcTtGsN4VkatjJKlUBq+Sr2Y3o36gdABZuIgJR2DiOuKxHrND1HIGwAaW9tdSqkzC8YlQUurVvcijQurPNoWzVFP7HcpjkuxiiNsxgaV5aVGuYvr1wsGwgAx/yGsKF46jHC4dx+1dGofyavSX6iuRCSyNBDdNBU25rernsbKANYazPmVQcvpedXMJA1i1/aocpU2hTtUQBANYFoHQKSjeuo/4EDnD+kdWtc8A1pjnmteSc43f3fQWC0J3H0sXPST9Iewwyo6cZV1nsFjol3Y2g66aptUaRW4WC1gOlw68FFBhKExgOVxlcNFAxQiWJlwQtOVtY2UYbNRGhB9LQSXHD1CtE7KCpQFX2AaFReBHZjlYZxYqTguifjsA6gsRwQZT+JhYfM/JggBUD7Pd08j2KuwLPsehh5Df7pTVbwuAPdit62F9r+YD6HEmFYBaHO/tiQeCUELFbLGCwFOyLhEQhLLWfWKjvZ0isntkKwbrhL+nI/p3UERwOs/WAfppoZoCWOG1iJQUALHqyQkGhiOWrs18BcVYPVgmWNDUVGgscz2zJv2GGqqpgBXgQibmsrMNSm/+0toguwhaaYYB+nVkSbIggp8PsPlUi4Nn9rEW+xoszIFuJsNXeV1BwFZg5VjRZWPGq3F7Z1VhAffXjKjHmOkpWaxFJxf/r7m72SIXrNSSWo85lpGsclOyWFkDjKxkkWffyh3VkSI8tpiDNZeHxSI4UlY0rWqWgmtVcrDmkj8sIvsUldDa5efJonGw5iLT3vZeNckumQKDCg7WXKja6dGU294N+FnZpIM1Fw2yWOFnaT2lMSytflRrx8Gai17r7NOgSI0YVjUoNH7YwdqQotaXIdVZoBqQ5LThYG1ILZy5gAVq5HHlPL+IyCGlMyByfp+mjoNFo4q2OuJgtaVPmtE4WDSqaKsjDlZb+qQZjYNFo4q2OuJgtaVPmtE4WDSqaKsjDlZb+qQZjYNFo4q2OuJgtaVPmtE4WDSqaKsjDlZb+qQZjYNFo4q2OuJgtaVPmtE4WDSqaKsjDlZb+qQZjYNFo4q2OuJgtaVPmtE4WDSqaKsjDlZb+qQZjYNFo4q2OuJgtaVPmtH8B6A+4KZf6e4PAAAAAElFTkSuQmCC'; + } + } + private static formatTextBody(type: SHARED.NotifyType, params: SHARED.NotifyPackage): string { + const msgPrefix: string = SHARED.getMessagePrefixByType(type); + const startDate: Date = params.startDate; + const duration: string | undefined = (params.endDate) ? UTIL.getDurationString(params.startDate,params.endDate) : undefined; + + let result: string = `${msgPrefix.toUpperCase()}\n${params.message}\n`; + result += (params.detailsMessage) ? '\n'+params.detailsMessage : ''; + result += `\n\nStarted: ${UTIL.getFormattedDate(startDate)}`; + result += (duration) ? '\nDuration: '+duration : ''; + result += '\nWho: '+(params.sendTo?.join(', ') ?? 'NA'); + result += (params.detailsLink) ? `\n${UTIL.toTitleCase(params.detailsLink.label)}: ${params.detailsLink.url}` : ''; + + return result; } - result += '

'; - result += `Who: ${params.sendTo?.join(', ') ?? 'NA'}`; - result += '

'; - - // more info button - if(params.detailsLink) { - result += '
'; - result += ``; - result += UTIL.toTitleCase(params.detailsLink.label); - result += ''; + private static formatHtmlBody(type: SHARED.NotifyType, params: SHARED.NotifyPackage): string { + + const msgPrefix: string = SHARED.getMessagePrefixByType(type); + const startDate: Date = params.startDate; + const duration: string | undefined = (params.endDate) ? UTIL.getDurationString(params.startDate,params.endDate) : undefined; + + let result: string = ''; + result += ''; + + // head + result += ''; + result += ''; + result += ''; + result += `[${msgPrefix}] ${params.message}`; + result += ''; + + // body + result += ''; + result += '
'; + + // banner + result += '
'; + result += ''; // image references specific attachment by CID result += '
'; + + // header and subtitle + result += `

[${msgPrefix}]

`; + result += `

${params.message}

`; + + // details paragraph + if(params.detailsMessage) { + result += '

'; + result += params.detailsMessage; + result += '

'; + } + result += '

'; + result += `Started: ${UTIL.getFormattedDate(startDate)}`; + result += '

'; + if(duration) { + result += '

'; + result += `Duration: ${duration}`; + result += '

'; + } + result += '

'; + result += `Who: ${params.sendTo?.join(', ') ?? 'NA'}`; + result += '

'; + + // more info button + if(params.detailsLink) { + result += ''; + } + + // close body + result += '
'; + result += ''; + + // close html + result += ''; + + return result; } + //#endregion + + //#region PUBLIC + public static async sendMessageRaw(type: SHARED.NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise { + + // get our email addresses if needed + if(sendTo.length<=0) + return { success: false, message: 'failed to send message', data: { error: 'no addresses provided' } }; + + // get our SMTP parameters from config + // NOTE: currently unencrypted and insecure. do not send anything sensitive! + const smtpHost: string = 'smtp.si.edu'; + const smtpPort: number = 25; + + const from: string = 'maslowskiec@si.edu'; + const boundary: string = '----=_Packrat_Ops_Msg_001'; + + return new Promise((resolve) => { + const serverResponses: { statusCode: number, message: string}[] = []; + const client = NET.createConnection(smtpPort,smtpHost, () => { + // SMTP dialog + client.write('HELO si.edu\r\n'); + client.write(`MAIL FROM:<${from}>\r\n`); + for(const recipient of sendTo) + client.write(`RCPT TO:<${recipient}>\r\n`); + client.write('DATA\r\n'); + + // MIME email body with plain text and HTML parts + client.write(`From: ${from}\r\n`); + client.write(`To: ${sendTo.join(', ')}\r\n`); + client.write(`Subject: ${UTIL.truncateString(subject,60)}\r\n`); + client.write('MIME-Version: 1.0\r\n'); + client.write(`Content-Type: multipart/alternative; boundary="${boundary}"\r\n`); + client.write('\r\n'); - // close body - result += '
'; - result += ''; - - // close html - result += ''; - - return result; -}; -//#endregion - -//#region PUBLIC -export const sendMessageRaw = async (type: SHARED.NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise => { - - // get our email addresses if needed - if(sendTo.length<=0) - return { success: false, message: 'failed to send message', data: { error: 'no addresses provided' } }; - - // get our SMTP parameters from config - // NOTE: currently unencrypted and insecure. do not send anything sensitive! - const smtpHost: string = 'smtp.si.edu'; - const smtpPort: number = 25; - - const from: string = 'maslowskiec@si.edu'; - const boundary: string = '----=_Packrat_Ops_Msg_001'; - - return new Promise((resolve) => { - const serverResponses: { statusCode: number, message: string}[] = []; - const client = NET.createConnection(smtpPort,smtpHost, () => { - // SMTP dialog - client.write('HELO si.edu\r\n'); - client.write(`MAIL FROM:<${from}>\r\n`); - for(const recipient of sendTo) - client.write(`RCPT TO:<${recipient}>\r\n`); - client.write('DATA\r\n'); - - // MIME email body with plain text and HTML parts - client.write(`From: ${from}\r\n`); - client.write(`To: ${sendTo.join(', ')}\r\n`); - client.write(`Subject: ${UTIL.truncateString(subject,60)}\r\n`); - client.write('MIME-Version: 1.0\r\n'); - client.write(`Content-Type: multipart/alternative; boundary="${boundary}"\r\n`); - client.write('\r\n'); - - // Plain-text part - client.write(`--${boundary}\r\n`); - client.write('Content-Type: text/plain; charset="UTF-8"\r\n'); - client.write('Content-Transfer-Encoding: 7bit\r\n'); - client.write('\r\n'); - client.write(`${textBody}\r\n`); - - // HTML part - if(htmlBody) { + // Plain-text part client.write(`--${boundary}\r\n`); - client.write('Content-Type: text/html; charset="UTF-8"\r\n'); + client.write('Content-Type: text/plain; charset="UTF-8"\r\n'); client.write('Content-Transfer-Encoding: 7bit\r\n'); client.write('\r\n'); - client.write(`${htmlBody}\r\n`); - } - - // attachments - // NOTE: we need to put all images as attachments and then reference by CID - // for compatability since GMail removes any base64 embedded images - client.write(`--${boundary}\r\n`); - client.write('Content-Type: image/png; name="header.png"\r\n'); - client.write('Content-Disposition: inline; filename="header.png"\r\n'); - client.write('Content-Transfer-Encoding: base64\r\n'); - client.write('Content-ID: <0123456789>\r\n'); - client.write('Content-Location: header.png\r\n'); - client.write('\r\n'); - - // add our base64 icon from the type - const base64Icon: string = getMessageIconBase64(type); - client.write(`${base64Icon}\r\n`); - - // End of message - client.write(`--${boundary}--\r\n`); - client.write('.\r\n'); - client.write('QUIT\r\n'); - }); + client.write(`${textBody}\r\n`); + + // HTML part + if(htmlBody) { + client.write(`--${boundary}\r\n`); + client.write('Content-Type: text/html; charset="UTF-8"\r\n'); + client.write('Content-Transfer-Encoding: 7bit\r\n'); + client.write('\r\n'); + client.write(`${htmlBody}\r\n`); + } + + // attachments + // NOTE: we need to put all images as attachments and then reference by CID + // for compatability since GMail removes any base64 embedded images + client.write(`--${boundary}\r\n`); + client.write('Content-Type: image/png; name="header.png"\r\n'); + client.write('Content-Disposition: inline; filename="header.png"\r\n'); + client.write('Content-Transfer-Encoding: base64\r\n'); + client.write('Content-ID: <0123456789>\r\n'); + client.write('Content-Location: header.png\r\n'); + client.write('\r\n'); - client.on('data', (data) => { - // get our data and make sure it's not an error - const response = data.toString(); - serverResponses.push(...storeServerResponse(response)); + // add our base64 icon from the type + const base64Icon: string = this.getMessageIconBase64(type); + client.write(`${base64Icon}\r\n`); - // see if we have an errors in the mix - const errors = extractErrorFromResponse(serverResponses); + // End of message + client.write(`--${boundary}--\r\n`); + client.write('.\r\n'); + client.write('QUIT\r\n'); + }); + + client.on('data', (data) => { + // get our data and make sure it's not an error + const response = data.toString(); + serverResponses.push(...this.storeServerResponse(response)); + + // see if we have an errors in the mix + const errors = this.extractErrorFromResponse(serverResponses); + + // Handle server responses + if (errors.length > 0) { + resolve({ + success: false, + message: 'failed to send email.', + data: { error: errors } + }); + } + }); + + client.on('end', () => { + // console.log('Connection closed.'); + // go through our responses and see if it makes sense + if(this.verifyServerResponses(serverResponses)===true) + resolve({ success: true, message: 'email sent' }); + else + resolve({ success: false, message: 'failed to send email', data: { error: this.extractErrorFromResponse(serverResponses) } }); + }); - // Handle server responses - if (errors.length > 0) { + client.on('error', (err) => { + // console.error('Error:', err); resolve({ success: false, message: 'failed to send email.', - data: { error: errors } + data: { error: UTIL.getErrorString(err) } }); - } - }); - - client.on('end', () => { - // console.log('Connection closed.'); - // go through our responses and see if it makes sense - if(verifyServerResponses(serverResponses)===true) - resolve({ success: true, message: 'email sent' }); - else - resolve({ success: false, message: 'failed to send email', data: { error: extractErrorFromResponse(serverResponses) } }); - }); - - client.on('error', (err) => { - // console.error('Error:', err); - resolve({ - success: false, - message: 'failed to send email.', - data: { error: UTIL.getErrorString(err) } }); }); - }); -}; -export const sendMessage = async (notifyType: SHARED.NotifyType, params: SHARED.NotifyPackage): Promise => { - - // if we have sendTo address(es) then we ignore the channel - if(!params.sendTo) - return { success: false, message: 'failed to send message', data: { error: 'no email address provided.' } }; - - // build our text and html bodies - const textBody: string = formatTextBody(notifyType, params); - const htmlBody: string = formatHtmlBody(notifyType, params); - - try { - const subject: string = `[Packrat:${SHARED.getMessagePrefixByType(notifyType)}] ${params.message}`; - return await sendMessageRaw(notifyType,params.sendTo,subject,textBody,htmlBody); - } catch (error) { - return { success: false, message: 'failed to send message', data: { error: UTIL.getErrorString(error) } }; } -}; -//#endregion \ No newline at end of file + public static async sendMessage(notifyType: SHARED.NotifyType, params: SHARED.NotifyPackage): Promise { + + // if we have sendTo address(es) then we ignore the channel + if(!params.sendTo) + return { success: false, message: 'failed to send message', data: { error: 'no email address provided.' } }; + + // build our text and html bodies + const textBody: string = this.formatTextBody(notifyType, params); + const htmlBody: string = this.formatHtmlBody(notifyType, params); + + try { + const subject: string = `[Packrat:${SHARED.getMessagePrefixByType(notifyType)}] ${params.message}`; + return await this.sendMessageRaw(notifyType,params.sendTo,subject,textBody,htmlBody); + } catch (error) { + return { success: false, message: 'failed to send message', data: { error: UTIL.getErrorString(error) } }; + } + } + //#endregion +} \ No newline at end of file From 05caf28eea5d3192240035c8b922600fa5c6a7f1 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Mon, 7 Oct 2024 15:27:51 -0400 Subject: [PATCH 03/12] Email rate management (new) email rate management (new) Notify class with configuration (new) return type for onPost in RateManager (new) added type to NotifyPackage --- server/records/logger/log.ts | 6 +- server/records/notify/notify.ts | 21 ++-- server/records/notify/notifyEmail.ts | 137 +++++++++++++++++++------- server/records/notify/notifyShared.ts | 1 + server/records/recordKeeper.ts | 12 ++- server/records/utils/rateManager.ts | 18 +++- 6 files changed, 145 insertions(+), 50 deletions(-) diff --git a/server/records/logger/log.ts b/server/records/logger/log.ts index fa0add11..c33752f0 100644 --- a/server/records/logger/log.ts +++ b/server/records/logger/log.ts @@ -80,7 +80,8 @@ interface ProfileRequest { } interface LoggerResult { success: boolean, - message: string + message: string, + data?: any } //#endregion @@ -475,9 +476,10 @@ export class Logger { await this.postLogToWinston(entry); } } - private static async postLogToWinston(entry: LogEntry): Promise { + private static async postLogToWinston(entry: LogEntry): Promise { await this.logger.log(entry); this.updateStats(entry); + return { success: true, message: 'posted message' }; } // wrappers for each level of log diff --git a/server/records/notify/notify.ts b/server/records/notify/notify.ts index 3f80c7b2..59557575 100644 --- a/server/records/notify/notify.ts +++ b/server/records/notify/notify.ts @@ -1,9 +1,18 @@ -export { NotifyChannel, NotifyType, NotifyPackage } from './notifyShared'; - -// Email +// import { NotifyResult } from './notifyShared'; +import { NotifyResult, NotifyPackage, NotifyChannel, NotifyType } from './notifyShared'; import { NotifyEmail } from './notifyEmail'; -export const sendEmailMessage = NotifyEmail.sendMessage; -export const sendEmailMessageRaw = NotifyEmail.sendMessageRaw; +export class Notify { + + // email wrappers + public static configureEmail(env: 'prod' | 'dev', targetRate?: number, burstRate?: number, burstThreshold?: number): NotifyResult { + return NotifyEmail.configure(env,targetRate,burstRate,burstThreshold); + } + public static sendEmailMessage = NotifyEmail.sendMessage; + public static sendEmailMessageRaw = NotifyEmail.sendMessageRaw; + + // slack +} -// slack +// export shared types so they can be accessed via Notify +export { NotifyPackage, NotifyChannel, NotifyType }; \ No newline at end of file diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index b3133555..2a973b67 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -1,8 +1,64 @@ import * as NET from 'net'; -import * as SHARED from './notifyShared'; +import { NotifyPackage, NotifyType, getMessagePrefixByType } from './notifyShared'; import * as UTIL from '../utils/utils'; +import { RateManager, RateManagerConfig, RateManagerResult } from '../utils/rateManager'; + +/** + * - get error/success messages out allowing caller to wait for results + * - test routines + */ + +// declaring this empty for branding/clarity since it is used +// for instances that are not related to the RateManager +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface EmailResult extends RateManagerResult {} + +interface EmailEntry { + type: NotifyType, + sendTo: string[], + subject: string, + textBody: string, + htmlBody?: string +} export class NotifyEmail { + private static rateManager: RateManager | null = null; + private static environment: 'prod' | 'dev' = 'dev'; + + public static isActive(): boolean { + // we're initialized if we have a logger running + return (NotifyEmail.rateManager!=null && NotifyEmail.rateManager.isActive()); + } + public static configure(env: 'prod' | 'dev', targetRate?: number, burstRate?: number, burstThreshold?: number): EmailResult { + // we allow for re-assigning configuration options even if already running + NotifyEmail.environment = env; + + // if we want a rate limiter then we build it + const rmConfig: RateManagerConfig = { + targetRate: targetRate ?? 1, + burstRate: burstRate ?? 5, + burstThreshold: burstThreshold ?? 25, + onPost: NotifyEmail.postMessage, + }; + + // if we already have a manager we re-configure it (causes restart). otherwise, we create a new one + if(NotifyEmail.rateManager) + NotifyEmail.rateManager.setConfig(rmConfig); + else { + NotifyEmail.rateManager = new RateManager(rmConfig); + } + + // if we already configured skip creating another one + if(NotifyEmail.isActive()===true) + return { success: true, message: 'email system already running' }; + + // start our rate manager if needed + if(NotifyEmail.rateManager) + NotifyEmail.rateManager.startRateManager(); + + return { success: true, message: `(${NotifyEmail.environment}) configured email notifier.` }; + } + //#region UTILS private static storeServerResponse(response: string): { statusCode: number, message: string}[] { @@ -51,31 +107,31 @@ export class NotifyEmail { //#endregion //#region FORMATING - private static getMessageIconBase64(type: SHARED.NotifyType): string { + private static getMessageIconBase64(type: NotifyType): string { // pre-converted base64 strings for each icon type switch(type) { - case SHARED.NotifyType.SYSTEM_ERROR: + case NotifyType.SYSTEM_ERROR: return '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'; - case SHARED.NotifyType.SYSTEM_NOTICE: + case NotifyType.SYSTEM_NOTICE: return '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'; - case SHARED.NotifyType.JOB_FAILED: + case NotifyType.JOB_FAILED: return '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'; - case SHARED.NotifyType.JOB_PASSED: + case NotifyType.JOB_PASSED: return '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'; - case SHARED.NotifyType.JOB_STARTED: + case NotifyType.JOB_STARTED: return 'iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAAFBxJREFUeF7t3QXQPM1RBvAObgGCu3uwAMHd3d3dCa4BAiS4u1twd3d3t+Du7i73q9qpNFN7d7v33urNVP3re7+72b2dnmd7erqf7rlHtNYkMIEE7jHBPdstmwSiAauBYBIJNGBNItZ20washoFJJNCANYlY200bsBoGJpFAA9YkYm03bcBqGJhEAg1Yk4i13bQBq2FgEgk0YE0i1nbTBqyGgUkk0IA1iVjbTRuwGgYmkUAD1iRibTdtwGoYmEQCDViTiLXdtAGrYWASCTRgTSLWdtMGrIaBSSTQgDWJWNtNG7AaBiaRQAPWJGJtN23AahiYRAINWJOItd20AathYBIJ3CqwHiMinjwini4ifikifn0S6d7wTW8RWI8aEe8cEfeLiMePiO/r/v7lG8bB1Yd+a8B6uIh4+4j4xEqSrxkRX3N16d7wDW8NWG8TEZ9RzfefRcTzRsTv3zAOrj70WwIWm+pzI+IlkhT/PCJePyK+94xkHyYi/ufq0t/xDW8JWO8XER+a5pKmep+I+MIT8/vwEfF8EfFOEfFNEfFdEfGnO8bD1YZ2K8B6ms6GetYkua+KiNc+IUmyeZPD7vHzUx///+CrSX/HN7oVYL1xpZksgS8QEb99Ym6fLSK+ICKePfVh9L/LjvFwtaFtHVhsn+fopPGHEQEwdXuUiPiiiHj19MWXH3xXr3dCin3X/EVEvHJE/MTVpL/jG20dWDTRu0UE7fIVEfG+h+Xrd6v5eoqD1vn6rk/5Chh/7oy2+raIeMLU5wMi4kE7xsJVh7ZlYL1sRLxnRLx4ksjzRMRPVhK6z0HT/Gz6jFYDrD85IclPjoh3TN9/96H/W/eA9qqTsaebbRlYvOefUAHmRXrCMwx02qw0nnZL2j8dmUje+I+vlkr+r8/a08RPPZatAsvy9tERwWNe2tce4n6v0SOwN4iIL06f/2BEvFRE/McR4T5LtxN8zu57Gu65I+IPpp6MPd1/q8DiPvjqZDdZ1mihn+mZnBft4oHlq1/pgPIvRybSbvGH03e/1v3Of+5p4qcey1aBRYPk3Zkd2wtGxG/2COy5IuIbDkvZE3Xf/eLBDnv5iPjjI8K1nH5/+u6HDp75F556IvZ2/60Ci3YCltJMPs3UF3Z5qoj41Ihg7Jf2qtX1eV6fPyK+MiKeuPvwVztj/9/3NvlTjmerwHqdiOCLKs1yxZHZZzfxSTHG7epK+7rOOH/Eg4sCYP43Ih4hIiyPz3wISn97cjWwsew2W5B6BBK3CqwX63Z6j9uN1VL4ahHxjxFxz4h4koh4tG7544t6pk6jFdHo/5AOUH/VfQhcDxsRjxQRls9yb19/8OGzb+6A98/djtJvHdsAjJiCfXbdGrAeMyIe+6CtnqFjKnANlCaoDBzZqXntWROABsq/PoR2/q5jn/5WB1KfASnA3XxbO7AeubN1GN6ve9BSz9iB6glWOHM/320eMFHZZZyyf3SrWm2NwLIcMbhfpXMhPO2BnLdGIJ3D9i90nnosih+7Na/9moCFi26Je6uO0Sn+d0mzVP10RFii+J44NrkW/ruzve5fAfV7OqqyHaWl9W8i4sk6G8uz5OXW89BGjxMRjzfi4YBMvFK4Canw30Zcu8muawGWeB4u+isMtJF4zwGIsxPHiuGe21t2Nlg9KTjvPPaZ+sJuElz+tJ4ZFNSWdJE1JmB+S0e7sVQjAlqiH2uAZvVbCIPcH/xpu21LA4vt9OYR8Q5nJgWQTAQGJzDRKn/bzcq9u4ni2CztOw8T/UkdAOrJs2MU4sn9JVIg8dnx5Waz8DlVqIjH3/MCdmm0Fy133y4o7plce2wjwR77lG6n2Uf12TzglgTW0x98Rx9W8aSyQHm/AUkM0CQC07HGz8RhmpctSRNvd+SCN+w0V9FEDG7Bav6wuuFt8YPle9OQlrZjDaBoMI5cscp7Jc9/vgZoPzwifmfzSKoGsBSwxPo+u/ItlUczYXjobJ8xW3caCje9NFqOjfQbPZMmsYJfijO0tBeqYoTlc+DjjM0a7gER8SEDwUBzARen7lP3AIy2fPe9OWCXANajH5yYH3vYirODcqM1aBlv8SXhE07Nn0o3tGQClh1ZX0OfEQYq7c06KnJfX/ZX1n4oNO/d+bIG4itsTt7isKFgo9WGPzvOi7Ybo34JYKG62ILnhk1gebpL2IQ2+NFq0qR2ARgjmw30l53dY9z4XILZpQE1LYkpYYf4pN0OkN0FVO+V+iL+AcklVBraWhpaDmwz6nG+GPa7aEsAi82Ud3G27y99gm1wTNB2eLb9bBk2DFvIDq1w4F3HMOZmKMyGMZPGruPgxKsHTDZhaWjN79953nncj1Fwjv0ePx2bMC/FwkYfNOYB19x3bmABga02L3ppNAf7aEgDJCDBdVfY4ym7OOCUYZxTz2Wnyl+GB6a4iP+eojzne31MZ1uVz2hxy3G9Mx0il9X1mRtYvOgoKTmliu/qW09IRtKo63jiaTaaYykgnZtA4OKOsKQB3KnNh/HknSVGBQ7/LoqTzA0sS8CPVD4rTsiPODJjHI92U+yPLYV1xAktl5Y7L43luG5yFBntpfFtoVbvwvUwN7DQVxjTuWEl4Eplw1W80Paf8HP28jmNsbbvgcvSz64sDl3PWGsrnzHg0YF2UatrbmDVqVgFCIxs6VZ8OqgvbLAvWxtK7vA8b9u5MsQu7YrVkeiLhUrkaEvhBYKWjXyqDpUtP9fDy3VOxQt+YpWXlHikHMi+TKLy0OcSaVc5uL6HmltjDdFEbI1s3G9GmGce1JJ/zk60RH7jHgY8N7Dsej4qCY7a50I4J/A9yLoeA6D9a+cyKd/tprLg3MCqa1RJarAjBDYe6ZIZs0cg5TExB4yZL8smpTT1uoSl8PXZW+QhyQOfjH0m5GPXaAPEgZs3BKuS2dzAErzN3mV+HF54zAEhHT4tlYz3CjDxSbFQISHA4PPKNteQ5RKA7DYF10Ut/P0Dh+Io/7AmZM0NLGGLD0wCoLFyeSFLIvqKbbe6oHtYItmMPPIcoPIfM/+qDm9dgg0bA3YZsKqQswrP/dzAqlkCNbCKYMXmUFtesWNqCt1Ix7ok5nfJZN31GkRD8UMaCU1acL2PuSCMk+tP3PV3sS4wR/qoQne996jr1wqsPAhJpUI4lkuFadFj2GOM/rWEdgp37Pc6/xutRJOcyztka2WNbdyWNVrOPbAxAFKMVWjLC6dYiWD7seC6ZxF7XXR3OTew0HHRektjb+BMjWkETND+IdExfmU789CXvEN8J45W/+6q5QCkTDJGxd93gWa1I2gG+YWMaCyH/xozkAOV5/O6wDNSIlnYJYs3nroPtgUTAeXnXTubtDYZPK8wWC5DMPLR7tZ9i8A6NmJhIGQ6IPNmy2im7eyqcLF8Dxi0nHHnsQMgAOFh0QT+4VrROEiHtAbXgOWNDdMX+7tkJvj1LJMANYYtW34LyOwevbCKouT2JR3tuQ6hXfKco6+ZG1h1pbxLNNboQW70AjXAuBi8JIrEAbedX1/hE9rbpui1Kg3NhrUi0KaztrmBVfPSjxnvswphRT+GHkQDKQwnx5LGpGG5ISy3qjxbevH1Bauz5qSZVXmui/Zy49Bes7a5gYV1+cA0QsbrNXdFswrvij/GRuLPkluJAXKuMe7Rm7kw8L5KY2Oy2zJDVyKIAsCzHnwwN7B4lqU7lWa7jZc+1ug9J/gtfc8h7MSMS14wtVVdy+AvDd1Z2lw26HHaECxna3MDS0LCR6bR8USrrreb7JSRM9eXWFHf4lw8FWDe45Dcoc59fmEzUGUAoe7Mdh7Q3MCikjnwSlPHQHW9S3ZEI+dwdd1x9m1eak3FdwUs6ENcGrQ5QEibU5VQ2ly9XNYlBSTw/ngaMQ2GT3+sPObVhTM3sOxaskoWhqCmT2U5X33QK7khh68XK4NE7O+Nqrr09eOKQOCtZccqUqSk12JHKSPA45+zt1GRFCeZpc0NrPpNkuUiQWKX9QvOzKBdn81LAZbsHjs6ztJzTS4AUBY7Cog4RMtBCXx431HlLvKZ5Xr3537jTt/PDSx0XDua0vytvkG2D+40oA1dLCE2J7yO4bwDjoBzzuSu3Qo1c+JUpvfVxTY3sKRuMdhLjI8w1UToK6N99cGu7IYoQvxR+SXjJhBvHNJotszlkpnN1VBaHT6b9SyguYHlLZWNkxMJXiYisAFurTG47dZKIwNaZWjCq/4cqaU54Uwib2lKNSlGUppsoXw+0KTynhtYCtNa5/PxuZbC3dQsGDFbgvG0Smnk4rOh4ZeacqP6DSJlaQz6nHEu5JOd0yMedXzXuYHVZ1SOSbEfP8L1XvGlVfjFTo+fb4jrheGPXpOr1rxkV9SkjFi4jCuntHqpnFQycwPLYD6zKub/cVUNg0kHvKKb1xqHNsns2mOPitEgdyBrHzRnhnwpHMfksBTmijZs2SE7zquIaAlg1QkVs679V5Ha3W8CHDROprq86ZmDz/2qEgXsprrom9wBRedUx9E4Uu0aS7NJ4taZLRl2CWApLKv0Yml44Dju1+I43X3ap7+DYLEaFtk5KplVhg6PPHqMTB10GX39bZPDHiWr3GgreQKAWlq9zLLfGO7lFI7JR7gEsGT7MtYLs9NbRE3fkvcdtViRuNyU6kYkpM28ZOYGAVE4R2C5LgvuWuEf/it05PJioi7TVvnIFrUx8g50l8BSeU8cLEff2Qf5jZt84Av/QH0m4iWP44UU0KedcnCZdqLBSsPlYtjPyiRdQmNxOYgX5rOcz1UhvkTwa76mPvV1zLMCFPOBO8F/c+srwynljqE/q6mxBLD8pjSlXNzW2c4SA26lYXqqKz+0iVZYIsX/kPtEKuozrdV9UN48222C/Hxjs6eDLQEswqwJf5ZGRuktEP4kdViqLF/+BhLcdtlHwFLmBE0ZO7RkGzG82VR141rAGsl0JH3sBDlIZ3Mx5AdbClhSlzjwcmoWAZXt8tA3+Zb7yThyGqzjWnLMsMhEpURgW+Qs66WAJctZzluOGe6mhM/EaEdltqsUV8yHGpSfFWvkG5S4Ui+XEz/aQ2+/FLBklCD5Z9K/uFk+WWI2Icz8Q2TOFcBfJQ+Si8GyyFeFJepzTWjH0idPkpkgHYz95Ppjh58j8iH88ZEtSvdeClgE18d/ZysMDcLOjIer/By6kKWLB734q+QEApa5YBoU/9WYY+u4FFBw1Gydzbt+SiJLAovTD/Uj1194pYqjdJXZXNFNeL8l7V6rMdCFc2h/4Jp993dsIEsCi9pXuCLbCXuPG/Lf0cp3aeWcRssdBi5XxOqiFksCi3AddJRrvMuPkw62x92hVC9hnBxqwfk3B/5xPbCpuBdK87fPec39wy5lnwr/rFpGSwMLVdm5hDm8I4F1T6W4C0hqb7sEEnwpafNS6dlZCpCwvcxLMezVbuDfU+VmM21pYPHFWB7y7lDmilNXV1X68I4ziqEg2pCXQdX8TpXmvuNPLnv50sAyeiUhgSk7S73Ji9V2mmBK6vr2jG5H1e1pjP9PbGsAlu02VmnOCLbTkWO3qC/mSgDjh8LmzFpZ1EFi6irqhV5pnKsDlgcCIqfL52rJe0myqM/N4Rm3JNZ8rCnmd7F7rkFjGTzjHa8osyNpLYa8SnpbbbzpD+6xIRHvVuciuKaQ1wIsY7JrQlzLWotxy8jdauvjRx071HyrY+x97jUBi9biZshp49LEFSPro4usfSJQiXnE83i8JHhoqz1R4lpCXROwjAlPSaJF3iEOyV65ljyueR/sg5zybifIgFeaaPdtbcAS2ceuzImWIvYC1ltKw1caHNszO365VOwEt2wzDn4h1gYsD66OkyyTPCmLVf8dLMmHdsRK+PSqfhUvu41JSSi94LbbumSNwCLBOqnVZ/fv4oqzlTu8YCpFEjA364RSx+k56etm2lqBRVthOtTHgaw9jtiXfcNtIkS1e4M9vzVrBZZnVO9c7cycqIlzhGVqt7i2ZuOBaJeXcGwNztBdHCA+RuBrBpZxmBSTlcmAwCVVTPB6Dcui5e9+PVkyntMm5CZ2gTXo1g4sz6eYmHy5+rAlTEw142ctjF8JkDPXMlfbVJ5JiWzRhJtsaweWSVFTSyERS2ANLuQ3J7Yq9DpniEQ2N3tKaObeFXKASmkm/9agURcB9haARTBO8VK2G9u074xCZEHOSLXNMSun8BVhKch9dOiRMkE1oDwnqrAsGc9zs6AiiK0Aq7x1EjRVvmPY9zUTi5GpLPVDOjov5qVUKmlWmfZ77E0u7M17dqlYaD1Azb8mp+/YccKyYyyLShHdfNsasEwY7jgPNs1x7oRVy5I4o2XSdp8WYVT7b0ldL5zzcsCk/97rUCtVLXX9zv0GMIsBYjE4e7C1DWqsMml2YrSWovn4TkscSg6g6MZcH/mQpAasDQOrTJ4MFsUzgOu+3cHkU4Ks5O45A4c9d4v16Qe9OFtcCo8NDKnOrpFhLZh9n27ZlG5l2Tu3pNX3tcTJypbUYUOgaovPMEDZa62dkMCegFUPU/4dgDHCyznRipH4nB1VzoYu50ErTKYyCzvJrhIfndHvRNNbKK901Rdlz8C6qqDazcZJoAFrnLxa74ESaMAaKKjWbZwEGrDGyav1HiiBBqyBgmrdxkmgAWucvFrvgRJowBooqNZtnAQasMbJq/UeKIEGrIGCat3GSaABa5y8Wu+BEmjAGiio1m2cBBqwxsmr9R4ogQasgYJq3cZJoAFrnLxa74ESaMAaKKjWbZwEGrDGyav1HiiBBqyBgmrdxkmgAWucvFrvgRJowBooqNZtnAQasMbJq/UeKIEGrIGCat3GSaABa5y8Wu+BEmjAGiio1m2cBBqwxsmr9R4ogQasgYJq3cZJoAFrnLxa74ESaMAaKKjWbZwE/g8BwXjEyuIx8AAAAABJRU5ErkJggg=='; - case SHARED.NotifyType.SECURITY_NOTICE: + case NotifyType.SECURITY_NOTICE: return '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'; default: return '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'; } } - private static formatTextBody(type: SHARED.NotifyType, params: SHARED.NotifyPackage): string { - const msgPrefix: string = SHARED.getMessagePrefixByType(type); + private static formatTextBody(params: NotifyPackage): string { + const msgPrefix: string = getMessagePrefixByType(params.type); const startDate: Date = params.startDate; const duration: string | undefined = (params.endDate) ? UTIL.getDurationString(params.startDate,params.endDate) : undefined; @@ -88,9 +144,9 @@ export class NotifyEmail { return result; } - private static formatHtmlBody(type: SHARED.NotifyType, params: SHARED.NotifyPackage): string { + private static formatHtmlBody(params: NotifyPackage): string { - const msgPrefix: string = SHARED.getMessagePrefixByType(type); + const msgPrefix: string = getMessagePrefixByType(params.type); const startDate: Date = params.startDate; const duration: string | undefined = (params.endDate) ? UTIL.getDurationString(params.startDate,params.endDate) : undefined; @@ -156,11 +212,7 @@ export class NotifyEmail { //#endregion //#region PUBLIC - public static async sendMessageRaw(type: SHARED.NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise { - - // get our email addresses if needed - if(sendTo.length<=0) - return { success: false, message: 'failed to send message', data: { error: 'no addresses provided' } }; + private static async postMessage(entry: EmailEntry): Promise { // get our SMTP parameters from config // NOTE: currently unencrypted and insecure. do not send anything sensitive! @@ -176,14 +228,14 @@ export class NotifyEmail { // SMTP dialog client.write('HELO si.edu\r\n'); client.write(`MAIL FROM:<${from}>\r\n`); - for(const recipient of sendTo) + for(const recipient of entry.sendTo) client.write(`RCPT TO:<${recipient}>\r\n`); client.write('DATA\r\n'); // MIME email body with plain text and HTML parts client.write(`From: ${from}\r\n`); - client.write(`To: ${sendTo.join(', ')}\r\n`); - client.write(`Subject: ${UTIL.truncateString(subject,60)}\r\n`); + client.write(`To: ${entry.sendTo.join(', ')}\r\n`); + client.write(`Subject: ${UTIL.truncateString(entry.subject,60)}\r\n`); client.write('MIME-Version: 1.0\r\n'); client.write(`Content-Type: multipart/alternative; boundary="${boundary}"\r\n`); client.write('\r\n'); @@ -193,15 +245,15 @@ export class NotifyEmail { client.write('Content-Type: text/plain; charset="UTF-8"\r\n'); client.write('Content-Transfer-Encoding: 7bit\r\n'); client.write('\r\n'); - client.write(`${textBody}\r\n`); + client.write(`${entry.textBody}\r\n`); // HTML part - if(htmlBody) { + if(entry.htmlBody) { client.write(`--${boundary}\r\n`); client.write('Content-Type: text/html; charset="UTF-8"\r\n'); client.write('Content-Transfer-Encoding: 7bit\r\n'); client.write('\r\n'); - client.write(`${htmlBody}\r\n`); + client.write(`${entry.htmlBody}\r\n`); } // attachments @@ -216,7 +268,7 @@ export class NotifyEmail { client.write('\r\n'); // add our base64 icon from the type - const base64Icon: string = this.getMessageIconBase64(type); + const base64Icon: string = NotifyEmail.getMessageIconBase64(entry.type); client.write(`${base64Icon}\r\n`); // End of message @@ -228,10 +280,10 @@ export class NotifyEmail { client.on('data', (data) => { // get our data and make sure it's not an error const response = data.toString(); - serverResponses.push(...this.storeServerResponse(response)); + serverResponses.push(...NotifyEmail.storeServerResponse(response)); // see if we have an errors in the mix - const errors = this.extractErrorFromResponse(serverResponses); + const errors = NotifyEmail.extractErrorFromResponse(serverResponses); // Handle server responses if (errors.length > 0) { @@ -246,10 +298,10 @@ export class NotifyEmail { client.on('end', () => { // console.log('Connection closed.'); // go through our responses and see if it makes sense - if(this.verifyServerResponses(serverResponses)===true) + if(NotifyEmail.verifyServerResponses(serverResponses)===true) resolve({ success: true, message: 'email sent' }); else - resolve({ success: false, message: 'failed to send email', data: { error: this.extractErrorFromResponse(serverResponses) } }); + resolve({ success: false, message: 'failed to send email', data: { error: NotifyEmail.extractErrorFromResponse(serverResponses) } }); }); client.on('error', (err) => { @@ -262,19 +314,38 @@ export class NotifyEmail { }); }); } - public static async sendMessage(notifyType: SHARED.NotifyType, params: SHARED.NotifyPackage): Promise { + public static async sendMessageRaw(type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise { + + // get our email addresses if needed + if(sendTo.length<=0) + return { success: false, message: 'failed to send message', data: { error: 'no addresses provided' } }; + + const entry: EmailEntry = { + type, + sendTo, + subject, + textBody, + htmlBody + }; + + if(NotifyEmail.rateManager && NotifyEmail.rateManager.isActive()===true) + NotifyEmail.rateManager.add(entry); + + return { success: true, message: 'added message to be sent' }; + } + public static async sendMessage(params: NotifyPackage): Promise { // if we have sendTo address(es) then we ignore the channel if(!params.sendTo) return { success: false, message: 'failed to send message', data: { error: 'no email address provided.' } }; // build our text and html bodies - const textBody: string = this.formatTextBody(notifyType, params); - const htmlBody: string = this.formatHtmlBody(notifyType, params); + const textBody: string = NotifyEmail.formatTextBody(params); + const htmlBody: string = NotifyEmail.formatHtmlBody(params); try { - const subject: string = `[Packrat:${SHARED.getMessagePrefixByType(notifyType)}] ${params.message}`; - return await this.sendMessageRaw(notifyType,params.sendTo,subject,textBody,htmlBody); + const subject: string = `[Packrat:${getMessagePrefixByType(params.type)}] ${params.message}`; + return await NotifyEmail.sendMessageRaw(params.type,params.sendTo,subject,textBody,htmlBody); } catch (error) { return { success: false, message: 'failed to send message', data: { error: UTIL.getErrorString(error) } }; } diff --git a/server/records/notify/notifyShared.ts b/server/records/notify/notifyShared.ts index 2a51a6dc..b81a7b23 100644 --- a/server/records/notify/notifyShared.ts +++ b/server/records/notify/notifyShared.ts @@ -21,6 +21,7 @@ export interface NotifyResult { data?: any } export interface NotifyPackage { + type: NotifyType, // what type of message is this message: string, // main message for the user startDate: Date, // when did this job/event happen or start endDate?: Date, // when did this job/event end (optional) diff --git a/server/records/recordKeeper.ts b/server/records/recordKeeper.ts index 4d60c9ef..586890ac 100644 --- a/server/records/recordKeeper.ts +++ b/server/records/recordKeeper.ts @@ -4,7 +4,7 @@ import { Config } from '../config'; import { ASL, LocalStore } from '../utils/localStore'; import * as H from '../utils/helpers'; import { Logger as LOG, LogSection } from './logger/log'; -import { sendEmailMessage, sendEmailMessageRaw, NotifyChannel, NotifyType, NotifyPackage } from './notify/notify'; +import { Notify as NOTIFY, NotifyChannel, NotifyType, NotifyPackage } from './notify/notify'; /** TODO: * - change H.IOResults.error to message and make a requirement @@ -70,7 +70,8 @@ export class RecordKeeper { // region CONFIG:NOTIFY // get our email addresses from the system. these can be cached because they will be - // the same for all users and sessions. + // the same for all users and sessions. Uses defaults of 1 email/sec. + NOTIFY.configureEmail('dev'); this.notifyChannelConfig[NotifyChannel.EMAIL_ADMIN] = await this.getEmailsFromChannel(NotifyChannel.EMAIL_ADMIN) ?? this.defaultEmail; this.notifyChannelConfig[NotifyChannel.EMAIL_ALL] = await this.getEmailsFromChannel(NotifyChannel.EMAIL_ALL) ?? this.defaultEmail; @@ -156,7 +157,7 @@ export class RecordKeeper { return this.notifyChannelConfig[NotifyChannel.EMAIL_ADMIN]; // get ids from Config and then get their emails - return ['maslowskiec@si.edu','ericmaslowski@gmail.com']; + return ['maslowskiec@si.edu']; } case NotifyChannel.EMAIL_USER: { @@ -172,6 +173,7 @@ export class RecordKeeper { // build our package const params: NotifyPackage = { + type, message: subject, detailsMessage: body, startDate: startDate ?? new Date(), @@ -180,14 +182,14 @@ export class RecordKeeper { // send our message out this.logInfo(LogSection.eSYS,'sending email',{ sendTo: params.sendTo },'RecordKeeper.sendEmail',true); - const emailResult = await sendEmailMessage(type,params); + const emailResult = await NOTIFY.sendEmailMessage(params); this.logInfo(LogSection.eSYS,emailResult.message,emailResult.data,'RecordKeeper.sendEmail',true); // convert and return the results return convertToIOResults(emailResult); } static async sendEmailRaw(type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise { - const emailResult = await sendEmailMessageRaw(type, sendTo, subject, textBody, htmlBody); + const emailResult = await NOTIFY.sendEmailMessageRaw(type, sendTo, subject, textBody, htmlBody); return convertToIOResults(emailResult); } static async emailTest(): Promise { diff --git a/server/records/utils/rateManager.ts b/server/records/utils/rateManager.ts index 78cb89dd..22d0eadb 100644 --- a/server/records/utils/rateManager.ts +++ b/server/records/utils/rateManager.ts @@ -3,7 +3,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ //#region TYPES & INTERFACES -interface RateManagerResult { +export interface RateManagerResult { success: boolean, message: string, data?: any, @@ -14,9 +14,13 @@ export interface RateManagerConfig { burstThreshold?: number, // when queue is bigger than this size, trigger 'burst mode' staggerLogs?: boolean, // do we spread logs out over each interval or submit as a single batch minInterval?: number, // minimum duration for a batch/interval in ms. (higher values use less resources) - onPost?: ((entry: T) => Promise), // function to call when posting an entry + onPost?: ((entry: T) => Promise), // function to call when posting an entry onMessage?: (isError: boolean, message: string, data: any) => void, // function to call when there's a message others should know about } +export interface RateManagerEntry { + resolve: (value?: RateManagerResult) => void, + reject: (reason?: RateManagerResult) => void +} //#endregion export class RateManager { @@ -30,8 +34,14 @@ export class RateManager { burstThreshold: 3000, staggerLogs: true, minInterval: 100, - onPost: async (entry: T) => { console.log('[Logger] Unconfigured onPost',entry); }, - onMessage: (isError, message, data) => { console.log('[Logger] Unconfigured onMessage',{ isError, message, data }); } + onPost: async (entry: T) => { + console.log('[Logger] Unconfigured onPost',entry); + return { success: true, message: 'Unconfigured onPost', data: { ...entry } }; + }, + onMessage: (isError, message, data) => { + console.log('[Logger] Unconfigured onMessage', { isError, message, data }); + return { isError, message, data }; + } }; constructor(cfg: RateManagerConfig) { From 46071cff21d2e38eeee236b97fc1d645ac982e89 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Mon, 7 Oct 2024 15:37:06 -0400 Subject: [PATCH 04/12] Binding fixes for logger (fix) explicit calls to Logger vs. dynamic this. resolves 'this' binding --- server/records/logger/log.ts | 166 +++++++++++++-------------- server/records/notify/notify.ts | 1 - server/records/notify/notifyEmail.ts | 1 + 3 files changed, 84 insertions(+), 84 deletions(-) diff --git a/server/records/logger/log.ts b/server/records/logger/log.ts index c33752f0..2bfe5b7c 100644 --- a/server/records/logger/log.ts +++ b/server/records/logger/log.ts @@ -101,12 +101,12 @@ export class Logger { //#region PUBLIC private static isActive(): boolean { // we're initialized if we have a logger running - return (this.logger); + return (Logger.logger); } public static configure(logDirectory: string, env: 'prod' | 'dev', rateManager: boolean = true, targetRate?: number, burstRate?: number, burstThreshold?: number, staggerLogs?: boolean): LoggerResult { // we allow for re-assigning configuration options even if already running - this.logDir = logDirectory; - this.environment = env; + Logger.logDir = logDirectory; + Logger.environment = env; // if we want a rate limiter then we build it if(rateManager===true) { @@ -115,23 +115,23 @@ export class Logger { burstRate, burstThreshold, staggerLogs, - onPost: this.postLogToWinston.bind(this), + onPost: Logger.postLogToWinston, //.bind(this), }; // if we already have a manager we re-configure it (causes restart). otherwise, we create a new one - if(this.rateManager) - this.rateManager.setConfig(rmConfig); + if(Logger.rateManager) + Logger.rateManager.setConfig(rmConfig); else { - this.rateManager = new RateManager(rmConfig); + Logger.rateManager = new RateManager(rmConfig); } - } else if(this.rateManager) { + } else if(Logger.rateManager) { // if we don't want a rate manager but have one, clean it up - this.rateManager.stopRateManager(); - this.rateManager = null; + Logger.rateManager.stopRateManager(); + Logger.rateManager = null; } // if we already have a logger skip creating another one - if(this.isActive()===true) + if(Logger.isActive()===true) return { success: true, message: 'Winston logger already running' }; const customLevels = { @@ -199,7 +199,7 @@ export class Logger { // Format data fields in parenthesis let dataFields: string = ''; if (info.data) { - dataFields = `${this.getTextColorCode('dim')}(${this.processData(info.data)})${this.getTextColorCode()}`; + dataFields = `${Logger.getTextColorCode('dim')}(${Logger.processData(info.data)})${Logger.getTextColorCode()}`; } // Build the formatted log message @@ -211,12 +211,12 @@ export class Logger { logDirectory = path.resolve(__dirname, logDirectory); } - if (!fs.existsSync(this.logDir)) { - fs.mkdirSync(this.logDir, { recursive: true }); + if (!fs.existsSync(Logger.logDir)) { + fs.mkdirSync(Logger.logDir, { recursive: true }); } const fileTransport = new transports.File({ - filename: this.getLogFilePath(), + filename: Logger.getLogFilePath(), format: customJsonFormat, // handleExceptions: false // used to disable buffering for higher volume support at risk of errors maxsize: 150 * 1024 * 1024, // 150 MB in bytes @@ -234,7 +234,7 @@ export class Logger { }); // Use both transports: console in dev mode, and file transport for file logging - this.logger = createLogger({ + Logger.logger = createLogger({ level: 'perf', // Logging all levels levels: customLevels.levels, transports: env === 'dev' ? [fileTransport, consoleTransport] : [fileTransport], @@ -245,15 +245,15 @@ export class Logger { addColors(customLevels.colors); // start our rate manager if needed - if(this.rateManager) - this.rateManager.startRateManager(); + if(Logger.rateManager) + Logger.rateManager.startRateManager(); // start up our metrics tracker (sampel every 5 seconds, 10 samples per avgerage calc) - this.trackLogMetrics(5000,10); + Logger.trackLogMetrics(5000,10); } catch(error) { - if(this.rateManager) - this.rateManager.stopRateManager(); + if(Logger.rateManager) + Logger.rateManager.stopRateManager(); return { success: false, @@ -264,11 +264,11 @@ export class Logger { return { success: true, message: `(${env}) configured Logger. Sending to file ${(env==='dev') ? 'and console' : ''}` }; } public static getStats(): LoggerStats { - this.stats.counts.total = (this.stats.counts.critical + this.stats.counts.error + this.stats.counts.warning + this.stats.counts.info + this.stats.counts.debug); - return this.stats; + Logger.stats.counts.total = (Logger.stats.counts.critical + Logger.stats.counts.error + Logger.stats.counts.warning + Logger.stats.counts.info + Logger.stats.counts.debug); + return Logger.stats; } public static setDebugMode(value: boolean): void { - this.debugMode = value; + Logger.debugMode = value; } //#endregion @@ -288,7 +288,7 @@ export class Logger { context: { section: context.section, caller: context.caller ?? null, - environment: this.environment, + environment: Logger.environment, idUser: context.idUser, idRequest: context.idRequest } @@ -300,7 +300,7 @@ export class Logger { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); - const logDir = path.join(this.logDir, `${year}`, `${month}`); + const logDir = path.join(Logger.logDir, `${year}`, `${month}`); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); @@ -318,12 +318,12 @@ export class Logger { // If data is an array, join elements with commas if (Array.isArray(data)) { - return data.map(item => this.processData(item)).join(', '); + return data.map(item => Logger.processData(item)).join(', '); } // If data is an object, flatten it and convert to a string of key/value pairs if (typeof data === 'object' && data !== null) { - const flatObject = this.flattenObject(data); + const flatObject = Logger.flattenObject(data); return Object.entries(flatObject) .map(([key, value]) => `${key}: ${value}`) .join(', '); @@ -337,7 +337,7 @@ export class Logger { const value = (obj as Record)[key]; if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - Object.assign(acc, this.flattenObject(value, newKey)); // Recursively flatten nested objects + Object.assign(acc, Logger.flattenObject(value, newKey)); // Recursively flatten nested objects } else { acc[newKey] = value.toString(); // Assign non-object values directly } @@ -350,21 +350,21 @@ export class Logger { private static updateStats(entry: LogEntry): void { // we do this here so we can track when it actually gets posted if done by the rate manager switch(entry.level) { - case 'crit': this.stats.counts.critical++; break; - case 'error': this.stats.counts.error++; break; - case 'warn': this.stats.counts.warning++; break; - case 'info': this.stats.counts.info++; break; - case 'debug': this.stats.counts.debug++; break; - case 'perf': this.stats.counts.profile++; break; + case 'crit': Logger.stats.counts.critical++; break; + case 'error': Logger.stats.counts.error++; break; + case 'warn': Logger.stats.counts.warning++; break; + case 'info': Logger.stats.counts.info++; break; + case 'debug': Logger.stats.counts.debug++; break; + case 'perf': Logger.stats.counts.profile++; break; } - this.stats.counts.total++; + Logger.stats.counts.total++; } private static async trackLogMetrics(interval: number, avgSamples: number): Promise { // if already running, bail - if(this.metricsIsRunning===true) + if(Logger.metricsIsRunning===true) return; - this.metricsIsRunning = true; + Logger.metricsIsRunning = true; // make sure our interval and samples are valid interval = Math.max(interval, 1000); @@ -375,22 +375,22 @@ export class Logger { const lastSample = { timestamp: new Date(), - startSize: this.stats.counts.total + startSize: Logger.stats.counts.total }; - while(this.metricsIsRunning) { - const currentSize: number = this.stats.counts.total; + while(Logger.metricsIsRunning) { + const currentSize: number = Logger.stats.counts.total; // Ensure no divide-by-zero and that log count is greater than last sample if (currentSize - lastSample.startSize > 0) { const newLogRate: number = (currentSize - lastSample.startSize) / elapsedSeconds; // see if we have a new maximum and assign the log rate - this.stats.metrics.logRateMax = Math.max(this.stats.metrics.logRate,newLogRate); - this.stats.metrics.logRate = newLogRate; + Logger.stats.metrics.logRateMax = Math.max(Logger.stats.metrics.logRate,newLogRate); + Logger.stats.metrics.logRate = newLogRate; // Track the log rate to calculate rolling average - logRates.push(this.stats.metrics.logRate); + logRates.push(Logger.stats.metrics.logRate); // Maintain rolling average window size if(logRates.length > avgSamples) { @@ -399,22 +399,22 @@ export class Logger { // Calculate rolling average const totalLogRate = logRates.reduce((sum, rate) => sum + rate, 0); - this.stats.metrics.logRateAvg = totalLogRate / logRates.length; + Logger.stats.metrics.logRateAvg = totalLogRate / logRates.length; } else { - this.stats.metrics.logRate = 0; + Logger.stats.metrics.logRate = 0; } lastSample.timestamp = new Date(); lastSample.startSize = currentSize; - if(this.debugMode===true) - console.log(`\tLog metrics update: (${this.stats.metrics.logRate} log/s | avg: ${this.stats.metrics.logRateAvg})`); + if(Logger.debugMode===true) + console.log(`\tLog metrics update: (${Logger.stats.metrics.logRate} log/s | avg: ${Logger.stats.metrics.logRateAvg})`); - await this.delay(interval); + await Logger.delay(interval); } - this.metricsIsRunning = false; + Logger.metricsIsRunning = false; } // helper to get specific color codes for text out in the console @@ -470,52 +470,52 @@ export class Logger { private static async postLog(entry: LogEntry): Promise { // if we have the rate manager running, queue it up // otherwise just send to the logger - if(this.rateManager && this.rateManager.isActive()===true) - this.rateManager.add(entry); + if(Logger.rateManager && Logger.rateManager.isActive()===true) + Logger.rateManager.add(entry); else { - await this.postLogToWinston(entry); + await Logger.postLogToWinston(entry); } } private static async postLogToWinston(entry: LogEntry): Promise { - await this.logger.log(entry); - this.updateStats(entry); + await Logger.logger.log(entry); + Logger.updateStats(entry); return { success: true, message: 'posted message' }; } // wrappers for each level of log public static critical(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { - if(this.isActive()===false) + if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - this.postLog(this.getLogEntry('crit', message, data, audit, { section, caller, idUser, idRequest })); + Logger.postLog(Logger.getLogEntry('crit', message, data, audit, { section, caller, idUser, idRequest })); return { success: true, message: 'posted log message' }; } public static error(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { - if(this.isActive()===false) + if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - this.postLog(this.getLogEntry('error', message, data, audit, { section, caller, idUser, idRequest })); + Logger.postLog(Logger.getLogEntry('error', message, data, audit, { section, caller, idUser, idRequest })); return { success: true, message: 'posted log message' }; } public static warning(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { - if(this.isActive()===false) + if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - this.postLog(this.getLogEntry('warn', message, data, audit, { section, caller, idUser, idRequest })); + Logger.postLog(Logger.getLogEntry('warn', message, data, audit, { section, caller, idUser, idRequest })); return { success: true, message: 'posted log message' }; } public static info(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { - if(this.isActive()===false) + if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - this.postLog(this.getLogEntry('info', message, data, audit, { section, caller, idUser, idRequest })); + Logger.postLog(Logger.getLogEntry('info', message, data, audit, { section, caller, idUser, idRequest })); return { success: true, message: 'posted log message' }; } public static debug(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { - if(this.isActive()===false) + if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - this.postLog(this.getLogEntry('debug', message, data, audit, { section, caller, idUser, idRequest })); + Logger.postLog(Logger.getLogEntry('debug', message, data, audit, { section, caller, idUser, idRequest })); return { success: true, message: 'posted log message' }; } //#endregion @@ -524,23 +524,23 @@ export class Logger { public static profile(key: string, section: LogSection, message: string, data?: any, caller?: string, idUser?: number, idRequest?: number): LoggerResult { // make sure we don't have the same - if(this.requests.has(key)===true) + if(Logger.requests.has(key)===true) return { success: false, message: 'profile request key already created.' }; // otherwise, create our request entry for performance level - const logEntry: LogEntry = this.getLogEntry('perf', message, data, false, { section, caller, idUser, idRequest } ); + const logEntry: LogEntry = Logger.getLogEntry('perf', message, data, false, { section, caller, idUser, idRequest } ); const profileRequest: ProfileRequest = { startTime: new Date(), logEntry, }; - this.requests.set(key,profileRequest); + Logger.requests.set(key,profileRequest); return { success: true, message: 'created profile request' }; } public static profileEnd(key: string): LoggerResult { // get our request and make sure it's valid - const profileRequest: ProfileRequest | undefined = this.requests.get(key); + const profileRequest: ProfileRequest | undefined = Logger.requests.get(key); if(!profileRequest) return { success: false, message: 'cannot find profile request' }; @@ -562,9 +562,9 @@ export class Logger { profileRequest.logEntry.data = { profiler: elapsedMilliseconds }; // log the results and cleanup - this.postLog(profileRequest.logEntry); - this.stats.counts.profile++; - this.requests.delete(key); + Logger.postLog(profileRequest.logEntry); + Logger.stats.counts.profile++; + Logger.requests.delete(key); const message: string = `${hours}:${minutes}:${seconds}:${milliseconds}`; return { success: true, message }; @@ -671,13 +671,13 @@ export class Logger { } public static async testLogs(numLogs: number): Promise { // NOTE: given the static assignment this works best when nothing else is feeding logs - const hasRateManager: boolean = this.rateManager?.isActive() ?? false; - const config: RateManagerConfig | null = this.rateManager?.getConfig() ?? null; + const hasRateManager: boolean = Logger.rateManager?.isActive() ?? false; + const config: RateManagerConfig | null = Logger.rateManager?.getConfig() ?? null; // create our profiler // we use a random string in case another test or profile is run to avoid collisisons const profileKey: string = `LogTest_${Math.random().toString(36).substring(2, 6)}`; - this.profile(profileKey, LogSection.eHTTP, `Log test: ${new Date().toLocaleString()}`, { + Logger.profile(profileKey, LogSection.eHTTP, `Log test: ${new Date().toLocaleString()}`, { numLogs, rateManager: hasRateManager, ...(hasRateManager === true && config && { @@ -686,19 +686,19 @@ export class Logger { },'Logger.test'); // capture the current total count so we can adjust in case other events are going on - const startCount: number = this.stats.counts.total; + const startCount: number = Logger.stats.counts.total; // test our logging for(let i=0; i timeout) { @@ -707,12 +707,12 @@ export class Logger { } // Wait for 1 second before checking again - await this.delay(1000); + await Logger.delay(1000); } // close our profiler and return results - const result = this.profileEnd(profileKey); - return { success: true, message: `finished testing ${numLogs} logs. (time: ${result.message} | maxRate: ${this.stats.metrics.logRateMax})` }; + const result = Logger.profileEnd(profileKey); + return { success: true, message: `finished testing ${numLogs} logs. (time: ${result.message} | maxRate: ${Logger.stats.metrics.logRateMax})` }; } //#endregion } diff --git a/server/records/notify/notify.ts b/server/records/notify/notify.ts index 59557575..85c0445d 100644 --- a/server/records/notify/notify.ts +++ b/server/records/notify/notify.ts @@ -1,4 +1,3 @@ -// import { NotifyResult } from './notifyShared'; import { NotifyResult, NotifyPackage, NotifyChannel, NotifyType } from './notifyShared'; import { NotifyEmail } from './notifyEmail'; diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index 2a973b67..035c4982 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -6,6 +6,7 @@ import { RateManager, RateManagerConfig, RateManagerResult } from '../utils/rate /** * - get error/success messages out allowing caller to wait for results * - test routines + * - stats (rate, type counts) */ // declaring this empty for branding/clarity since it is used From 659564544ad042b60929a00ddc682fd182cc1da5 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Tue, 8 Oct 2024 16:05:11 -0400 Subject: [PATCH 05/12] Asynchronous calling & improved messaging (new) asynchronous calling for Logger + Notify (new) defined fromAddress pointing to packrat@si.edu (fix) cleaner error handling and bubbling up of status (fix) cleaned up return types (fix) removed onMessage in RateManager. Messages attach to promise (fix) commented console handling in legacy Logger for cleaner debugging. --- server/http/routes/sandbox.ts | 8 +- server/records/logger/log.ts | 82 ++++++------ server/records/notify/notify.ts | 13 +- server/records/notify/notifyEmail.ts | 187 ++++++++++++++------------- server/records/recordKeeper.ts | 131 +++++++++++-------- server/records/utils/rateManager.ts | 33 +++-- server/utils/logger.ts | 116 ++++++++--------- 7 files changed, 303 insertions(+), 267 deletions(-) diff --git a/server/http/routes/sandbox.ts b/server/http/routes/sandbox.ts index ff9f5b3b..4a8b5526 100644 --- a/server/http/routes/sandbox.ts +++ b/server/http/routes/sandbox.ts @@ -7,12 +7,12 @@ export const play = async (_req: Request, res: Response): Promise => { await RK.configure(); // test our logging - // const numLogs: number = 1000; - // const result = await RK.logTest(numLogs); + const numLogs: number = 10; + const result = await RK.logTest(numLogs); // test email notifications - const result = await RK.emailTest(); + // const result = await RK.emailTest(); // return our results - res.status(200).send(H.Helpers.JSONStringify({ message: result.error })); + res.status(200).send(H.Helpers.JSONStringify(result)); }; \ No newline at end of file diff --git a/server/records/logger/log.ts b/server/records/logger/log.ts index 2bfe5b7c..746da7f3 100644 --- a/server/records/logger/log.ts +++ b/server/records/logger/log.ts @@ -4,7 +4,7 @@ import { createLogger, format, transports, addColors } from 'winston'; import * as path from 'path'; import * as fs from 'fs'; -import { RateManager, RateManagerConfig } from '../utils/rateManager'; +import { RateManager, RateManagerConfig, RateManagerResult } from '../utils/rateManager'; // adjust our default event hanlder to support higher throughput. (default is 10) require('events').EventEmitter.defaultMaxListeners = 50; @@ -78,11 +78,12 @@ interface ProfileRequest { startTime: Date, logEntry: LogEntry } -interface LoggerResult { - success: boolean, - message: string, - data?: any -} + +// declaring this empty for branding/clarity since it is used +// for instances that are not related to the RateManager +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface LoggerResult extends RateManagerResult {} + //#endregion export class Logger { @@ -115,7 +116,7 @@ export class Logger { burstRate, burstThreshold, staggerLogs, - onPost: Logger.postLogToWinston, //.bind(this), + onPost: Logger.postLogToWinston, }; // if we already have a manager we re-configure it (causes restart). otherwise, we create a new one @@ -261,7 +262,7 @@ export class Logger { }; } - return { success: true, message: `(${env}) configured Logger. Sending to file ${(env==='dev') ? 'and console' : ''}` }; + return { success: true, message: `configured Logger. Sending to file ${(env==='dev') ? 'and console' : ''}` }; } public static getStats(): LoggerStats { Logger.stats.counts.total = (Logger.stats.counts.critical + Logger.stats.counts.error + Logger.stats.counts.warning + Logger.stats.counts.info + Logger.stats.counts.debug); @@ -467,61 +468,60 @@ export class Logger { //#endregion //#region LOG - private static async postLog(entry: LogEntry): Promise { + private static async postLog(entry: LogEntry): Promise { // if we have the rate manager running, queue it up // otherwise just send to the logger if(Logger.rateManager && Logger.rateManager.isActive()===true) - Logger.rateManager.add(entry); + return Logger.rateManager.add(entry); else { - await Logger.postLogToWinston(entry); + return Logger.postLogToWinston(entry); } } private static async postLogToWinston(entry: LogEntry): Promise { - await Logger.logger.log(entry); - Logger.updateStats(entry); - return { success: true, message: 'posted message' }; + // wrapping in a promise to ensure the logger finishes all transports + // before moving on. + return new Promise((resolve)=> { + Logger.logger.log(entry); + Logger.updateStats(entry); + resolve ({ success: true, message: 'posted message' }); + }); } // wrappers for each level of log - public static critical(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static async critical(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): Promise { if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - Logger.postLog(Logger.getLogEntry('crit', message, data, audit, { section, caller, idUser, idRequest })); - return { success: true, message: 'posted log message' }; + return Logger.postLog(Logger.getLogEntry('crit', message, data, audit, { section, caller, idUser, idRequest })); } - public static error(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static async error(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): Promise { if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - Logger.postLog(Logger.getLogEntry('error', message, data, audit, { section, caller, idUser, idRequest })); - return { success: true, message: 'posted log message' }; + return Logger.postLog(Logger.getLogEntry('error', message, data, audit, { section, caller, idUser, idRequest })); } - public static warning(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static async warning(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): Promise { if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - Logger.postLog(Logger.getLogEntry('warn', message, data, audit, { section, caller, idUser, idRequest })); - return { success: true, message: 'posted log message' }; + return Logger.postLog(Logger.getLogEntry('warn', message, data, audit, { section, caller, idUser, idRequest })); } - public static info(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static async info(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): Promise { if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - Logger.postLog(Logger.getLogEntry('info', message, data, audit, { section, caller, idUser, idRequest })); - return { success: true, message: 'posted log message' }; + return Logger.postLog(Logger.getLogEntry('info', message, data, audit, { section, caller, idUser, idRequest })); } - public static debug(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): LoggerResult { + public static async debug(section: LogSection, message: string, data?: any, caller?: string, audit: boolean=false, idUser?: number, idRequest?: number): Promise { if(Logger.isActive()===false) return { success: false, message: 'cannot post log. no Logger. run configure' }; - Logger.postLog(Logger.getLogEntry('debug', message, data, audit, { section, caller, idUser, idRequest })); - return { success: true, message: 'posted log message' }; + return Logger.postLog(Logger.getLogEntry('debug', message, data, audit, { section, caller, idUser, idRequest })); } //#endregion //#region PROFILING - public static profile(key: string, section: LogSection, message: string, data?: any, caller?: string, idUser?: number, idRequest?: number): LoggerResult { + public static async profile(key: string, section: LogSection, message: string, data?: any, caller?: string, idUser?: number, idRequest?: number): Promise { // make sure we don't have the same if(Logger.requests.has(key)===true) @@ -537,7 +537,7 @@ export class Logger { return { success: true, message: 'created profile request' }; } - public static profileEnd(key: string): LoggerResult { + public static async profileEnd(key: string): Promise { // get our request and make sure it's valid const profileRequest: ProfileRequest | undefined = Logger.requests.get(key); @@ -561,13 +561,14 @@ export class Logger { else profileRequest.logEntry.data = { profiler: elapsedMilliseconds }; - // log the results and cleanup - Logger.postLog(profileRequest.logEntry); + // log the results and cleanup. + // we await so we can cleanup the request + const result: LoggerResult = await Logger.postLog(profileRequest.logEntry); Logger.stats.counts.profile++; Logger.requests.delete(key); - const message: string = `${hours}:${minutes}:${seconds}:${milliseconds}`; - return { success: true, message }; + result.message = `${hours}:${minutes}:${seconds}:${milliseconds}`; + return result; } //#endregion @@ -677,11 +678,11 @@ export class Logger { // create our profiler // we use a random string in case another test or profile is run to avoid collisisons const profileKey: string = `LogTest_${Math.random().toString(36).substring(2, 6)}`; - Logger.profile(profileKey, LogSection.eHTTP, `Log test: ${new Date().toLocaleString()}`, { + await Logger.profile(profileKey, LogSection.eHTTP, `Log test: ${new Date().toLocaleString()}`, { numLogs, rateManager: hasRateManager, ...(hasRateManager === true && config && { - config: (({ onPost: _onPost, onMessage: _onMessage, ...rest }) => rest)(config) // Exclude onPost and onMessage + config: (({ onPost: _onPost, ...rest }) => rest)(config) // Exclude onPost }) },'Logger.test'); @@ -710,8 +711,13 @@ export class Logger { await Logger.delay(1000); } + // test await/async + // console.log('pre'); + // const t = await Logger.info(LogSection.eTEST, 'await test' ); + // console.log('post',t); + // close our profiler and return results - const result = Logger.profileEnd(profileKey); + const result = await Logger.profileEnd(profileKey); return { success: true, message: `finished testing ${numLogs} logs. (time: ${result.message} | maxRate: ${Logger.stats.metrics.logRateMax})` }; } //#endregion diff --git a/server/records/notify/notify.ts b/server/records/notify/notify.ts index 85c0445d..ab72c3f0 100644 --- a/server/records/notify/notify.ts +++ b/server/records/notify/notify.ts @@ -1,16 +1,21 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { NotifyResult, NotifyPackage, NotifyChannel, NotifyType } from './notifyShared'; import { NotifyEmail } from './notifyEmail'; export class Notify { - // email wrappers + //#region EMAIL public static configureEmail(env: 'prod' | 'dev', targetRate?: number, burstRate?: number, burstThreshold?: number): NotifyResult { return NotifyEmail.configure(env,targetRate,burstRate,burstThreshold); } - public static sendEmailMessage = NotifyEmail.sendMessage; - public static sendEmailMessageRaw = NotifyEmail.sendMessageRaw; - // slack + // cast the returns to NotifyResult so it's consistent with what is exported + public static sendEmailMessage = NotifyEmail.sendMessage as (params: NotifyPackage) => Promise; + public static sendEmailMessageRaw = NotifyEmail.sendMessageRaw as (type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string) => Promise; + //#endregion + + //#region SLACK + //#endregio } // export shared types so they can be accessed via Notify diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index 035c4982..52938bf1 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -25,6 +25,7 @@ interface EmailEntry { export class NotifyEmail { private static rateManager: RateManager | null = null; private static environment: 'prod' | 'dev' = 'dev'; + private static fromAddress: string = ''; public static isActive(): boolean { // we're initialized if we have a logger running @@ -33,6 +34,7 @@ export class NotifyEmail { public static configure(env: 'prod' | 'dev', targetRate?: number, burstRate?: number, burstThreshold?: number): EmailResult { // we allow for re-assigning configuration options even if already running NotifyEmail.environment = env; + NotifyEmail.fromAddress = (NotifyEmail.environment==='dev') ? 'packrat@si.edu' : 'packrat@si.edu'; // TODO: 'packrat-dev@si.edu' : 'packrat-noreply@si.edu'; // if we want a rate limiter then we build it const rmConfig: RateManagerConfig = { @@ -45,9 +47,8 @@ export class NotifyEmail { // if we already have a manager we re-configure it (causes restart). otherwise, we create a new one if(NotifyEmail.rateManager) NotifyEmail.rateManager.setConfig(rmConfig); - else { + else NotifyEmail.rateManager = new RateManager(rmConfig); - } // if we already configured skip creating another one if(NotifyEmail.isActive()===true) @@ -57,7 +58,7 @@ export class NotifyEmail { if(NotifyEmail.rateManager) NotifyEmail.rateManager.startRateManager(); - return { success: true, message: `(${NotifyEmail.environment}) configured email notifier.` }; + return { success: true, message: 'configured email notifier.' }; } //#region UTILS @@ -220,99 +221,100 @@ export class NotifyEmail { const smtpHost: string = 'smtp.si.edu'; const smtpPort: number = 25; - const from: string = 'maslowskiec@si.edu'; + const from: string = NotifyEmail.fromAddress; const boundary: string = '----=_Packrat_Ops_Msg_001'; - return new Promise((resolve) => { - const serverResponses: { statusCode: number, message: string}[] = []; - const client = NET.createConnection(smtpPort,smtpHost, () => { - // SMTP dialog - client.write('HELO si.edu\r\n'); - client.write(`MAIL FROM:<${from}>\r\n`); - for(const recipient of entry.sendTo) - client.write(`RCPT TO:<${recipient}>\r\n`); - client.write('DATA\r\n'); - - // MIME email body with plain text and HTML parts - client.write(`From: ${from}\r\n`); - client.write(`To: ${entry.sendTo.join(', ')}\r\n`); - client.write(`Subject: ${UTIL.truncateString(entry.subject,60)}\r\n`); - client.write('MIME-Version: 1.0\r\n'); - client.write(`Content-Type: multipart/alternative; boundary="${boundary}"\r\n`); - client.write('\r\n'); - - // Plain-text part - client.write(`--${boundary}\r\n`); - client.write('Content-Type: text/plain; charset="UTF-8"\r\n'); - client.write('Content-Transfer-Encoding: 7bit\r\n'); - client.write('\r\n'); - client.write(`${entry.textBody}\r\n`); - - // HTML part - if(entry.htmlBody) { + try { + const serverResponses: { statusCode: number, message: string}[] = []; + const client = NET.createConnection(smtpPort,smtpHost, () => { + // SMTP dialog + client.write('HELO si.edu\r\n'); + client.write(`MAIL FROM:<${from}>\r\n`); + for(const recipient of entry.sendTo) + client.write(`RCPT TO:<${recipient}>\r\n`); + client.write('DATA\r\n'); + + // MIME email body with plain text and HTML parts + client.write(`From: ${from}\r\n`); + client.write(`To: ${entry.sendTo.join(', ')}\r\n`); + client.write(`Subject: ${UTIL.truncateString(entry.subject,60)}\r\n`); + client.write('MIME-Version: 1.0\r\n'); + client.write(`Content-Type: multipart/alternative; boundary="${boundary}"\r\n`); + client.write('\r\n'); + + // Plain-text part client.write(`--${boundary}\r\n`); - client.write('Content-Type: text/html; charset="UTF-8"\r\n'); + client.write('Content-Type: text/plain; charset="UTF-8"\r\n'); client.write('Content-Transfer-Encoding: 7bit\r\n'); client.write('\r\n'); - client.write(`${entry.htmlBody}\r\n`); - } - - // attachments - // NOTE: we need to put all images as attachments and then reference by CID - // for compatability since GMail removes any base64 embedded images - client.write(`--${boundary}\r\n`); - client.write('Content-Type: image/png; name="header.png"\r\n'); - client.write('Content-Disposition: inline; filename="header.png"\r\n'); - client.write('Content-Transfer-Encoding: base64\r\n'); - client.write('Content-ID: <0123456789>\r\n'); - client.write('Content-Location: header.png\r\n'); - client.write('\r\n'); - - // add our base64 icon from the type - const base64Icon: string = NotifyEmail.getMessageIconBase64(entry.type); - client.write(`${base64Icon}\r\n`); - - // End of message - client.write(`--${boundary}--\r\n`); - client.write('.\r\n'); - client.write('QUIT\r\n'); - }); - - client.on('data', (data) => { - // get our data and make sure it's not an error - const response = data.toString(); - serverResponses.push(...NotifyEmail.storeServerResponse(response)); - - // see if we have an errors in the mix - const errors = NotifyEmail.extractErrorFromResponse(serverResponses); - - // Handle server responses - if (errors.length > 0) { + client.write(`${entry.textBody}\r\n`); + + // HTML part + if(entry.htmlBody) { + client.write(`--${boundary}\r\n`); + client.write('Content-Type: text/html; charset="UTF-8"\r\n'); + client.write('Content-Transfer-Encoding: 7bit\r\n'); + client.write('\r\n'); + client.write(`${entry.htmlBody}\r\n`); + } + + // attachments + // NOTE: we need to put all images as attachments and then reference by CID + // for compatability since GMail removes any base64 embedded images + client.write(`--${boundary}\r\n`); + client.write('Content-Type: image/png; name="header.png"\r\n'); + client.write('Content-Disposition: inline; filename="header.png"\r\n'); + client.write('Content-Transfer-Encoding: base64\r\n'); + client.write('Content-ID: <0123456789>\r\n'); + client.write('Content-Location: header.png\r\n'); + client.write('\r\n'); + + // add our base64 icon from the type + const base64Icon: string = NotifyEmail.getMessageIconBase64(entry.type); + client.write(`${base64Icon}\r\n`); + + // End of message + client.write(`--${boundary}--\r\n`); + client.write('.\r\n'); + client.write('QUIT\r\n'); + }); + + client.on('data', (data) => { + // get our data and make sure it's not an error + const response = data.toString(); + serverResponses.push(...NotifyEmail.storeServerResponse(response)); + + // see if we have an errors in the mix + const errors = NotifyEmail.extractErrorFromResponse(serverResponses); + + // Handle server responses + if (errors.length > 0) { + resolve({ + success: false, + message: 'failed to send email.', + data: { error: errors } + }); + } + }); + + client.on('end', () => { + // go through our responses and see if it makes sense + if(NotifyEmail.verifyServerResponses(serverResponses)===true) + resolve({ success: true, message: 'email sent' }); + else + resolve({ success: false, message: 'failed to send email', data: { error: NotifyEmail.extractErrorFromResponse(serverResponses) } }); + }); + + client.on('error', (err) => { resolve({ success: false, message: 'failed to send email.', - data: { error: errors } + data: { error: UTIL.getErrorString(err) } }); - } - }); - - client.on('end', () => { - // console.log('Connection closed.'); - // go through our responses and see if it makes sense - if(NotifyEmail.verifyServerResponses(serverResponses)===true) - resolve({ success: true, message: 'email sent' }); - else - resolve({ success: false, message: 'failed to send email', data: { error: NotifyEmail.extractErrorFromResponse(serverResponses) } }); - }); - - client.on('error', (err) => { - // console.error('Error:', err); - resolve({ - success: false, - message: 'failed to send email.', - data: { error: UTIL.getErrorString(err) } }); - }); + } catch(error) { + resolve({ success: false, message: 'failed to postMessage', data: { error: UTIL.getErrorString(error) } }); + } }); } public static async sendMessageRaw(type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise { @@ -329,10 +331,11 @@ export class NotifyEmail { htmlBody }; + // if we have a manager use it, otherwise, just send directly if(NotifyEmail.rateManager && NotifyEmail.rateManager.isActive()===true) - NotifyEmail.rateManager.add(entry); - - return { success: true, message: 'added message to be sent' }; + return NotifyEmail.rateManager.add(entry); + else + return this.postMessage(entry); } public static async sendMessage(params: NotifyPackage): Promise { @@ -345,8 +348,10 @@ export class NotifyEmail { const htmlBody: string = NotifyEmail.formatHtmlBody(params); try { - const subject: string = `[Packrat:${getMessagePrefixByType(params.type)}] ${params.message}`; - return await NotifyEmail.sendMessageRaw(params.type,params.sendTo,subject,textBody,htmlBody); + // figure out our subject and send to raw output + // returning the promise so it can be waited on (if needed) + const subject: string = `[${getMessagePrefixByType(params.type)}] ${params.message}`; + return NotifyEmail.sendMessageRaw(params.type,params.sendTo,subject,textBody,htmlBody); } catch (error) { return { success: false, message: 'failed to send message', data: { error: UTIL.getErrorString(error) } }; } diff --git a/server/records/recordKeeper.ts b/server/records/recordKeeper.ts index 586890ac..97550473 100644 --- a/server/records/recordKeeper.ts +++ b/server/records/recordKeeper.ts @@ -2,30 +2,35 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Config } from '../config'; import { ASL, LocalStore } from '../utils/localStore'; -import * as H from '../utils/helpers'; import { Logger as LOG, LogSection } from './logger/log'; import { Notify as NOTIFY, NotifyChannel, NotifyType, NotifyPackage } from './notify/notify'; /** TODO: - * - change H.IOResults.error to message and make a requirement + * - change IOResults.error to message and make a requirement */ // utils -const convertToIOResults = ( result: { success: boolean, message: string, data?: any }): H.IOResults => { - // converts standard response from logging and notification systems into Packrat's current - // 'results' structure, which has 'error' instead of message. - let error: string | undefined = undefined; - if(result.success===false && result.data) { - if(result.data.error) - error = result.data.error; - else - error = JSON.stringify(result.data); - } - - const msg: string = result.message + ((error!=undefined) ? ` ${error}` : ''); - return { success: result.success, error: msg }; +// const convertToIOResults = ( result: { success: boolean, message: string, data?: any }): IOResults => { +// // converts standard response from logging and notification systems into Packrat's current +// // 'results' structure, which has 'error' instead of message. +// let error: string | undefined = undefined; +// if(result.success===false && result.data) { +// if(result.data.error) +// error = result.data.error; +// else +// error = JSON.stringify(result.data); +// } + +// const msg: string = result.message + ((error!=undefined) ? ` ${error}` : ''); +// return { success: result.success, error: msg }; +// }; + +// temp definition for where IOResults will be +type IOResults = { + success: boolean, + message: string, + data?: any }; - type ChannelConfig = { [NotifyChannel.SLACK_DEV]: string, [NotifyChannel.SLACK_OPS]: string, @@ -39,7 +44,7 @@ export class RecordKeeper { static NotifyChannel = NotifyChannel; static NotifyType = NotifyType; - private static defaultEmail: string[] = ['maslowskiec@si.edu']; + private static defaultEmail: string[] = ['packrat@si.edu']; private static notifyChannelConfig: ChannelConfig = { [NotifyChannel.EMAIL_ADMIN]: [], [NotifyChannel.EMAIL_ALL]: [], @@ -47,10 +52,11 @@ export class RecordKeeper { [NotifyChannel.SLACK_OPS]: 'C07NCJE9FJM', // packrat-ops }; - static async configure(): Promise { + static async configure(): Promise { //#region CONFIG:LOGGER // get our log path from the config + const environment: 'prod' | 'dev' = 'dev'; const logPath: string = Config.log.root ? Config.log.root : /* istanbul ignore next */ './var/logs'; // our default Logger configuration options @@ -62,24 +68,28 @@ export class RecordKeeper { const staggerLogs: boolean = true; // do we spread the logs out during each interval or all at once // initialize logger sub-system - const logResults = LOG.configure(logPath, 'dev', useRateManager, targetRate, burstRate, burstThreshold, staggerLogs); + const logResults = LOG.configure(logPath, environment, useRateManager, targetRate, burstRate, burstThreshold, staggerLogs); if(logResults.success===false) - return convertToIOResults(logResults); - this.logInfo(LogSection.eSYS, logResults.message,{ path: logPath, useRateManager, targetRate, burstRate, burstThreshold, staggerLogs }); + return logResults; + this.logInfo(LogSection.eSYS, logResults.message, { environment, path: logPath, useRateManager, targetRate, burstRate, burstThreshold, staggerLogs }, 'Recordkeeper'); //#endregion - // region CONFIG:NOTIFY + //#region CONFIG:NOTIFY + const notifyResults = NOTIFY.configureEmail(environment); + if(notifyResults.success===false) + return notifyResults; + this.logInfo(LogSection.eSYS, notifyResults.message, { environment }, 'Recordkeeper'); + // get our email addresses from the system. these can be cached because they will be // the same for all users and sessions. Uses defaults of 1 email/sec. - NOTIFY.configureEmail('dev'); this.notifyChannelConfig[NotifyChannel.EMAIL_ADMIN] = await this.getEmailsFromChannel(NotifyChannel.EMAIL_ADMIN) ?? this.defaultEmail; this.notifyChannelConfig[NotifyChannel.EMAIL_ALL] = await this.getEmailsFromChannel(NotifyChannel.EMAIL_ALL) ?? this.defaultEmail; - //#endregion - return { success: true }; + + return { success: true, message: 'configured record keeper' }; } - static cleanup(): H.IOResults { - return { success: true }; + static cleanup(): IOResults { + return { success: true, message: 'record keeper cleaned up' }; } private static getContext(): { idUser: number, idRequest: number } { // get our user and request ids from the local store @@ -94,35 +104,35 @@ export class RecordKeeper { //#region LOG // Log routines for specific levels - static logCritical(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): void { + static async logCritical(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): Promise { const { idUser, idRequest } = this.getContext(); - LOG.critical(sec,message,data,caller,audit,idUser,idRequest); + return LOG.critical(sec,message,data,caller,audit,idUser,idRequest); } - static logError(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): void { + static async logError(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): Promise { const { idUser, idRequest } = this.getContext(); - LOG.error(sec,message,data,caller,audit,idUser,idRequest); + return LOG.error(sec,message,data,caller,audit,idUser,idRequest); } - static logWarning(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): void { + static async logWarning(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): Promise { const { idUser, idRequest } = this.getContext(); - LOG.warning(sec,message,data,caller,audit,idUser,idRequest); + return LOG.warning(sec,message,data,caller,audit,idUser,idRequest); } - static logInfo(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): void { + static async logInfo(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): Promise { const { idUser, idRequest } = this.getContext(); - LOG.info(sec,message,data,caller,audit,idUser,idRequest); + return LOG.info(sec,message,data,caller,audit,idUser,idRequest); } - static logDebug(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): void { + static async logDebug(sec: LogSection, message: string, data?: any, caller?: string, audit: boolean = false): Promise { const { idUser, idRequest } = this.getContext(); - LOG.debug(sec,message,data,caller,audit,idUser,idRequest); + return LOG.debug(sec,message,data,caller,audit,idUser,idRequest); } // profiler functions // Usage: call 'profile' with a unique label and any needed metadata. This creates/starts a timer. // to stop the timer and log the result, call profileEnd with the same label used to create it. - static profile(label: string, sec: LogSection, message: string, data?: any, caller?: string): H.IOResults { + static async profile(label: string, sec: LogSection, message: string, data?: any, caller?: string): Promise { const { idUser, idRequest } = this.getContext(); return LOG.profile(label, sec, message, data, caller, idUser, idRequest); } - static profileEnd(label: string): H.IOResults { + static async profileEnd(label: string): Promise { return LOG.profileEnd(label); } @@ -130,9 +140,8 @@ export class RecordKeeper { static logTotalCount(): number { return LOG.getStats().counts.total; } - static async logTest(numLogs: number): Promise { - const result = await LOG.testLogs(numLogs); - return convertToIOResults(result); + static async logTest(numLogs: number): Promise { + return LOG.testLogs(numLogs); } //#endregion @@ -157,7 +166,7 @@ export class RecordKeeper { return this.notifyChannelConfig[NotifyChannel.EMAIL_ADMIN]; // get ids from Config and then get their emails - return ['maslowskiec@si.edu']; + return ['maslowskiec@si.edu','emaslowski@quotient-inc.com']; } case NotifyChannel.EMAIL_USER: { @@ -169,7 +178,7 @@ export class RecordKeeper { return undefined; } - static async sendEmail(type: NotifyType, channel: NotifyChannel, subject: string, body: string, startDate?: Date): Promise { + static async sendEmail(type: NotifyType, channel: NotifyChannel, subject: string, body: string, startDate?: Date): Promise { // build our package const params: NotifyPackage = { @@ -180,25 +189,39 @@ export class RecordKeeper { sendTo: await this.getEmailsFromChannel(channel) }; - // send our message out + // send our message out. + // we await the result so we can catch and audit the failure this.logInfo(LogSection.eSYS,'sending email',{ sendTo: params.sendTo },'RecordKeeper.sendEmail',true); const emailResult = await NOTIFY.sendEmailMessage(params); - this.logInfo(LogSection.eSYS,emailResult.message,emailResult.data,'RecordKeeper.sendEmail',true); + if(emailResult.success===false) + this.logError(LogSection.eSYS,'failed to send email',{ sendTo: params.sendTo },'RecordKeeper.sendEmail',true); - // convert and return the results - return convertToIOResults(emailResult); + // return the results + return emailResult; } - static async sendEmailRaw(type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise { + static async sendEmailRaw(type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise { + + // send our email but also log it for auditing + // we wait for results so we can log the failure + this.logInfo(LogSection.eSYS,'sending raw email',{ sendTo },'RecordKeeper.sendEmailRaw',true); const emailResult = await NOTIFY.sendEmailMessageRaw(type, sendTo, subject, textBody, htmlBody); - return convertToIOResults(emailResult); + if(emailResult.success===false) + this.logError(LogSection.eSYS,'failed to send raw email',{ sendTo },'RecordKeeper.sendEmailRaw',true); + + return emailResult; } - static async emailTest(): Promise { - const result = await this.sendEmail(NotifyType.JOB_FAILED, NotifyChannel.EMAIL_ADMIN, 'test message', 'test body for email...'); + static async emailTest(): Promise { + + // this.sendEmail(NotifyType.SYSTEM_ERROR, NotifyChannel.EMAIL_ADMIN, 'Packrat out of disk space!', 'Packrat is running low with only 6% of disk space available. If you have pending items uploaded but not ingested please do so. '); + // this.sendEmail(NotifyType.SYSTEM_NOTICE, NotifyChannel.EMAIL_ADMIN, 'Packrat maintenance @ 11pm', 'Packrat will be offline starting at 11pm tonight'); + // this.sendEmail(NotifyType.JOB_PASSED, NotifyChannel.EMAIL_ADMIN, 'Model ingestion completed', 'Model "Something awesome" was ingested...'); + // this.sendEmail(NotifyType.JOB_STARTED, NotifyChannel.EMAIL_ADMIN, 'Uploading capture data started', 'Uploading capture data comprised of 456 images for subject "something awesome"...'); + const result = await this.sendEmail(NotifyType.JOB_FAILED, NotifyChannel.EMAIL_ADMIN, 'Scene generation failed for: Something awesome', 'Voyager scene failed to be created because: no normals...'); if(result.success===true) - this.logInfo(LogSection.eTEST,result.error ?? 'NA', undefined, 'RecordKeeper.emailTest',false); + this.logInfo(LogSection.eTEST,result.message ?? 'NA', undefined, 'RecordKeeper.emailTest',false); else - this.logError(LogSection.eTEST,result.error ?? 'Unknown error', undefined, 'RecordKeeper.emailTest'); + this.logError(LogSection.eTEST,result.message ?? 'Unknown error', undefined, 'RecordKeeper.emailTest'); return result; } diff --git a/server/records/utils/rateManager.ts b/server/records/utils/rateManager.ts index 22d0eadb..7d34e7b2 100644 --- a/server/records/utils/rateManager.ts +++ b/server/records/utils/rateManager.ts @@ -15,17 +15,16 @@ export interface RateManagerConfig { staggerLogs?: boolean, // do we spread logs out over each interval or submit as a single batch minInterval?: number, // minimum duration for a batch/interval in ms. (higher values use less resources) onPost?: ((entry: T) => Promise), // function to call when posting an entry - onMessage?: (isError: boolean, message: string, data: any) => void, // function to call when there's a message others should know about -} -export interface RateManagerEntry { - resolve: (value?: RateManagerResult) => void, - reject: (reason?: RateManagerResult) => void } //#endregion export class RateManager { private isRunning: boolean = false; - private queue: T[] = []; + private queue: Array <{ + entry: T, + resolve: (value: RateManagerResult) => void + // reject: (reason: RateManagerResult) => void + }> = []; private isQueueLocked: boolean = false; private debugMode: boolean = false; private config: RateManagerConfig = { @@ -37,10 +36,6 @@ export class RateManager { onPost: async (entry: T) => { console.log('[Logger] Unconfigured onPost',entry); return { success: true, message: 'Unconfigured onPost', data: { ...entry } }; - }, - onMessage: (isError, message, data) => { - console.log('[Logger] Unconfigured onMessage', { isError, message, data }); - return { isError, message, data }; } }; @@ -52,7 +47,6 @@ export class RateManager { staggerLogs: cfg.staggerLogs ?? this.config.staggerLogs, minInterval: cfg.minInterval ?? this.config.minInterval, onPost: cfg.onPost ?? this.config.onPost, - onMessage: cfg.onMessage ?? this.config.onMessage }; } public isActive(): boolean { @@ -81,7 +75,7 @@ export class RateManager { // grab entries from the start of the array to process this.isQueueLocked = true; - const entries: T[] = this.queue.splice(0,entriesToGrab); + const entries = this.queue.splice(0,entriesToGrab); // cycle through entries processing each with an optional delay to spread things out let startTime: number = 0; @@ -91,7 +85,7 @@ export class RateManager { // the flow of logs to watson and ensure a FIFO process startTime = new Date().getTime(); if(this.config.onPost) - await this.config.onPost(entry); + entry.resolve(await this.config.onPost(entry.entry)); // if we want to spread out the requests, do so if(delay>0) { @@ -102,7 +96,6 @@ export class RateManager { if(ellapsedTimedelay) { - // (this.config.onMessage) && this.config.onMessage(true,'log took longer than delay',{ ellapsedTime, delay }); continue; // already took too long. just keep moving } else this.delay(waitTime); @@ -143,6 +136,7 @@ export class RateManager { } // display interval details if in debug mode + // TODO: send through to logger system/output if(this.debugMode) console.log(`\t Interval update (mode: ${currentRate === burstRate ? 'Burst' : 'Normal'} | batch: ${batchSize} | interval: ${interval} ms)`); }; @@ -185,9 +179,13 @@ export class RateManager { this.isRunning = false; return { success: true, message: 'stopped rate manager' }; } - public add(entry: T): RateManagerResult { - this.queue.push(entry); - return { success: true, message: 'added to queue' }; + public add(entry: T): Promise { + return new Promise((resolve) => { + this.queue.push({ entry, resolve }); + // return { success: true, message: 'added to queue' }; + + // TODO: start process queue... + }); } //#endregion @@ -200,7 +198,6 @@ export class RateManager { staggerLogs: cfg.staggerLogs ?? this.config.staggerLogs, minInterval: cfg.minInterval ?? this.config.minInterval, onPost: cfg.onPost ?? this.config.onPost, - onMessage: cfg.onMessage ?? this.config.onMessage }; // if our rate manager is already running then we need to restart it so it gets diff --git a/server/utils/logger.ts b/server/utils/logger.ts index 35544f21..36459ea5 100644 --- a/server/utils/logger.ts +++ b/server/utils/logger.ts @@ -8,7 +8,7 @@ import { Config } from '../config'; import { ASL, LocalStore } from './localStore'; let logger: winston.Logger; -let ending: boolean = false; +// let ending: boolean = false; export enum LS { // logger section eAUDIT, // audit eAUTH, // authentication @@ -46,7 +46,7 @@ export function error(message: string | undefined, eLogSection: LS, obj: any | n } export function end(): void { - ending = true; + // ending = true; logger.end(); } @@ -140,41 +140,41 @@ function configureLogger(logPath: string | null): void { } // Replace console debug/info/log/warn/error with our own versions: - const _debug = console.debug; - const _info = console.info; - const _log = console.log; - const _warn = console.warn; - const _error = console.error; - - console.debug = function(...args) { - if (!ending) - info(`console.debug: ${handleConsoleArgs(args)}`, LS.eCON); - return _debug.apply(console, args); - }; - - console.info = function(...args) { - if (!ending) - info(`console.info: ${handleConsoleArgs(args)}`, LS.eCON); - return _info.apply(console, args); - }; - - console.log = function(...args) { - if (!ending) - info(`console.log: ${handleConsoleArgs(args)}`, LS.eCON); - return _log.apply(console, args); - }; - - console.warn = function(...args) { - if (!ending) - info(`console.warn: ${handleConsoleArgs(args)}`, LS.eCON); - return _warn.apply(console, args); - }; - - console.error = function(...args) { - if (!ending) - error(`console.error: ${handleConsoleArgs(args)}`, LS.eCON); - return _error.apply(console, args); - }; + // const _debug = console.debug; + // const _info = console.info; + // const _log = console.log; + // const _warn = console.warn; + // const _error = console.error; + + // console.debug = function(...args) { + // if (!ending) + // info(`console.debug: ${handleConsoleArgs(args)}`, LS.eCON); + // return _debug.apply(console, args); + // }; + + // console.info = function(...args) { + // if (!ending) + // info(`console.info: ${handleConsoleArgs(args)}`, LS.eCON); + // return _info.apply(console, args); + // }; + + // console.log = function(...args) { + // if (!ending) + // info(`console.log: ${handleConsoleArgs(args)}`, LS.eCON); + // return _log.apply(console, args); + // }; + + // console.warn = function(...args) { + // if (!ending) + // info(`console.warn: ${handleConsoleArgs(args)}`, LS.eCON); + // return _warn.apply(console, args); + // }; + + // console.error = function(...args) { + // if (!ending) + // error(`console.error: ${handleConsoleArgs(args)}`, LS.eCON); + // return _error.apply(console, args); + // }; // The following approach does not work. More thought and investigation is needed here // Observe writes to stdout and stderr; forward to our log @@ -185,26 +185,26 @@ function configureLogger(logPath: string | null): void { info(`Writing logs to ${path.resolve(logPath)}`, LS.eSYS); } -function handleConsoleArgs(args): string { - if (typeof(args) === 'string') - return args; - if (!Array.isArray(args)) - return JSON.stringify(args, null, 0); - - let first: boolean = true; - let value: string = ''; - for (const arg of args) { - if (first) - first = false; - else - value += ', '; - - if (typeof(arg) === 'string') - value += arg; - else - value += JSON.stringify(arg); - } - return value; -} +// function handleConsoleArgs(args): string { +// if (typeof(args) === 'string') +// return args; +// if (!Array.isArray(args)) +// return JSON.stringify(args, null, 0); + +// let first: boolean = true; +// let value: string = ''; +// for (const arg of args) { +// if (first) +// first = false; +// else +// value += ', '; + +// if (typeof(arg) === 'string') +// value += arg; +// else +// value += JSON.stringify(arg); +// } +// return value; +// } configureLogger(null); \ No newline at end of file From 0187b65462c183749aaa543fa8f7234a73938af4 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Wed, 9 Oct 2024 15:48:52 -0400 Subject: [PATCH 06/12] Refactored RateManager for improved balancing (new) dynamic burst mode and maintained threshold (new) metrics for average rates and counts (fix) removed obsolete staggerLogs --- server/http/routes/sandbox.ts | 2 +- server/records/logger/log.ts | 17 +- server/records/notify/notifyEmail.ts | 8 +- server/records/recordKeeper.ts | 7 +- server/records/utils/rateManager.ts | 249 ++++++++++++++++++++++++++- 5 files changed, 258 insertions(+), 25 deletions(-) diff --git a/server/http/routes/sandbox.ts b/server/http/routes/sandbox.ts index 4a8b5526..5bec7ace 100644 --- a/server/http/routes/sandbox.ts +++ b/server/http/routes/sandbox.ts @@ -7,7 +7,7 @@ export const play = async (_req: Request, res: Response): Promise => { await RK.configure(); // test our logging - const numLogs: number = 10; + const numLogs: number = 1000; const result = await RK.logTest(numLogs); // test email notifications diff --git a/server/records/logger/log.ts b/server/records/logger/log.ts index 746da7f3..05a35885 100644 --- a/server/records/logger/log.ts +++ b/server/records/logger/log.ts @@ -104,7 +104,7 @@ export class Logger { // we're initialized if we have a logger running return (Logger.logger); } - public static configure(logDirectory: string, env: 'prod' | 'dev', rateManager: boolean = true, targetRate?: number, burstRate?: number, burstThreshold?: number, staggerLogs?: boolean): LoggerResult { + public static configure(logDirectory: string, env: 'prod' | 'dev', rateManager: boolean = true, targetRate?: number, burstRate?: number, burstThreshold?: number): LoggerResult { // we allow for re-assigning configuration options even if already running Logger.logDir = logDirectory; Logger.environment = env; @@ -115,7 +115,6 @@ export class Logger { targetRate, burstRate, burstThreshold, - staggerLogs, onPost: Logger.postLogToWinston, }; @@ -127,7 +126,7 @@ export class Logger { } } else if(Logger.rateManager) { // if we don't want a rate manager but have one, clean it up - Logger.rateManager.stopRateManager(); + // Logger.rateManager.stopRateManager(); Logger.rateManager = null; } @@ -246,15 +245,15 @@ export class Logger { addColors(customLevels.colors); // start our rate manager if needed - if(Logger.rateManager) - Logger.rateManager.startRateManager(); + // if(Logger.rateManager) + // Logger.rateManager.startRateManager(); // start up our metrics tracker (sampel every 5 seconds, 10 samples per avgerage calc) Logger.trackLogMetrics(5000,10); } catch(error) { - if(Logger.rateManager) - Logger.rateManager.stopRateManager(); + // if(Logger.rateManager) + // Logger.rateManager.stopRateManager(); return { success: false, @@ -471,7 +470,7 @@ export class Logger { private static async postLog(entry: LogEntry): Promise { // if we have the rate manager running, queue it up // otherwise just send to the logger - if(Logger.rateManager && Logger.rateManager.isActive()===true) + if(Logger.rateManager)// && Logger.rateManager.isActive()===true) return Logger.rateManager.add(entry); else { return Logger.postLogToWinston(entry); @@ -672,7 +671,7 @@ export class Logger { } public static async testLogs(numLogs: number): Promise { // NOTE: given the static assignment this works best when nothing else is feeding logs - const hasRateManager: boolean = Logger.rateManager?.isActive() ?? false; + const hasRateManager: boolean = (Logger.rateManager) ? (Logger.rateManager!=null) : false; const config: RateManagerConfig | null = Logger.rateManager?.getConfig() ?? null; // create our profiler diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index 52938bf1..2fc844fb 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -29,7 +29,7 @@ export class NotifyEmail { public static isActive(): boolean { // we're initialized if we have a logger running - return (NotifyEmail.rateManager!=null && NotifyEmail.rateManager.isActive()); + return (NotifyEmail.rateManager!=null); } public static configure(env: 'prod' | 'dev', targetRate?: number, burstRate?: number, burstThreshold?: number): EmailResult { // we allow for re-assigning configuration options even if already running @@ -55,8 +55,8 @@ export class NotifyEmail { return { success: true, message: 'email system already running' }; // start our rate manager if needed - if(NotifyEmail.rateManager) - NotifyEmail.rateManager.startRateManager(); + // if(NotifyEmail.rateManager) + // NotifyEmail.rateManager.startRateManager(); return { success: true, message: 'configured email notifier.' }; } @@ -332,7 +332,7 @@ export class NotifyEmail { }; // if we have a manager use it, otherwise, just send directly - if(NotifyEmail.rateManager && NotifyEmail.rateManager.isActive()===true) + if(NotifyEmail.rateManager) return NotifyEmail.rateManager.add(entry); else return this.postMessage(entry); diff --git a/server/records/recordKeeper.ts b/server/records/recordKeeper.ts index 97550473..e52ad8da 100644 --- a/server/records/recordKeeper.ts +++ b/server/records/recordKeeper.ts @@ -64,14 +64,13 @@ export class RecordKeeper { const useRateManager: boolean = true; // do we manage our log output for greater consistency const targetRate: number = Config.log.targetRate; // targeted logs per second (100-500) const burstRate: number = targetRate * 5; // target rate when in burst mode and playing catchup - const burstThreshold: number = burstRate * 5; // when queue is bigger than this size, trigger 'burst mode' - const staggerLogs: boolean = true; // do we spread the logs out during each interval or all at once + const burstThreshold: number = burstRate; // when queue is bigger than this size, trigger 'burst mode' // initialize logger sub-system - const logResults = LOG.configure(logPath, environment, useRateManager, targetRate, burstRate, burstThreshold, staggerLogs); + const logResults = LOG.configure(logPath, environment, useRateManager, targetRate, burstRate, burstThreshold); if(logResults.success===false) return logResults; - this.logInfo(LogSection.eSYS, logResults.message, { environment, path: logPath, useRateManager, targetRate, burstRate, burstThreshold, staggerLogs }, 'Recordkeeper'); + this.logInfo(LogSection.eSYS, logResults.message, { environment, path: logPath, useRateManager, targetRate, burstRate, burstThreshold }, 'Recordkeeper'); //#endregion //#region CONFIG:NOTIFY diff --git a/server/records/utils/rateManager.ts b/server/records/utils/rateManager.ts index 7d34e7b2..09ae45cc 100644 --- a/server/records/utils/rateManager.ts +++ b/server/records/utils/rateManager.ts @@ -9,15 +9,249 @@ export interface RateManagerResult { data?: any, } export interface RateManagerConfig { - targetRate?: number, // targeted logs per second (200-2000) - burstRate?: number, // target rate when in burst mode and playing catchup - burstThreshold?: number, // when queue is bigger than this size, trigger 'burst mode' - staggerLogs?: boolean, // do we spread logs out over each interval or submit as a single batch - minInterval?: number, // minimum duration for a batch/interval in ms. (higher values use less resources) - onPost?: ((entry: T) => Promise), // function to call when posting an entry + targetRate?: number, // targeted logs per second (200-2000) + burstRate?: number, // target rate when in burst mode and playing catchup + burstThreshold?: number, // when queue is bigger than this size, trigger 'burst mode' + minInterval?: number, // minimum duration for a batch/interval in ms. (higher values use less resources) + onPost: ((entry: T) => Promise), // function to call when posting an entry +} +interface RateManagerMetrics { + counts: { + processed: number, + success: number, + failure: number + }, + queueLength: number, // average queue length + rateAverage: number, + rateMax: number, + timeStart: Date, + timeProcessed: Date, } //#endregion + +export class RateManager { + private isRunning: boolean = false; + private mode: 'standard' | 'burst' = 'standard'; + private queue: Array <{ + entry: T, + resolve: (value: RateManagerResult) => void + }> = []; + private config: Required>; + private metrics: RateManagerMetrics; + + constructor(cfg: Partial>) { + // merge our configs with what the user provided + // we use partial to gaurantee that the properties have values + this.config = { + targetRate: cfg.targetRate ?? 10, + burstRate: cfg.burstRate ?? 50, + burstThreshold: cfg.burstThreshold ?? 250, + minInterval: cfg.minInterval ?? 1000, + onPost: cfg.onPost ?? (async (entry: T) => { + console.log('[Logger] Unconfigured onPost',entry); + return { success: true, message: 'Unconfigured onPost', data: { ...entry } }; + }), + }; + + this.metrics = { + counts: { + processed: 0, + success: 0, + failure: 0 + }, + queueLength: 0, + rateAverage: 0, + rateMax: 0, + timeStart: new Date(), + timeProcessed: new Date() + }; + + this.mode = 'standard'; + } + + //#region PUBLIC + public async add(entry: T): Promise { + return new Promise((resolve) => { + this.queue.push({ entry, resolve }); + return this.processQueue(); + }); + } + public setConfig(config: Partial>) { + this.config = { ...this.config, ...config }; + } + public getConfig(): RateManagerConfig { + return this.config; + } + //#endregion + + //#region UTILS + private async delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + private calculateAverageProcessingTime(): number { + const elapsedTime = (Date.now() - this.metrics.timeStart.getTime()) / 1000; // Total time in ms + const processedEntries = this.metrics.counts.processed || 1; // Avoid divide by zero + return (elapsedTime / processedEntries); // Average time per entry in ms + } + //#endregion + + //#region QUEUE + private updateRate(queueLength: number): void { + this.metrics.queueLength = queueLength; + if (queueLength > this.config.burstThreshold && this.mode === 'standard') { + this.switchToBurstMode(); + } else if (queueLength <= this.config.burstThreshold && this.mode === 'burst') { + this.switchToNormalMode(); + } + } + private switchToBurstMode(): void { + this.mode = 'burst'; + console.warn('Switched to burst mode'); + } + private switchToNormalMode(): void { + this.mode = 'standard'; + console.warn('Switched to normal mode'); + } + + private async processQueue(): Promise { + if(this.isRunning===true) + return { success: true, message: ' already running' }; + + let lastResult: RateManagerResult = { success: true, message: 'Queue processed successfully' }; + + this.isRunning = true; + while (this.queue.length > 0 && this.isRunning) { + this.updateRate(this.queue.length); + + if (this.mode === 'burst') + lastResult = await this.processAtBurstRate(); + else + lastResult = await this.processAtTargetRate(); + + // add our delay as calculated, if success + if(lastResult.data) + await this.delay(lastResult.data.delayForRate); + + // store our metrics + this.updateMetrics(); + } + + // Return the result of the last processed item or batch + this.isRunning = false; + return lastResult; + } + private async processAtBurstRate(): Promise { + // Calculate dynamic batch size based on burst rate and throughput + const idealEntriesPerSecond = this.config.burstRate; + const processingTimePerEntry = this.calculateAverageProcessingTime(); + const maxEntriesPerBatch = Math.max(1,Math.floor(1 / processingTimePerEntry)); + + const batchSize = Math.min(this.queue.length, Math.min(maxEntriesPerBatch, idealEntriesPerSecond)); + const batch = this.queue.splice(0, batchSize); // Remove batch from queue + + // place to store our accumulated failed messages + const failedMessages: Array<{ entry: T, reason: string }> = []; + + console.log('process: burst', { idealEntriesPerSecond, processingTimePerEntry, maxEntriesPerBatch, batchSize }); + const startTime: number = Date.now(); + + // Process each entry in the batch + await Promise.all(batch.map(async (queueItem) => { + let attempts = 0; + let success = false; + let result: RateManagerResult; + + while (attempts < 3 && !success) { + attempts++; + result = await this.config.onPost(queueItem.entry); + + if (result.success) { + // Resolve the promise as successful + queueItem.resolve(result); + success = true; + } else if (attempts < 3) { + // Delay before retrying (small delay between retries) + await this.delay(500); // Adjust delay as needed (500ms here) + } else { + // Max retries reached, resolve as failed and add to failedMessages + queueItem.resolve(result); + failedMessages.push({ entry: queueItem.entry, reason: result.message }); + } + } + })); + + // update our metrics + const processedCount = batchSize - failedMessages.length; + this.metrics.counts.processed += processedCount; + this.metrics.counts.success += processedCount; + this.metrics.counts.failure += failedMessages.length; + + // figure out our actual rate based on how much time has elapsed + // we do this to ensure we don't exceed our burst rate even if the + // the system processes things faster than expected + const processingTime = (Date.now() - startTime) / 1000; + const actualRate = batchSize / processingTime; + + // Calculate the delay required to maintain the burst rate + const targetTimeForBatch = batchSize / this.config.burstRate; // Time we expect the batch to take + const delayForRate = Math.max(0, (targetTimeForBatch - processingTime) * 1000); + // console.log('process: burst', { actualRate, delayForRate }); + + // if we had an failed then we return these results + if (failedMessages.length > 0) + return { success: false, message: 'One or more entries failed after retries', data: failedMessages }; + + return { success: true, message: `Batch processed successfully: ${batchSize} entries`, data: { processingTime, actualRate, delayForRate } }; + } + private async processAtTargetRate(): Promise { + // bump an entry off the list + const entry = this.queue.shift(); + if (!entry) + return { success: false, message: 'No entry found to process' }; + + // post/process our entry and resolve its promise when done + const startTime: number = Date.now(); + const result = await this.config.onPost(entry.entry); + entry.resolve(result); + + // update our metrics + this.metrics.counts.processed++; + if (result.success) + this.metrics.counts.success++; + else + this.metrics.counts.failure++; + + // figure out how long things took + // calculate delay to maintain the target rate + const processingTime = (Date.now() - startTime); + const actualRate = 1 / processingTime; + const delayForRate = 1000 / this.config.targetRate; + + return { success: true, message: 'processed entry', data: { processingTime, actualRate, delayForRate } }; + } + //#endregion + + //#region METRICS + private updateMetrics(): void { + const currentTime: Date = new Date(); + const elapsedTime = (currentTime.getTime() - this.metrics.timeStart.getTime()) / 1000; // Convert ms to seconds + + const currentRate = this.metrics.counts.processed / elapsedTime; + + this.metrics.rateAverage = (this.metrics.rateAverage * (this.metrics.counts.processed - 1) + currentRate) / this.metrics.counts.processed; + + if (currentRate > this.metrics.rateMax) + this.metrics.rateMax = currentRate; + + this.metrics.timeProcessed = currentTime; + } + public getMetrics(): RateManagerMetrics { + return this.metrics; + } + //#endregion +} +/* export class RateManager { private isRunning: boolean = false; private queue: Array <{ @@ -213,4 +447,5 @@ export class RateManager { return this.config; } //#endregion -} \ No newline at end of file +} + */ \ No newline at end of file From 647ab062676851dc74314121811e89add680cca7 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Thu, 10 Oct 2024 10:39:31 -0400 Subject: [PATCH 07/12] Robust metrics & manager control routines (new) start/stop/cleanup for the manager (new) added 'rates' to available metrics (new) debugMode for additional details (stdout) (fix) more robust and efficient metrics tracking (fix) exposed metrics structure --- server/records/utils/rateManager.ts | 305 +++++++++------------------- 1 file changed, 91 insertions(+), 214 deletions(-) diff --git a/server/records/utils/rateManager.ts b/server/records/utils/rateManager.ts index 09ae45cc..cb48485b 100644 --- a/server/records/utils/rateManager.ts +++ b/server/records/utils/rateManager.ts @@ -13,25 +13,30 @@ export interface RateManagerConfig { burstRate?: number, // target rate when in burst mode and playing catchup burstThreshold?: number, // when queue is bigger than this size, trigger 'burst mode' minInterval?: number, // minimum duration for a batch/interval in ms. (higher values use less resources) + metricsInterval?: number, // the rate in which metrics (averages) are calculated in ms (default: 5s) onPost: ((entry: T) => Promise), // function to call when posting an entry } -interface RateManagerMetrics { +export interface RateManagerMetrics { counts: { processed: number, success: number, failure: number }, + rates: { + current: number, + average: number, + max: number + } queueLength: number, // average queue length - rateAverage: number, - rateMax: number, - timeStart: Date, - timeProcessed: Date, + startTime: Date, + processedTime: Date, } //#endregion - export class RateManager { private isRunning: boolean = false; + private isMetricsRunning: boolean = false; + private debugMode: boolean = false; private mode: 'standard' | 'burst' = 'standard'; private queue: Array <{ entry: T, @@ -48,8 +53,9 @@ export class RateManager { burstRate: cfg.burstRate ?? 50, burstThreshold: cfg.burstThreshold ?? 250, minInterval: cfg.minInterval ?? 1000, + metricsInterval: cfg.metricsInterval ?? 5000, onPost: cfg.onPost ?? (async (entry: T) => { - console.log('[Logger] Unconfigured onPost',entry); + console.log('[RateManager] Unconfigured onPost',entry); return { success: true, message: 'Unconfigured onPost', data: { ...entry } }; }), }; @@ -60,13 +66,17 @@ export class RateManager { success: 0, failure: 0 }, + rates: { + current: 0, + average: 0, + max: 0 + }, queueLength: 0, - rateAverage: 0, - rateMax: 0, - timeStart: new Date(), - timeProcessed: new Date() + startTime: new Date(), + processedTime: new Date() }; + // set our mode. the manager starts automatically when first entry is received this.mode = 'standard'; } @@ -77,12 +87,30 @@ export class RateManager { return this.processQueue(); }); } + public setConfig(config: Partial>) { this.config = { ...this.config, ...config }; } public getConfig(): RateManagerConfig { return this.config; } + + public startManager(): void { + // start our queue if needed. used if manager is stopped with entries still in the queue. + this.processQueue(); + + // start our metrics tracker + this.trackRateMetrics(this.config.metricsInterval, 5); + } + public stopManager(): void { + this.isRunning = false; + this.isMetricsRunning = false; + } + public cleanup(): void { + this.stopManager(); + this.queue = []; + this.mode = 'standard'; + } //#endregion //#region UTILS @@ -90,7 +118,7 @@ export class RateManager { return new Promise(resolve => setTimeout(resolve, ms)); } private calculateAverageProcessingTime(): number { - const elapsedTime = (Date.now() - this.metrics.timeStart.getTime()) / 1000; // Total time in ms + const elapsedTime = (Date.now() - this.metrics.startTime.getTime()) / 1000; // Total time in ms const processedEntries = this.metrics.counts.processed || 1; // Avoid divide by zero return (elapsedTime / processedEntries); // Average time per entry in ms } @@ -132,9 +160,6 @@ export class RateManager { // add our delay as calculated, if success if(lastResult.data) await this.delay(lastResult.data.delayForRate); - - // store our metrics - this.updateMetrics(); } // Return the result of the last processed item or batch @@ -153,10 +178,11 @@ export class RateManager { // place to store our accumulated failed messages const failedMessages: Array<{ entry: T, reason: string }> = []; - console.log('process: burst', { idealEntriesPerSecond, processingTimePerEntry, maxEntriesPerBatch, batchSize }); - const startTime: number = Date.now(); + if(this.debugMode===true) + console.log('[RateManager] process: pre batch', { idealEntriesPerSecond, processingTimePerEntry, maxEntriesPerBatch, batchSize }); // Process each entry in the batch + const startTime: number = Date.now(); await Promise.all(batch.map(async (queueItem) => { let attempts = 0; let success = false; @@ -196,7 +222,9 @@ export class RateManager { // Calculate the delay required to maintain the burst rate const targetTimeForBatch = batchSize / this.config.burstRate; // Time we expect the batch to take const delayForRate = Math.max(0, (targetTimeForBatch - processingTime) * 1000); - // console.log('process: burst', { actualRate, delayForRate }); + + if(this.debugMode===true) + console.log('[RateManager] process: post batch', { actualRate, delayForRate }); // if we had an failed then we return these results if (failedMessages.length > 0) @@ -233,219 +261,68 @@ export class RateManager { //#endregion //#region METRICS - private updateMetrics(): void { - const currentTime: Date = new Date(); - const elapsedTime = (currentTime.getTime() - this.metrics.timeStart.getTime()) / 1000; // Convert ms to seconds - - const currentRate = this.metrics.counts.processed / elapsedTime; - - this.metrics.rateAverage = (this.metrics.rateAverage * (this.metrics.counts.processed - 1) + currentRate) / this.metrics.counts.processed; - - if (currentRate > this.metrics.rateMax) - this.metrics.rateMax = currentRate; - - this.metrics.timeProcessed = currentTime; - } - public getMetrics(): RateManagerMetrics { - return this.metrics; - } - //#endregion -} -/* -export class RateManager { - private isRunning: boolean = false; - private queue: Array <{ - entry: T, - resolve: (value: RateManagerResult) => void - // reject: (reason: RateManagerResult) => void - }> = []; - private isQueueLocked: boolean = false; - private debugMode: boolean = false; - private config: RateManagerConfig = { - targetRate: 200, - burstRate: 1000, - burstThreshold: 3000, - staggerLogs: true, - minInterval: 100, - onPost: async (entry: T) => { - console.log('[Logger] Unconfigured onPost',entry); - return { success: true, message: 'Unconfigured onPost', data: { ...entry } }; - } - }; - - constructor(cfg: RateManagerConfig) { - this.config = { - targetRate: cfg.targetRate ?? this.config.targetRate, - burstRate: cfg.burstRate ?? this.config.burstRate, - burstThreshold: cfg.burstThreshold ?? this.config.burstThreshold, - staggerLogs: cfg.staggerLogs ?? this.config.staggerLogs, - minInterval: cfg.minInterval ?? this.config.minInterval, - onPost: cfg.onPost ?? this.config.onPost, - }; - } - public isActive(): boolean { - return this.isRunning; - } - - //#region UTILS - private async delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); - } - //#endregion - - //#region QUEUE MANAGEMENT - private async processBatch(batchSize: number,delay: number): Promise { - - // single the queue is working and we should receive another batch - // this should be handled by the interval, but a safety check to avoid - // breaking FIFO. - if(this.isQueueLocked===true) - return; - - // make sure we don't grab more entries than we have - const entriesToGrab: number = (batchSize > this.queue.length) ? this.queue.length : batchSize; - if(entriesToGrab===0) + private async trackRateMetrics(interval: number, maxSamples: number): Promise { + // updating our metrics runs in the background at some interval that makes sense for the system + // if already running, bail + if(this.isMetricsRunning===true) return; + this.isMetricsRunning = true; - // grab entries from the start of the array to process - this.isQueueLocked = true; - const entries = this.queue.splice(0,entriesToGrab); - - // cycle through entries processing each with an optional delay to spread things out - let startTime: number = 0; - let ellapsedTime: number = 0; - for(const entry of entries) { - // send the log statement to Watson and wait since we want to control - // the flow of logs to watson and ensure a FIFO process - startTime = new Date().getTime(); - if(this.config.onPost) - entry.resolve(await this.config.onPost(entry.entry)); - - // if we want to spread out the requests, do so - if(delay>0) { - ellapsedTime = new Date().getTime() - startTime; - - // if the ellapsed time is less than delay - let waitTime: number = delay; - if(ellapsedTimedelay) { - continue; // already took too long. just keep moving - } else - this.delay(waitTime); - } - } - - this.isQueueLocked = false; - } - public startRateManager(): RateManagerResult { - const { targetRate, burstRate, burstThreshold, minInterval } = this.config; - let interval: number; - let batchSize: number; - let currentRate: number; - - // handle undefined - if(!targetRate || !burstRate || !burstThreshold || !minInterval) - return { success: false, message: 'failed to start. missing configuration', data: { targetRate, burstRate, burstThreshold, minInterval } }; - - // utility function for recalculating how fast we should be sending logs - const adjustIntervalAndBatchSize = () => { - // Determine if we're in burst mode or normal mode - if (this.queue.length > burstThreshold) { - currentRate = burstRate; // Burst mode - } else { - currentRate = targetRate; // Normal mode - } + // make sure our interval and samples are valid + interval = Math.max(interval, 1000); + maxSamples = Math.max(maxSamples,5); - // Calculate initial batch size and interval - batchSize = Math.ceil(currentRate / 10); // Base batch size calculation - interval = Math.floor(1000 / (currentRate / batchSize)); // Base interval calculation + // fixed interval duration + const elapsedSeconds: number = interval/1000; - // Adjust the interval to respect the minimum interval - if (interval < minInterval) { - interval = minInterval; - - // Recalculate batch size to maintain the desired currentRate while ensuring interval >= 100ms - batchSize = Math.ceil(currentRate / (1000 / interval)); - } - - // display interval details if in debug mode - // TODO: send through to logger system/output - if(this.debugMode) - console.log(`\t Interval update (mode: ${currentRate === burstRate ? 'Burst' : 'Normal'} | batch: ${batchSize} | interval: ${interval} ms)`); + // storage of our rates + const previousRates: number[] = []; + const lastSample = { + timestamp: new Date(), + startSize: this.metrics.counts.processed }; - // our main loop for the rate manager - const rateManagerLoop = async () => { - while (this.isRunning===true) { - if (this.queue.length === 0) { - if(this.debugMode) - console.log('\tQueue is empty, waiting for logs...'); + while(this.isMetricsRunning===true) { + const currentSize: number = this.metrics.counts.processed; + const currentDiff: number = currentSize - lastSample.startSize; - await this.delay(5000); - continue; - } + // Ensure no divide-by-zero and that count is greater than last sample + // if there is no difference then we ignore storing it so we don't skew + // our average with idle time. (note: the average is for throughput vs. usage) + if (currentSize - lastSample.startSize > 0) { + const newLogRate: number = currentDiff / elapsedSeconds; - // Adjust interval and batch size - adjustIntervalAndBatchSize(); + // see if we have a new maximum and assign the rate + this.metrics.rates.max = Math.max(this.metrics.rates.current,newLogRate); + this.metrics.rates.current = newLogRate; - // figure out if we want a delay between each log sent to lower chance of - // overflow with larger batch sizes. - const logDelay: number = (this.config.staggerLogs) ? (interval / batchSize) : 0; + // Track the rate to calculate rolling average + previousRates.push(this.metrics.rates.current); - // process our batch - await this.processBatch(batchSize, logDelay); + // Maintain rolling average window size + // Remove the oldest rate to keep the window size constant + if(previousRates.length > maxSamples) + previousRates.shift(); - // Wait for the next iteration - await this.delay(interval); - } - }; + // Calculate rolling average + const totalLogRate = previousRates.reduce((sum, rate) => sum + rate, 0); + this.metrics.rates.average = totalLogRate / previousRates.length; - // Start the rate manager loop if not already running - if(this.isRunning===false) { - this.isRunning = true; - rateManagerLoop().catch(err => console.error(err)); - } + } - return { success: true, message: 'started rate manager' }; - } - public stopRateManager(): RateManagerResult { - this.isRunning = false; - return { success: true, message: 'stopped rate manager' }; - } - public add(entry: T): Promise { - return new Promise((resolve) => { - this.queue.push({ entry, resolve }); - // return { success: true, message: 'added to queue' }; + lastSample.timestamp = new Date(); + lastSample.startSize = currentSize; - // TODO: start process queue... - }); - } - //#endregion + if(this.debugMode===true) + console.log(`[RateManager] metrics update: (${this.metrics.rates.current} log/s | avg: ${this.metrics.rates.average})`); - //#region CONFIG - public setConfig(cfg: RateManagerConfig): RateManagerResult { - this.config = { - targetRate: cfg.targetRate ?? this.config.targetRate, - burstRate: cfg.burstRate ?? this.config.burstRate, - burstThreshold: cfg.burstThreshold ?? this.config.burstThreshold, - staggerLogs: cfg.staggerLogs ?? this.config.staggerLogs, - minInterval: cfg.minInterval ?? this.config.minInterval, - onPost: cfg.onPost ?? this.config.onPost, - }; - - // if our rate manager is already running then we need to restart it so it gets - // the current updated values. - if(this.isRunning===true) { - this.stopRateManager(); - this.startRateManager(); + await this.delay(interval); } - return { success: true, message: 'updated configuration', data: this.config }; + this.isMetricsRunning = false; } - public getConfig(): RateManagerConfig { - return this.config; + public getMetrics(): RateManagerMetrics { + return this.metrics; } //#endregion -} - */ \ No newline at end of file +} \ No newline at end of file From ec7bdfccb73437255228b3a2eedb7b2b7912f940 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Thu, 10 Oct 2024 11:42:46 -0400 Subject: [PATCH 08/12] Email image fix and imrpoved metric calculations (new) optional base64 images for Emails. defaults to URL (fix) rate calculations for metrics --- server/http/routes/sandbox.ts | 6 +++--- server/records/logger/log.ts | 2 +- server/records/notify/notifyEmail.ts | 31 +++++++++++++++------------- server/records/utils/rateManager.ts | 8 +++---- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/server/http/routes/sandbox.ts b/server/http/routes/sandbox.ts index 5bec7ace..7837ccdf 100644 --- a/server/http/routes/sandbox.ts +++ b/server/http/routes/sandbox.ts @@ -7,11 +7,11 @@ export const play = async (_req: Request, res: Response): Promise => { await RK.configure(); // test our logging - const numLogs: number = 1000; - const result = await RK.logTest(numLogs); + // const numLogs: number = 1000; + // const result = await RK.logTest(numLogs); // test email notifications - // const result = await RK.emailTest(); + const result = await RK.emailTest(); // return our results res.status(200).send(H.Helpers.JSONStringify(result)); diff --git a/server/records/logger/log.ts b/server/records/logger/log.ts index 05a35885..16972b90 100644 --- a/server/records/logger/log.ts +++ b/server/records/logger/log.ts @@ -717,7 +717,7 @@ export class Logger { // close our profiler and return results const result = await Logger.profileEnd(profileKey); - return { success: true, message: `finished testing ${numLogs} logs. (time: ${result.message} | maxRate: ${Logger.stats.metrics.logRateMax})` }; + return { success: true, message: `finished testing ${numLogs} logs. (time: ${result.message} | maxRate: ${Logger.stats.metrics.logRateMax} | avgRate: ${Logger.rateManager?.getMetrics().rates.average ?? -1})` }; } //#endregion } diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index 2fc844fb..3ecbcc2f 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -1,5 +1,5 @@ import * as NET from 'net'; -import { NotifyPackage, NotifyType, getMessagePrefixByType } from './notifyShared'; +import { NotifyPackage, NotifyType, getMessagePrefixByType, getMessageIconUrlByType } from './notifyShared'; import * as UTIL from '../utils/utils'; import { RateManager, RateManagerConfig, RateManagerResult } from '../utils/rateManager'; @@ -26,6 +26,7 @@ export class NotifyEmail { private static rateManager: RateManager | null = null; private static environment: 'prod' | 'dev' = 'dev'; private static fromAddress: string = ''; + private static useBase64: boolean = false; public static isActive(): boolean { // we're initialized if we have a logger running @@ -167,8 +168,9 @@ export class NotifyEmail { result += '
'; // banner + const imageRef: string = (NotifyEmail.useBase64===true) ? 'cid:0123456789' : getMessageIconUrlByType(params.type); result += '
'; - result += ''; // image references specific attachment by CID + result += ``; // image references specific attachment by CID result += '
'; // header and subtitle @@ -208,7 +210,6 @@ export class NotifyEmail { // close html result += ''; - return result; } //#endregion @@ -261,17 +262,19 @@ export class NotifyEmail { // attachments // NOTE: we need to put all images as attachments and then reference by CID // for compatability since GMail removes any base64 embedded images - client.write(`--${boundary}\r\n`); - client.write('Content-Type: image/png; name="header.png"\r\n'); - client.write('Content-Disposition: inline; filename="header.png"\r\n'); - client.write('Content-Transfer-Encoding: base64\r\n'); - client.write('Content-ID: <0123456789>\r\n'); - client.write('Content-Location: header.png\r\n'); - client.write('\r\n'); + if(NotifyEmail.useBase64===true) { + client.write(`--${boundary}\r\n`); + client.write('Content-Type: image/png; name="header.png"\r\n'); + client.write('Content-Disposition: inline; filename="header.png"\r\n'); + client.write('Content-Transfer-Encoding: base64\r\n'); + client.write('Content-ID: <0123456789>\r\n'); + client.write('Content-Location: header.png\r\n'); + client.write('\r\n'); - // add our base64 icon from the type - const base64Icon: string = NotifyEmail.getMessageIconBase64(entry.type); - client.write(`${base64Icon}\r\n`); + // add our base64 icon from the type + const base64Icon: string = NotifyEmail.getMessageIconBase64(entry.type); + client.write(`${base64Icon}\r\n`); + } // End of message client.write(`--${boundary}--\r\n`); @@ -335,7 +338,7 @@ export class NotifyEmail { if(NotifyEmail.rateManager) return NotifyEmail.rateManager.add(entry); else - return this.postMessage(entry); + return NotifyEmail.postMessage(entry); } public static async sendMessage(params: NotifyPackage): Promise { diff --git a/server/records/utils/rateManager.ts b/server/records/utils/rateManager.ts index cb48485b..8862d552 100644 --- a/server/records/utils/rateManager.ts +++ b/server/records/utils/rateManager.ts @@ -53,7 +53,7 @@ export class RateManager { burstRate: cfg.burstRate ?? 50, burstThreshold: cfg.burstThreshold ?? 250, minInterval: cfg.minInterval ?? 1000, - metricsInterval: cfg.metricsInterval ?? 5000, + metricsInterval: cfg.metricsInterval ?? 1000, onPost: cfg.onPost ?? (async (entry: T) => { console.log('[RateManager] Unconfigured onPost',entry); return { success: true, message: 'Unconfigured onPost', data: { ...entry } }; @@ -78,6 +78,7 @@ export class RateManager { // set our mode. the manager starts automatically when first entry is received this.mode = 'standard'; + this.trackRateMetrics(this.config.metricsInterval,5); } //#region PUBLIC @@ -293,7 +294,7 @@ export class RateManager { const newLogRate: number = currentDiff / elapsedSeconds; // see if we have a new maximum and assign the rate - this.metrics.rates.max = Math.max(this.metrics.rates.current,newLogRate); + this.metrics.rates.max = Math.max(this.metrics.rates.max,newLogRate); this.metrics.rates.current = newLogRate; // Track the rate to calculate rolling average @@ -307,14 +308,13 @@ export class RateManager { // Calculate rolling average const totalLogRate = previousRates.reduce((sum, rate) => sum + rate, 0); this.metrics.rates.average = totalLogRate / previousRates.length; - } lastSample.timestamp = new Date(); lastSample.startSize = currentSize; if(this.debugMode===true) - console.log(`[RateManager] metrics update: (${this.metrics.rates.current} log/s | avg: ${this.metrics.rates.average})`); + console.log(`[RateManager] metrics update: (${currentDiff} entries | current: ${this.metrics.rates.current}/s | avg: ${this.metrics.rates.average}/s | max: ${this.metrics.rates.max}/s)`); await this.delay(interval); } From daf7c6befeacdc7d95a50f1359e9d7798713bc82 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Thu, 10 Oct 2024 13:25:11 -0400 Subject: [PATCH 09/12] Email testing routines (new) testEmail in NotifyEmail and RecordKeeper --- server/http/routes/sandbox.ts | 2 +- server/records/notify/notify.ts | 3 + server/records/notify/notifyEmail.ts | 121 +++++++++++++++++++++++++++ server/records/recordKeeper.ts | 16 +--- 4 files changed, 127 insertions(+), 15 deletions(-) diff --git a/server/http/routes/sandbox.ts b/server/http/routes/sandbox.ts index 7837ccdf..f5635f1d 100644 --- a/server/http/routes/sandbox.ts +++ b/server/http/routes/sandbox.ts @@ -11,7 +11,7 @@ export const play = async (_req: Request, res: Response): Promise => { // const result = await RK.logTest(numLogs); // test email notifications - const result = await RK.emailTest(); + const result = await RK.emailTest(5); // return our results res.status(200).send(H.Helpers.JSONStringify(result)); diff --git a/server/records/notify/notify.ts b/server/records/notify/notify.ts index ab72c3f0..698fc907 100644 --- a/server/records/notify/notify.ts +++ b/server/records/notify/notify.ts @@ -12,6 +12,9 @@ export class Notify { // cast the returns to NotifyResult so it's consistent with what is exported public static sendEmailMessage = NotifyEmail.sendMessage as (params: NotifyPackage) => Promise; public static sendEmailMessageRaw = NotifyEmail.sendMessageRaw as (type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string) => Promise; + + // testing emails + public static testEmail = NotifyEmail.testEmails as (numEmails: number) => Promise; //#endregion //#region SLACK diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index 3ecbcc2f..86570631 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -2,6 +2,7 @@ import * as NET from 'net'; import { NotifyPackage, NotifyType, getMessagePrefixByType, getMessageIconUrlByType } from './notifyShared'; import * as UTIL from '../utils/utils'; import { RateManager, RateManagerConfig, RateManagerResult } from '../utils/rateManager'; +import { Logger as LOG, LogSection } from '../logger/log'; /** * - get error/success messages out allowing caller to wait for results @@ -360,4 +361,124 @@ export class NotifyEmail { } } //#endregion + + //#region TESTING + private static randomNotifyPackage(index: number): NotifyPackage { + + const getRandomInt= (min: number, max: number): number => { + return Math.floor(Math.random() * (max - min + 1)) + min; + }; + + // predefined options for each type + const messages = { + [NotifyType.SYSTEM_ERROR]: [ + 'A critical system error has occurred. Immediate attention required.', + 'System failure detected in the main service pipeline.', + 'Unexpected system downtime. Engineers have been notified.', + 'Database connection failure. Systems are currently offline.', + 'Critical resource unavailable. Check service status immediately.' + ], + [NotifyType.SYSTEM_NOTICE]: [ + 'System maintenance is scheduled for tomorrow at 3 AM.', + 'System updates will be applied this weekend. Expect downtime.', + 'Routine server checks are complete. All systems are operational.', + 'A new feature has been deployed successfully. No action required.', + 'Configuration changes have been made. Please verify system performance.' + ], + [NotifyType.JOB_FAILED]: [ + 'The job you submitted has failed due to a processing error.', + 'Job processing could not be completed. Please check inputs.', + 'Job failure detected during data validation.', + 'Your job did not complete successfully. Logs have been updated.', + 'Unexpected failure during job execution. Please retry.' + ], + [NotifyType.JOB_PASSED]: [ + 'Your job has successfully completed.', + 'Job execution finished without errors. Data is ready for review.', + 'Processing complete. Results have been stored and are available.', + 'The job you submitted completed successfully. No further action needed.', + 'All tasks finished without issues. You can access the output.' + ], + [NotifyType.JOB_STARTED]: [ + 'A new job has started processing.', + 'Job execution has begun. You will be notified when it completes.', + 'Your job is now in progress.', + 'The system has started processing your job. Please wait for further updates.', + 'Processing of the submitted job has commenced.' + ], + [NotifyType.SECURITY_NOTICE]: [ + 'A security alert has been triggered on your account.', + 'Unusual activity detected. Please verify your login credentials.', + 'Security protocols have been activated due to suspicious access.', + 'System lockdown initiated due to a security breach.', + 'Unauthorized login attempt detected. Immediate action required.' + ] + }; + const details = [ + { url: 'https://example.com/job-details', label: 'Job Details' }, + { url: 'https://example.com/error-log', label: 'Error Log' }, + { url: 'https://example.com/security-alert', label: 'Security Alert' }, + { url: 'https://example.com/system-status', label: 'System Status' } + ]; + const detailsMessages = [ + 'Check the logs for further details.', + 'Please contact support if you need further assistance.', + 'More information can be found in the error log.', + 'Refer to the system documentation for troubleshooting.', + 'Visit the provided link for additional details.', + 'The issue has been escalated to the dev team.', + 'System updates will follow shortly. Stay tuned.' + ]; + const types = [ + NotifyType.SYSTEM_ERROR, + NotifyType.SYSTEM_NOTICE, + NotifyType.JOB_FAILED, + NotifyType.JOB_PASSED, + NotifyType.JOB_STARTED, + NotifyType.SECURITY_NOTICE + ]; + + // our date range for the entry + const notifyType = types[getRandomInt(0, types.length - 1)]; + const currentDate = new Date(); + const startDate = new Date(currentDate.getTime() - getRandomInt(1000000, 100000000)); // random past start date + const endDate = Math.random() > 0.5 ? new Date(startDate.getTime() + getRandomInt(1000000, 100000000)) : undefined; + + // structure our return + return { + type: notifyType, + message: `${index}: ${messages[notifyType][getRandomInt(0, 4)]}`, + startDate, + endDate, + detailsLink: Math.random() > 0.5 ? details[getRandomInt(0, details.length - 1)] : undefined, + detailsMessage: detailsMessages[getRandomInt(0, detailsMessages.length - 1)], + sendTo: ['emaslowski@quotient-inc.com'] + }; + } + public static async testEmails(numEmails: number): Promise { + + const hasRateManager: boolean = (NotifyEmail.rateManager) ? (NotifyEmail.rateManager!=null) : false; + const config: RateManagerConfig | null = NotifyEmail.rateManager?.getConfig() ?? null; + + // create our profiler + // we use a random string in case another test or profile is run to avoid collisisons + const profileKey: string = `EmailTest_${Math.random().toString(36).substring(2, 6)}`; + await LOG.profile(profileKey, LogSection.eSYS, `Email test: ${new Date().toLocaleString()}`, { + numEmails, + rateManager: hasRateManager, + ...(hasRateManager === true && config && { + config: (({ onPost: _onPost, ...rest }) => rest)(config) // Exclude onPost + }) + },'NotifyEmail.test'); + + // test our emails + for(let i=0; i { - - // this.sendEmail(NotifyType.SYSTEM_ERROR, NotifyChannel.EMAIL_ADMIN, 'Packrat out of disk space!', 'Packrat is running low with only 6% of disk space available. If you have pending items uploaded but not ingested please do so. '); - // this.sendEmail(NotifyType.SYSTEM_NOTICE, NotifyChannel.EMAIL_ADMIN, 'Packrat maintenance @ 11pm', 'Packrat will be offline starting at 11pm tonight'); - // this.sendEmail(NotifyType.JOB_PASSED, NotifyChannel.EMAIL_ADMIN, 'Model ingestion completed', 'Model "Something awesome" was ingested...'); - // this.sendEmail(NotifyType.JOB_STARTED, NotifyChannel.EMAIL_ADMIN, 'Uploading capture data started', 'Uploading capture data comprised of 456 images for subject "something awesome"...'); - const result = await this.sendEmail(NotifyType.JOB_FAILED, NotifyChannel.EMAIL_ADMIN, 'Scene generation failed for: Something awesome', 'Voyager scene failed to be created because: no normals...'); - - if(result.success===true) - this.logInfo(LogSection.eTEST,result.message ?? 'NA', undefined, 'RecordKeeper.emailTest',false); - else - this.logError(LogSection.eTEST,result.message ?? 'Unknown error', undefined, 'RecordKeeper.emailTest'); - - return result; + static async emailTest(numEmails: number): Promise { + return NOTIFY.testEmail(numEmails); } // slack From 85d8723268597d1a3d39bba1d4dcf677c766a6d7 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Fri, 11 Oct 2024 10:34:18 -0400 Subject: [PATCH 10/12] (fix) comment cleanup --- server/records/notify/notifyEmail.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index 86570631..68216f30 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -4,12 +4,6 @@ import * as UTIL from '../utils/utils'; import { RateManager, RateManagerConfig, RateManagerResult } from '../utils/rateManager'; import { Logger as LOG, LogSection } from '../logger/log'; -/** - * - get error/success messages out allowing caller to wait for results - * - test routines - * - stats (rate, type counts) - */ - // declaring this empty for branding/clarity since it is used // for instances that are not related to the RateManager // eslint-disable-next-line @typescript-eslint/no-empty-interface From 89ee6f9729ebe296db156079f66b2f2d0f798557 Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Fri, 11 Oct 2024 11:02:17 -0400 Subject: [PATCH 11/12] Adjusted 'From' addresses (fix) using packrat emails: packrat-dev & packrat-noreply (fix) override email address for generic email sending --- server/records/notify/notifyEmail.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index 68216f30..f0a96072 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -11,6 +11,7 @@ export interface EmailResult extends RateManagerResult {} interface EmailEntry { type: NotifyType, + from: string, sendTo: string[], subject: string, textBody: string, @@ -30,7 +31,7 @@ export class NotifyEmail { public static configure(env: 'prod' | 'dev', targetRate?: number, burstRate?: number, burstThreshold?: number): EmailResult { // we allow for re-assigning configuration options even if already running NotifyEmail.environment = env; - NotifyEmail.fromAddress = (NotifyEmail.environment==='dev') ? 'packrat@si.edu' : 'packrat@si.edu'; // TODO: 'packrat-dev@si.edu' : 'packrat-noreply@si.edu'; + NotifyEmail.fromAddress = (NotifyEmail.environment==='dev') ? 'packrat-dev@si.edu' : 'packrat-noreply@si.edu'; // if we want a rate limiter then we build it const rmConfig: RateManagerConfig = { @@ -46,14 +47,6 @@ export class NotifyEmail { else NotifyEmail.rateManager = new RateManager(rmConfig); - // if we already configured skip creating another one - if(NotifyEmail.isActive()===true) - return { success: true, message: 'email system already running' }; - - // start our rate manager if needed - // if(NotifyEmail.rateManager) - // NotifyEmail.rateManager.startRateManager(); - return { success: true, message: 'configured email notifier.' }; } @@ -217,7 +210,7 @@ export class NotifyEmail { const smtpHost: string = 'smtp.si.edu'; const smtpPort: number = 25; - const from: string = NotifyEmail.fromAddress; + const from: string = entry.from; const boundary: string = '----=_Packrat_Ops_Msg_001'; return new Promise((resolve) => { try { @@ -315,7 +308,7 @@ export class NotifyEmail { } }); } - public static async sendMessageRaw(type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string): Promise { + public static async sendMessageRaw(type: NotifyType, sendTo: string[], subject: string, textBody: string, htmlBody?: string, from?: string): Promise { // get our email addresses if needed if(sendTo.length<=0) @@ -323,6 +316,7 @@ export class NotifyEmail { const entry: EmailEntry = { type, + from: from ?? NotifyEmail.fromAddress, sendTo, subject, textBody, @@ -335,7 +329,7 @@ export class NotifyEmail { else return NotifyEmail.postMessage(entry); } - public static async sendMessage(params: NotifyPackage): Promise { + public static async sendMessage(params: NotifyPackage, from?: string): Promise { // if we have sendTo address(es) then we ignore the channel if(!params.sendTo) @@ -349,7 +343,7 @@ export class NotifyEmail { // figure out our subject and send to raw output // returning the promise so it can be waited on (if needed) const subject: string = `[${getMessagePrefixByType(params.type)}] ${params.message}`; - return NotifyEmail.sendMessageRaw(params.type,params.sendTo,subject,textBody,htmlBody); + return NotifyEmail.sendMessageRaw(params.type,params.sendTo,subject,textBody,htmlBody,from); } catch (error) { return { success: false, message: 'failed to send message', data: { error: UTIL.getErrorString(error) } }; } From be8bec1f6e5beabf8e1b67d0c575b79053fd030d Mon Sep 17 00:00:00 2001 From: Eric Maslowski Date: Fri, 11 Oct 2024 11:28:18 -0400 Subject: [PATCH 12/12] Email image URLS (new) generic naming and routine for getting icon URLs --- server/records/notify/notifyEmail.ts | 2 +- server/records/notify/notifyShared.ts | 27 ++++++--------------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/server/records/notify/notifyEmail.ts b/server/records/notify/notifyEmail.ts index f0a96072..e55cdf53 100644 --- a/server/records/notify/notifyEmail.ts +++ b/server/records/notify/notifyEmail.ts @@ -156,7 +156,7 @@ export class NotifyEmail { result += '
'; // banner - const imageRef: string = (NotifyEmail.useBase64===true) ? 'cid:0123456789' : getMessageIconUrlByType(params.type); + const imageRef: string = (NotifyEmail.useBase64===true) ? 'cid:0123456789' : getMessageIconUrlByType(params.type,'email'); result += '
'; result += ``; // image references specific attachment by CID result += '
'; diff --git a/server/records/notify/notifyShared.ts b/server/records/notify/notifyShared.ts index b81a7b23..ad126d78 100644 --- a/server/records/notify/notifyShared.ts +++ b/server/records/notify/notifyShared.ts @@ -30,27 +30,12 @@ export interface NotifyPackage { sendTo?: string[], // where are we sending the message. for Slack it is a user slack ID. for email it is the 'to' email address } -export const getMessageIconUrlByType = (type: NotifyType): string => { - - switch(type) { - case NotifyType.SYSTEM_ERROR: - return 'https://egofarms.com/packrat/fire-solid.png'; - case NotifyType.SYSTEM_NOTICE: - return 'https://egofarms.com/packrat/alarm-on.png'; - - case NotifyType.JOB_FAILED: - return 'https://egofarms.com/packrat/attack.png'; - case NotifyType.JOB_PASSED: - return 'https://egofarms.com/packrat/award-ribbon.png'; - case NotifyType.JOB_STARTED: - return 'https://egofarms.com/packrat/coffee.png'; - - case NotifyType.SECURITY_NOTICE: - return 'https://egofarms.com/packrat/privacy-shield-solid.png'; - - default: - return 'https://egofarms.com/packrat/gear-solid.png'; - } +export const getMessageIconUrlByType = (type: NotifyType, system: 'email' | 'slack'): string => { + // build our filename based on the type + const host: string = 'https://egofarms.com/packrat'; + const typeStr: string = NotifyType[type]; + const filename: string = `${host}/icon_${system}_${typeStr.toLowerCase()}.png`; + return filename; }; export const getMessagePrefixByType = (type: NotifyType): string => {