Compare commits

..

3 Commits

17 changed files with 384 additions and 128 deletions

View File

@@ -25,5 +25,13 @@ PROD_PLANT_TOKEN=test3
PROD_USER=alplaprod
PROD_PASSWORD=password
# Docker Related
PROD_IP=10.193.0.56
LOGISTICS_NETWORK=10.193.14.0/24
LOGISTICS_GATEWAY=10.193.14.252
MLAN1_NETWORK=192.168.193.0/24
MLAN1_GATEWAY=192.168.193.252
# admin
WEBHOOK_URL

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ app/src/frontend
lstWrapper/bin
lstWrapper/obj
lstWrapper/publish
testScripts
# Logs
logs
*.log

View File

@@ -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);
// });

View File

@@ -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, {

View File

@@ -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(),

View File

@@ -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) {

View File

@@ -1,5 +1,4 @@
import pino, { type Logger } from "pino";
import { env } from "../utils/envValidator.js";
export let logLevel = process.env.LOG_LEVEL || "info";

View File

@@ -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);
}
}
});

View File

@@ -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.

View File

@@ -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`,

View File

@@ -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;

View File

@@ -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<typeof envSchema>;
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;

View File

@@ -0,0 +1,45 @@
import type { Log } from "../db/schema/logs.js";
import { validateEnv } from "../utils/envValidator.js";
const env = validateEnv(process.env);
export async function sendNotify(log: any) {
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 ?? "no stack") + "```",
},
],
footer: {
text: "LST Logger 💀",
},
timestamp: new Date().toISOString(),
},
],
};
await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
}

View File

@@ -8,7 +8,7 @@ services:
- "${VITE_PORT:-4200}:4200"
environment:
- NODE_ENV=devolpment
- DATABASE_HOST=${DATABASE_HOST}
- DATABASE_HOST=host.docker.internal
- DATABASE_PORT=${DATABASE_PORT}
- DATABASE_USER=${DATABASE_USER}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
@@ -18,3 +18,29 @@ services:
- PROD_USER=${PROD_USER}
- PROD_PASSWORD=${PROD_PASSWORD}
restart: unless-stopped
# for all host including prod servers, plc's, printers, or other de
extra_hosts:
- "${PROD_SERVER}:${PROD_IP}"
networks:
- default
- logisticsNetwork
- mlan1
networks:
logisticsNetwork:
driver: macvlan
driver_opts:
parent: eth0
ipam:
config:
- subnet: ${LOGISTICS_NETWORK}
gateway: ${LOGISTICS_GATEWAY}
mlan1:
driver: macvlan
driver_opts:
parent: eth0
ipam:
config:
- subnet: ${MLAN1_NETWORK}
gateway: ${MLAN1_GATEWAY}

View File

@@ -0,0 +1,2 @@
ALTER TABLE "logs" ALTER COLUMN "stack" SET DATA TYPE jsonb;--> statement-breakpoint
ALTER TABLE "logs" ALTER COLUMN "stack" SET DEFAULT '[]'::jsonb;

View File

@@ -0,0 +1,192 @@
{
"id": "922093ba-6949-4d30-9c17-007257754cf2",
"prevId": "5eac3348-eeab-4210-b686-29e570f87911",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.logs": {
"name": "logs",
"schema": "",
"columns": {
"log_id": {
"name": "log_id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": false
},
"module": {
"name": "module",
"type": "text",
"primaryKey": false,
"notNull": true
},
"subModule": {
"name": "subModule",
"type": "text",
"primaryKey": false,
"notNull": false
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"stack": {
"name": "stack",
"type": "jsonb",
"primaryKey": false,
"notNull": false,
"default": "'[]'::jsonb"
},
"checked": {
"name": "checked",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"hostname": {
"name": "hostname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.settings": {
"name": "settings",
"schema": "",
"columns": {
"settings_id": {
"name": "settings_id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"moduleName": {
"name": "moduleName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"roles": {
"name": "roles",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[\"systemAdmin\"]'::jsonb"
},
"add_User": {
"name": "add_User",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'LST_System'"
},
"add_Date": {
"name": "add_Date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"upd_User": {
"name": "upd_User",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'LST_System'"
},
"upd_date": {
"name": "upd_date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"name": {
"name": "name",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -15,6 +15,13 @@
"when": 1756693049476,
"tag": "0001_solid_kronos",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1756843987534,
"tag": "0002_mean_nuke",
"breakpoints": true
}
]
}