From 80c0e1ec301a07f6d31861d78bfb81a601be46f8 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Tue, 2 Sep 2025 17:56:00 -0500 Subject: [PATCH] fix(validator): corrections to no leak like crazy --- app/src/main.ts | 65 ++++++++++++++++----------- app/src/pkg/db/db.ts | 3 +- app/src/pkg/db/schema/logs.ts | 11 ++++- app/src/pkg/logger/dbTransport.ts | 25 +++++++---- app/src/pkg/logger/logger.ts | 1 - app/src/pkg/logger/notification.ts | 55 +++-------------------- app/src/pkg/prodSql/prodQuery.ts | 3 +- app/src/pkg/prodSql/prodSqlConfig.ts | 4 +- app/src/pkg/prodSql/prodSqlConnect.ts | 4 +- app/src/pkg/utils/envValidator.ts | 58 +++++++++--------------- 10 files changed, 102 insertions(+), 127 deletions(-) diff --git a/app/src/main.ts b/app/src/main.ts index 8b6a3ff..3440f07 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -7,15 +7,17 @@ import path, { dirname, join } from "path"; import { fileURLToPath } from "url"; import { db } from "./pkg/db/db.js"; import { settings, type Setting } from "./pkg/db/schema/settings.js"; -import { env } from "./pkg/utils/envValidator.js"; +import { validateEnv } from "./pkg/utils/envValidator.js"; import { createLogger } from "./pkg/logger/logger.js"; import { returnFunc } from "./pkg/utils/return.js"; -import { initializeProdPool } from "./pkg/prodSql/prodSqlConnect.js"; +import { closePool, initializeProdPool } from "./pkg/prodSql/prodSqlConnect.js"; import { tryCatch } from "./pkg/utils/tryCatch.js"; - -const PORT = Number(env.VITE_PORT) || 4200; +import os, { hostname } from "os"; +import { sendNotify } from "./pkg/utils/notify.js"; const main = async () => { + const env = validateEnv(process.env); + const PORT = Number(env.VITE_PORT) || 4200; //create the logger const log = createLogger({ module: "system", subModule: "main start" }); @@ -94,28 +96,16 @@ const main = async () => { // start the server up server.listen(PORT, "0.0.0.0", () => log.info( + { stack: { name: "test" } }, `Server running in ${ process.env.NODE_ENV ? process.env.NODE_ENV : "dev" }, on http://0.0.0.0:${PORT}${basePath}` ) ); - // Handle app exit signals - process.on("SIGINT", async () => { - console.log("\nGracefully shutting down..."); - //await closePool(); - process.exit(0); - }); - - process.on("SIGTERM", async () => { - console.log("Received termination signal, closing database..."); - //await closePool(); - process.exit(0); - }); - process.on("uncaughtException", async (err) => { - console.log("Uncaught Exception:", err); - //await closePool(); + //console.log("Uncaught Exception:", err); + // await closePool(); // const emailData = { // email: "blake.matthes@alpla.com", // should be moved to the db so it can be reused. // subject: `${os.hostname()} has just encountered a crash.`, @@ -126,15 +116,40 @@ const main = async () => { // }, // }; - // await sendEmail(emailData); + if (!process.env.WEBHOOK_URL) { + // await sendEmail(emailData); + } else { + await sendNotify({ + module: "system", + subModule: "fatalCrash", + hostname: os.hostname(), + message: err.message, + stack: err?.stack, + }); + } + process.exit(1); }); - process.on("beforeExit", async () => { - console.log("Process is about to exit..."); - //await closePool(); - process.exit(0); - }); + // setInterval(() => { + // const used = process.memoryUsage(); + // console.log( + // `Heap: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB / RSS: ${( + // used.rss / + // 1024 / + // 1024 + // ).toFixed(2)} MB` + // ); + // }, 10000); }; main(); + +// .catch((err) => { +// const log = createLogger({ module: "system", subModule: "main" }); +// log.fatal( +// { notify: true }, +// "There was a crash that occured and caused the app to restart." +// ); +// process.exit(1); +// }); diff --git a/app/src/pkg/db/db.ts b/app/src/pkg/db/db.ts index bae75cb..ec304c1 100644 --- a/app/src/pkg/db/db.ts +++ b/app/src/pkg/db/db.ts @@ -1,7 +1,8 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; -import { env } from "../utils/envValidator.js"; +import { validateEnv } from "../utils/envValidator.js"; +const env = validateEnv(process.env); const dbURL = `postgres://${env.DATABASE_USER}:${env.DATABASE_PASSWORD}@${env.DATABASE_HOST}:${env.DATABASE_PORT}/${env.DATABASE_DB}`; const queryClient = postgres(dbURL, { diff --git a/app/src/pkg/db/schema/logs.ts b/app/src/pkg/db/schema/logs.ts index 4a3ed54..fac7808 100644 --- a/app/src/pkg/db/schema/logs.ts +++ b/app/src/pkg/db/schema/logs.ts @@ -1,4 +1,11 @@ -import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { z } from "zod"; @@ -8,7 +15,7 @@ export const logs = pgTable("logs", { module: text("module").notNull(), subModule: text("subModule"), message: text("message").notNull(), - stack: text("stack"), + stack: jsonb("stack").default([]), checked: boolean("checked").default(false), hostname: text("hostname"), createdAt: timestamp("createdAt").defaultNow(), diff --git a/app/src/pkg/logger/dbTransport.ts b/app/src/pkg/logger/dbTransport.ts index 0cb3cc1..59dd4dd 100644 --- a/app/src/pkg/logger/dbTransport.ts +++ b/app/src/pkg/logger/dbTransport.ts @@ -1,7 +1,7 @@ import build from "pino-abstract-transport"; import { db } from "../db/db.js"; import { logs, type Log } from "../db/schema/logs.js"; -import { checkENV } from "../utils/envValidator.js"; +import { tryCatch } from "../utils/tryCatch.js"; const pinoLogLevels: any = { 10: "trace", @@ -19,14 +19,21 @@ export default async function (log: Log) { for await (let obj of source) { // convert to the name to make it more easy to find later :P const levelName = pinoLogLevels[obj.level] || "unknown"; - await db.insert(logs).values({ - level: levelName, - module: obj?.module.toLowerCase(), - subModule: obj?.subModule.toLowerCase(), - hostname: obj?.hostname.toLowerCase(), - message: obj.msg, - stack: obj?.stack, - }); + + const res = await tryCatch( + db.insert(logs).values({ + level: levelName, + module: obj?.module?.toLowerCase(), + subModule: obj?.subModule?.toLowerCase(), + hostname: obj?.hostname?.toLowerCase(), + message: obj.msg, + stack: obj?.stack, + }) + ); + + if (res.error) { + console.log(res.error); + } } }); } catch (err) { diff --git a/app/src/pkg/logger/logger.ts b/app/src/pkg/logger/logger.ts index 01044e9..b5eb43e 100644 --- a/app/src/pkg/logger/logger.ts +++ b/app/src/pkg/logger/logger.ts @@ -1,5 +1,4 @@ import pino, { type Logger } from "pino"; -import { env } from "../utils/envValidator.js"; export let logLevel = process.env.LOG_LEVEL || "info"; diff --git a/app/src/pkg/logger/notification.ts b/app/src/pkg/logger/notification.ts index 7a8e314..6fae311 100644 --- a/app/src/pkg/logger/notification.ts +++ b/app/src/pkg/logger/notification.ts @@ -1,7 +1,8 @@ import build from "pino-abstract-transport"; -import { db } from "../db/db.js"; -import { logs, type Log } from "../db/schema/logs.js"; -import { env } from "../utils/envValidator.js"; +import { type Log } from "../db/schema/logs.js"; +import { validateEnv } from "../utils/envValidator.js"; +import { sendNotify } from "../utils/notify.js"; +const env = validateEnv(process.env); const pinoLogLevels: any = { 10: "trace", @@ -12,49 +13,6 @@ const pinoLogLevels: any = { 60: "fatal", }; // discord function -async function sendFatal(log: Log) { - const webhookUrl = process.env.WEBHOOK_URL!; - let payload = { - embeds: [ - { - title: `🚨 ${env.PROD_PLANT_TOKEN}: encounter a critical error `, - description: `Where was the error: ${log.module}${ - log.subModule ? `-${log.subModule}` : "" - }`, - color: 0xff0000, // red - fields: [ - { - name: "Message", - value: log.message, - inline: false, - }, - { - name: "Hostname", - value: log.hostname, - inline: false, - }, - { - name: "Stack", - value: - "```" + - (log.stack?.slice(0, 1000) ?? "no stack") + - "```", - }, - ], - footer: { - text: "LST Logger 💀", - }, - timestamp: new Date().toISOString(), - }, - ], - }; - - await fetch(webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); -} export default async function (log: Log) { //const {username, service, level, msg, ...extra} = log; @@ -76,14 +34,15 @@ export default async function (log: Log) { ? String(obj.hostname).toLowerCase() : undefined, message: obj.msg, + stack: obj.stack ? obj.stack : undefined, }; if (!process.env.WEBHOOK_URL) { - console.log("webhook missing?"); + console.log("WebHook is missing we wont move foward."); return; } if (obj.level >= 60 && obj.notify) { - sendFatal(newlog as Log); + sendNotify(newlog as Log); } } }); diff --git a/app/src/pkg/prodSql/prodQuery.ts b/app/src/pkg/prodSql/prodQuery.ts index 8b1aeee..6980787 100644 --- a/app/src/pkg/prodSql/prodQuery.ts +++ b/app/src/pkg/prodSql/prodQuery.ts @@ -1,7 +1,8 @@ -import { env } from "../utils/envValidator.js"; import { returnFunc } from "../utils/return.js"; import { connected, pool } from "./prodSqlConnect.js"; +import { validateEnv } from "../utils/envValidator.js"; +const env = validateEnv(process.env); /** * Run a prod query * just pass over the query as a string and the name of the query. diff --git a/app/src/pkg/prodSql/prodSqlConfig.ts b/app/src/pkg/prodSql/prodSqlConfig.ts index 663e10d..379c688 100644 --- a/app/src/pkg/prodSql/prodSqlConfig.ts +++ b/app/src/pkg/prodSql/prodSqlConfig.ts @@ -1,5 +1,7 @@ import sql from "mssql"; -import { env } from "../utils/envValidator.js"; +import { validateEnv } from "../utils/envValidator.js"; + +const env = validateEnv(process.env); export const sqlConfig: sql.config = { server: env.PROD_SERVER, database: `AlplaPROD_${env.PROD_PLANT_TOKEN}_cus`, diff --git a/app/src/pkg/prodSql/prodSqlConnect.ts b/app/src/pkg/prodSql/prodSqlConnect.ts index 54fa8b5..bfc7827 100644 --- a/app/src/pkg/prodSql/prodSqlConnect.ts +++ b/app/src/pkg/prodSql/prodSqlConnect.ts @@ -1,9 +1,11 @@ import sql from "mssql"; import { checkHostnamePort } from "../utils/checkHostNamePort.js"; import { sqlConfig } from "./prodSqlConfig.js"; -import { env } from "../utils/envValidator.js"; import { createLogger } from "../logger/logger.js"; import { returnFunc } from "../utils/return.js"; +import { validateEnv } from "../utils/envValidator.js"; + +const env = validateEnv(process.env); export let pool: any; export let connected: boolean = false; diff --git a/app/src/pkg/utils/envValidator.ts b/app/src/pkg/utils/envValidator.ts index 6947d56..d4c911f 100644 --- a/app/src/pkg/utils/envValidator.ts +++ b/app/src/pkg/utils/envValidator.ts @@ -1,55 +1,37 @@ import { z } from "zod"; -import { createLogger } from "../logger/logger.js"; -/** - * This is where we will validate the required ENV parapmeters. - * - */ + const envSchema = z.object({ - //Server stuff + // server stuff VITE_PORT: z.string().default("4200"), LOG_LEVEL: z.string().default("info"), - // app db stuff + + // db stuff DATABASE_HOST: z.string(), DATABASE_PORT: z.string(), DATABASE_USER: z.string(), DATABASE_PASSWORD: z.string(), DATABASE_DB: z.string().default("lst"), - // prod server checks + + // prod stuff PROD_SERVER: z.string(), PROD_PLANT_TOKEN: z.string(), PROD_USER: z.string(), PROD_PASSWORD: z.string(), + + // docker specifc + RUNNING_IN_DOCKER: z.string().default("false"), }); -// use safeParse instead of parse -const parsed = envSchema.safeParse(process.env); +export type Env = z.infer; -export const checkENV = () => { - return envSchema.safeParse(process.env); -}; - -const log = createLogger({ module: "envValidation" }); - -if (!parsed.success) { - log.fatal( - `Environment validation failed: Missing: ${parsed.error.issues - .map((e) => { - return e.path[0]; - }) - .join(", ")}` - ); - // 🔔 Send a notification (e.g., email, webhook, Slack) - // sendNotification(parsed.error.format()); - - // gracefully exit if in production - //process.exit(1); - throw Error( - `Environment validation failed: Missing: ${parsed.error.issues - .map((e) => { - return e.path[0]; - }) - .join(", ")}` - ); +export function validateEnv(raw: NodeJS.ProcessEnv): Env { + const parsed = envSchema.safeParse(raw); + if (!parsed.success) { + throw new Error( + `Environment validation failed. Missing: ${parsed.error.issues + .map((e) => e.path[0]) + .join(", ")}` + ); + } + return parsed.data; } - -export const env = parsed.data;