feat(logger): setup logger with discord and db logging

This commit is contained in:
2025-08-31 21:35:50 -05:00
parent fc3cfe999a
commit 2e51474a5e
13 changed files with 1577 additions and 20 deletions

View File

@@ -13,3 +13,6 @@ PROD_SERVER=usmcd1vms036
PROD_PLANT_TOKEN=test3 PROD_PLANT_TOKEN=test3
PROD_USER=alplaprod PROD_USER=alplaprod
PROD_PASSWORD=password PROD_PASSWORD=password
# admin
WEBHOOK_URL

View File

@@ -7,13 +7,22 @@ import path, { dirname, join } from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { db } from "./pkg/db/db.js"; import { db } from "./pkg/db/db.js";
import { settings } from "./pkg/db/schema/settings.js"; import { settings } from "./pkg/db/schema/settings.js";
import { env } from "./pkg/utils/envValidator.js";
import { createLogger } from "./pkg/logger/logger.js";
const PORT = Number(process.env.VITE_PORT) || 4200; const PORT = Number(env.VITE_PORT) || 4200;
const main = async () => { const main = async () => {
//create the logger
const log = createLogger({ module: "system", subModule: "main start" });
// base path // base path
let basePath: string = ""; let basePath: string = "";
if (process.env.NODE_ENV?.trim() !== "production") {
if (
process.env.NODE_ENV?.trim() !== "production" &&
!env.RUNNING_IN_DOCKER
) {
basePath = "/lst"; basePath = "/lst";
} }
@@ -24,7 +33,11 @@ const main = async () => {
try { try {
const set = await db.select().from(settings); const set = await db.select().from(settings);
console.log(set); if (set.length === 0) {
return log.fatal(
"Seems like the DB is not setup yet the app will close now"
);
}
} catch (error) { } catch (error) {
console.error("Error getting settings", error); console.error("Error getting settings", error);
} }
@@ -63,13 +76,16 @@ const main = async () => {
// start the server up // start the server up
server.listen(PORT, "0.0.0.0", () => server.listen(PORT, "0.0.0.0", () =>
console.log( log.info(
`Server running in ${process.env.NODE_ENV}, on http://0.0.0.0:${PORT}${basePath}` `Server running in ${
process.env.NODE_ENV ? process.env.NODE_ENV : "dev"
}, on http://0.0.0.0:${PORT}${basePath}`
) )
); );
}; };
main().catch((err) => { main().catch((err) => {
console.error("Startup error:", err); const log = createLogger({ module: "system", subModule: "main start" });
log.fatal("Startup error:", err);
process.exit(1); process.exit(1);
}); });

View File

@@ -0,0 +1,21 @@
import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
export const logs = pgTable("logs", {
log_id: uuid("log_id").defaultRandom().primaryKey(),
level: text("level"),
module: text("module").notNull(),
subModule: text("subModule"),
message: text("message").notNull(),
stack: text("stack"),
checked: boolean("checked").default(false),
hostname: text("hostname"),
createdAt: timestamp("createdAt").defaultNow(),
});
export const logSchema = createSelectSchema(logs);
export const newSettingSchema = createInsertSchema(logs);
export type Log = z.infer<typeof logSchema>;
export type NewLog = z.infer<typeof newSettingSchema>;

View File

@@ -0,0 +1,34 @@
import build from "pino-abstract-transport";
import { db } from "../db/db.js";
import { logs, type Log } from "../db/schema/logs.js";
const pinoLogLevels: any = {
10: "trace",
20: "debug",
30: "info",
40: "warn",
50: "error",
60: "fatal",
};
// Create a custom transport function
export default async function (log: Log) {
//const {username, service, level, msg, ...extra} = log;
try {
return build(async function (source) {
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,
});
}
});
} catch (err) {
console.error("Error inserting log into database:", err);
}
}

View File

@@ -0,0 +1,53 @@
import pino, { type Logger } from "pino";
import { env } from "../utils/envValidator.js";
export let logLevel = process.env.LOG_LEVEL || "info";
interface CustomLogger extends pino.Logger {
specialMonitor: pino.LogFn;
}
const transport = pino.transport({
targets: [
{
target: "pino-pretty",
options: {
colorize: true,
singleLine: true,
// customPrettifiers: {
// time: (time) => `🕰 ${time}`,
// },
destination: process.stdout.fd,
},
},
{
target: "./dbTransport.js",
},
{
target: "./notification.js",
},
// Only log to Go if LST_USE_GO=true
...(process.env.LST_USE_GO === "true"
? [
{
target: "./goTransport.js", // New transport for Go
},
]
: []),
],
});
export const rootLogger: Logger = pino(
{
level: logLevel,
redact: { paths: ["email", "password"], remove: true },
},
transport
);
/**
* factory to create child to log things for us
*/
export function createLogger(bindings: Record<string, unknown>): Logger {
return rootLogger.child(bindings);
}

View File

@@ -0,0 +1,87 @@
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";
const pinoLogLevels: any = {
10: "trace",
20: "debug",
30: "info",
40: "warn",
50: "error",
60: "fatal",
};
// discord function
async function sendFatal(log: Log) {
const webhookUrl = process.env.WEBHOOK_URL!;
const payload = {
embeds: [
{
title: `🚨 ${env.PROD_PLANT_TOKEN}: encounter a critical error `,
description: `Where was the error: ${log.module}${
log.subModule ? `-${log.subModule}` : null
}`,
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;
try {
return build(async function (source) {
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";
const newlog = {
level: levelName,
module: obj?.module.toLowerCase(),
subModule: obj?.subModule.toLowerCase(),
hostname: obj?.hostname.toLowerCase(),
message: obj.msg,
};
if (!process.env.WEBHOOK_URL) {
return;
}
if (obj.level >= 60) {
sendFatal(newlog as Log);
}
}
});
} catch (err) {
console.error("Error inserting log into database:", err);
}
}

View File

@@ -0,0 +1,60 @@
import dns from "dns";
import net from "net";
export async function checkHostnamePort(
hostnamePort: string
): Promise<boolean> {
try {
// Split the input into hostname and port
const [hostname, port] = hostnamePort.split(":");
if (!hostname || !port) {
return false; // Invalid format
}
// Resolve the hostname to an IP address
const ip = (await resolveHostname(hostname)) as string;
// Check if the port is open
const portCheck = await checkPort(ip, parseInt(port, 10));
return true; // Hostname:port is reachable
} catch (err) {
return false; // Any error means the hostname:port is not reachable
}
}
function checkPort(ip: string, port: number): Promise<boolean> {
return new Promise((resolve, reject) => {
const socket = new net.Socket();
socket.setTimeout(2000); // Set a timeout for the connection attempt
socket.on("connect", () => {
socket.destroy(); // Close the connection
resolve(true); // Port is open
});
socket.on("timeout", () => {
socket.destroy(); // Close the connection
reject(new Error("Connection timed out")); // Port is not reachable
});
socket.on("error", (err: any) => {
reject(new Error(`Unknown error: ${err}`)); // Handle non-Error types
});
socket.connect(port, ip);
});
}
function resolveHostname(hostname: string) {
return new Promise((resolve, reject) => {
dns.lookup(hostname, (err, address) => {
if (err) {
reject(err);
} else {
resolve(address);
}
});
});
}

View File

@@ -0,0 +1,53 @@
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
VITE_PORT: z.string().default("4200"),
LOG_LEVEL: z.string().default("info"),
// app 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_SERVER: z.string(),
PROD_PLANT_TOKEN: z.string(),
PROD_USER: z.string(),
PROD_PASSWORD: z.string(),
//docker specifics
RUNNING_IN_DOCKER: z.boolean().default(false),
});
// use safeParse instead of parse
const parsed = 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 const env = parsed.data;

View File

@@ -0,0 +1,11 @@
CREATE TABLE "logs" (
"log_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"level" text,
"module" text NOT NULL,
"subModule" text,
"message" text NOT NULL,
"stack" text,
"checked" boolean DEFAULT false,
"hostname" text,
"createdAt" timestamp DEFAULT now()
);

View File

@@ -0,0 +1,191 @@
{
"id": "5eac3348-eeab-4210-b686-29e570f87911",
"prevId": "c97e112f-6742-4754-be09-0abd659f1eb5",
"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": "text",
"primaryKey": false,
"notNull": false
},
"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

@@ -8,6 +8,13 @@
"when": 1756669145189, "when": 1756669145189,
"tag": "0000_wonderful_eternals", "tag": "0000_wonderful_eternals",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1756693049476,
"tag": "0001_solid_kronos",
"breakpoints": true
} }
] ]
} }

1039
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,14 +38,20 @@
"@dotenvx/dotenvx": "^1.49.0", "@dotenvx/dotenvx": "^1.49.0",
"drizzle-kit": "^0.31.4", "drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.5", "drizzle-orm": "^0.44.5",
"drizzle-zod": "^0.8.3",
"express": "^5.1.0", "express": "^5.1.0",
"morgan": "^1.10.1", "morgan": "^1.10.1",
"mssql": "^11.0.1",
"pg": "^8.16.3", "pg": "^8.16.3",
"postgres": "^3.4.7" "pino": "^9.9.0",
"pino-pretty": "^13.1.1",
"postgres": "^3.4.7",
"zod": "^4.1.5"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@types/morgan": "^1.9.10", "@types/morgan": "^1.9.10",
"@types/mssql": "^9.1.7",
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"standard-version": "^9.5.0", "standard-version": "^9.5.0",