diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1f10bd --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Code Error Microservice + +A real-time error monitoring and reporting service that integrates with Telex channels, providing prioritized error classification and automated notifications. + +## đŸŽ¯ Overview + +This microservice monitors your codebase for errors, processes them through a message queue system, and delivers prioritized notifications to your Telex channels. It supports real-time monitoring and configurable error thresholds. + + +### Core Components + +- **Error Controller**: Entry point for error processing +- **Categorization Service**: Analyzes and classifies errors +- **ZeroMQ Service**: Handles message queuing and distribution +- **Webhook Service**: Manages Telex channel communication + + +## đŸŽ¯ Features + +- **Error Detection** + - Real-time monitoring + - Static code analysis (ESLint) + - Stack trace processing + +- **Error Processing** + - Automatic categorization + - Priority classification + - Error enrichment + +- **Notification System** + - Real-time Telex updates + - Configurable webhooks + +## 🚀 Getting Started + +### Prerequisites + +- Node.js 20.x +- npm 9.x +- ZeroMQ library + +### Quick Start + +```bash +# Clone repository +git clone https://github.com/telexintegrations/code-error-microservice + +# Install dependencies +npm install + +# Setup environment +cp .env.example .env + +# Start development server +npm run dev +``` + +## đŸˇī¸ Error Classification + +| Severity | Description | Example | +|----------|-------------|---------| +| 🚨 High | System critical | Service crash, DB connection failure | +| 🔔 Medium | Functional issues | API timeout, validation errors | +| â„šī¸ Low | Minor problems | Deprecation warnings, style issues | + +## đŸ› ī¸ Project Structure + +``` +src/ +├── controllers/ # Request handlers +├── services/ # Business logic +├── middlewares/ # HTTP middlewares +├── routes/ # API routes +├── utils/ # Helper functions +└── app.ts # Application entry +``` + + +## đŸ“Ļ Core Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| express | ^4.21.2 | Web framework | +| zeromq | ^6.3.0 | Message queue | +| axios | ^1.8.3 | HTTP client | +| typescript | ^5.8.2 | Type support | +| pm2 | latest | Process management | diff --git a/src/controllers/errorController.ts b/src/controllers/errorController.ts index 877d8f1..6eaff9b 100644 --- a/src/controllers/errorController.ts +++ b/src/controllers/errorController.ts @@ -1,8 +1,140 @@ +/** + * local version + * */ + +// import { Request, Response, NextFunction } from "express"; +// import { categorizeError } from "../services/categorizationService"; + +// export interface ProcessedError { +// channelId: string; +// type: string; +// errors: ErrorItem[]; +// timestamp: string; +// priority?: string; +// } + +// export interface ErrorItem { +// message: string; +// stack: string; +// // A simplified, user-friendly description of the error. +// readableMessage?: string; +// } + +// let lastProcessedError: ProcessedError | null = null; + +// /** +// * Handles incoming error reports by: +// * - Validating the payload. +// * - Categorizing each error using the updated categorization service. +// * - Enriching errors with a user-friendly message that omits the verbose stack trace. +// * - Constructing a neat summary report with emojis and essential details. +// * +// * If the payload is invalid (missing channelId, type, or errors array), +// * responds with a 400 status and an explanatory message. +// */ +// export const handleIncomingError = ( +// req: Request, +// res: Response, +// next: NextFunction +// ): void => { +// try { +// const { channelId, type, errors, timestamp } = req.body; + +// if (!channelId || !type || !Array.isArray(errors) || errors.length === 0) { +// res.status(400).json({ +// error: +// "đŸšĢ Invalid error report format. Ensure that 'channelId', 'type', and a non-empty 'errors' array are provided.", +// }); +// return; +// } + +// // Enrich each error with a more friendly message (removing detailed stack traces). +// const enrichedErrors: ErrorItem[] = errors.map((err: ErrorItem) => { +// const severity = categorizeError(err.message); +// let emoji: string; +// switch (severity) { +// case "High": +// emoji = "🚨"; +// break; +// case "Medium": +// emoji = "🔔"; +// break; +// default: +// emoji = "â„šī¸"; +// break; +// } +// return { +// ...err, + +// readableMessage: `${emoji} ${severity} severity error: ${err.message}`, +// }; +// }); + +// // Determine the highest severity among reported errors. +// const highestSeverity = enrichedErrors +// .map((err) => categorizeError(err.message)) +// .reduce( +// (prev, current) => +// current === "High" +// ? current +// : prev === "High" +// ? prev +// : current === "Medium" +// ? current +// : prev, +// "Low" +// ); + +// // Format timestamp to a more readable local date and time string. +// const formattedTimestamp = timestamp +// ? new Date(timestamp).toLocaleString() +// : new Date().toLocaleString(); + +// lastProcessedError = { +// channelId, +// type, +// errors: enrichedErrors, +// timestamp: formattedTimestamp, +// priority: highestSeverity, +// }; + +// // Build a simplified user-friendly error report message. +// let reportMessage = `✅ Error Report Accepted: +// Channel: ${channelId} +// Type: ${type} +// Time: ${formattedTimestamp} +// Overall Severity: ${highestSeverity} + +// Detailed Errors: +// `; +// enrichedErrors.forEach((err, idx) => { +// reportMessage += `Error ${idx + 1}: ${err.readableMessage}\n`; +// }); + +// res.status(202).json({ +// status: "accepted", +// message: reportMessage, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// /** +// * Returns the last processed error report. +// */ +// export const getLastProcessedError = (): ProcessedError | null => { +// return lastProcessedError; +// }; + + +/** + * live version + */ import { Request, Response, NextFunction } from "express"; import { categorizeError } from "../services/categorizationService"; export interface ProcessedError { - channelId: string; type: string; errors: ErrorItem[]; timestamp: string; @@ -12,47 +144,97 @@ export interface ProcessedError { export interface ErrorItem { message: string; stack: string; + // A simplified, user-friendly description of the error. + readableMessage?: string; } let lastProcessedError: ProcessedError | null = null; +/** + * Handles incoming error reports by: + * - Validating the payload. + * - Categorizing each error using the updated categorization service. + * - Enriching errors with a user-friendly message that omits the verbose stack trace. + * + * If the payload is invalid (missing type or errors array), + * responds with a 400 status and an explanatory message. + */ export const handleIncomingError = ( req: Request, res: Response, next: NextFunction ): void => { try { - const { channelId, type, errors, timestamp } = req.body; + const { type, errors, timestamp } = req.body; - if (!channelId || !type || !Array.isArray(errors) || errors.length === 0) { - res.status(400).json({ error: "Invalid error report format." }); + if (!type || !Array.isArray(errors) || errors.length === 0) { + res.status(400).json({ + error: + "đŸšĢ Invalid error report format. Ensure that 'type' and a non-empty 'errors' array are provided.", + }); return; } - const highestSeverity = errors - .map(err => categorizeError(err.message)) - .reduce((prev, current) => - current === "High" ? current : - (prev === "High" ? prev : - (current === "Medium" ? current : prev)), - "Low" - ); + // Enrich each error with a more friendly message (removing detailed stack traces). + const enrichedErrors: ErrorItem[] = errors.map((err: ErrorItem) => { + const severity = categorizeError(err.message); + let emoji: string; + switch (severity) { + case "High": + emoji = "🚨"; + break; + case "Medium": + emoji = "🔔"; + break; + default: + emoji = "â„šī¸"; + break; + } + return { + ...err, + readableMessage: `${emoji} ${severity} severity error: ${err.message}`, + }; + }); + // Determine the highest severity among reported errors. + const highestSeverity = enrichedErrors + .map((err) => categorizeError(err.message)) + .reduce( + (prev, current) => + current === "High" + ? current + : prev === "High" + ? prev + : current === "Medium" + ? current + : prev, + "Low" + ); + + // Format timestamp to a more readable local date and time string. + const formattedTimestamp = timestamp + ? new Date(timestamp).toLocaleString() + : new Date().toLocaleString(); + lastProcessedError = { - channelId, type, - errors, - timestamp: timestamp || new Date().toISOString(), - priority: highestSeverity + errors: enrichedErrors, + timestamp: formattedTimestamp, + priority: highestSeverity, }; - res.status(202).json({ status: "accepted" }); - + res.status(202).json({ + status: "accepted", + severity: highestSeverity + }); } catch (error) { next(error); } }; +/** + * Returns the last processed error report. + */ export const getLastProcessedError = (): ProcessedError | null => { return lastProcessedError; }; \ No newline at end of file diff --git a/src/middlewares/requestLogger.ts b/src/middlewares/requestLogger.ts index 2ca94cd..3dd399e 100644 --- a/src/middlewares/requestLogger.ts +++ b/src/middlewares/requestLogger.ts @@ -1,9 +1,24 @@ import { Request, Response, NextFunction } from "express"; -const requestLogger = (req: Request, _res: Response, next: NextFunction) => { - console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); - console.log("Body:", JSON.stringify(req.body, null, 2)); +/** + * Logs incoming HTTP requests with timestamp, method, URL, query parameters, and body. + * This middleware helps with debugging incoming requests. + */ +const requestLogger = (req: Request, _res: Response, next: NextFunction): void => { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${req.method} ${req.url}`); + + // Log query parameters if present + if (Object.keys(req.query).length > 0) { + console.log("Query:", JSON.stringify(req.query, null, 2)); + } + + // Log request body if present + if (req.body && Object.keys(req.body).length > 0) { + console.log("Body:", JSON.stringify(req.body, null, 2)); + } + next(); }; -export default requestLogger; +export default requestLogger; \ No newline at end of file diff --git a/src/routes/tick.ts b/src/routes/tick.ts index 304b0d1..5dcdb06 100644 --- a/src/routes/tick.ts +++ b/src/routes/tick.ts @@ -42,7 +42,7 @@ router.post("/tick", async (req: Request, res: Response) => { "event_name": "Code Error Monitor Agent", "message": message, "status": "success", - "username": "Agent Sapa" + "username": "Code Error Agent" }; console.log(telexPayload.message); diff --git a/src/services/categorizationService.ts b/src/services/categorizationService.ts index 16c9bbd..9cb356b 100644 --- a/src/services/categorizationService.ts +++ b/src/services/categorizationService.ts @@ -1,15 +1,53 @@ - +/** + * Represents the severity level for an error. + * "High" errors are critical and might crash the application. + * "Medium" errors indicate runtime issues that may cause unexpected behavior. + * "Low" errors are less severe and often related to minor glitches. + */ export type ErrorCategory = "High" | "Medium" | "Low"; +/** + * Categorizes an error message into a severity level. + * + * This function attempts to cover most types of runtime errors by performing + * a series of case-insensitive checks against known error types. It is designed + * to align with the grouping strategies used in platforms like Sentry. + * + * - "High": Critical errors such as ReferenceError, SyntaxError, InternalError. + * - "Medium": Recoverable or non-critical runtime errors like TypeError, RangeError, + * EvalError, URIError, AggregateError, DOMException, NetworkError, Timeout errors. + * - "Low": All other errors that do not fall under the above categories. + * + * @param errorMessage - The error message to be categorized. + * @returns The determined error category. + */ export const categorizeError = (errorMessage: string): ErrorCategory => { + const message = errorMessage.toLowerCase(); + + // High severity errors indicate faults that likely break application execution. if ( - errorMessage.includes("ReferenceError") || - errorMessage.includes("SyntaxError") + message.includes("referenceerror") || + message.includes("syntaxerror") || + message.includes("internalerror") ) { return "High"; - } else if (errorMessage.includes("TypeError")) { + } + + // Medium severity errors are indicative of issues that affect functionality + // but might be recoverable or less critical. + if ( + message.includes("typeerror") || + message.includes("rangeerror") || + message.includes("evalerror") || + message.includes("urierror") || + message.includes("aggregateerror") || + message.includes("domexception") || + message.includes("networkerror") || + message.includes("timeout") + ) { return "Medium"; - } else { - return "Low"; } -}; + + // All other errors are considered low severity. + return "Low"; +}; \ No newline at end of file diff --git a/src/services/zeromqService.ts b/src/services/zeromqService.ts index 8aa14f4..4c8c3ad 100644 --- a/src/services/zeromqService.ts +++ b/src/services/zeromqService.ts @@ -1,45 +1,106 @@ +/** + * Local version + */ import * as zmq from "zeromq"; -import { ProcessedError } from "../controllers/errorController"; import axios from "axios"; +import { ProcessedError } from "../controllers/errorController"; import { ENV_CONFIG } from "../utils/envConfig"; +import { categorizeError } from "../services/categorizationService"; // Importing for fallback severity computation + +// Define an interface for the error object structure +interface ErrorObject { + message?: string; + stack?: string; + readableMessage?: string; +} const webhookUrl = "https://ping.telex.im/v1/webhooks"; +// You can optionally use environment variables for host/port configuration +const ZERO_MQ_BIND_HOST = "0.0.0.0"; + +/** + * Determines the overall severity based on the errors of the report. + * It uses the same categorization logic as in errorController. + */ +function computeOverallSeverity(errors: ErrorObject[]): string { + return errors + .map((err) => categorizeError(err.message || "")) + .reduce((prev, current) => + current === "High" + ? current + : prev === "High" + ? prev + : current === "Medium" + ? current + : prev, + "Low" + ); +} + +/** + * Initializes the ZeroMQ server with a Reply socket for receiving error payloads + * and a Publisher socket for broadcasting updates. + */ async function initializeServer() { const replySocket = new zmq.Reply(); const publishSocket = new zmq.Publisher(); try { - const zeroMqBindHost = "0.0.0.0"; - const zeroMqBindPortPublish = ENV_CONFIG.PORT + 1; - const zeroMqBindPortReply = zeroMqBindPortPublish + 1; - await replySocket.bind(`tcp://${zeroMqBindHost}:${zeroMqBindPortReply}`); - await publishSocket.bind( - `tcp://${zeroMqBindHost}:${zeroMqBindPortPublish}` - ); + // Use ENV_CONFIG setting for base port if available, with fallback values + const basePort = ENV_CONFIG.PORT; + const zeroMqBindPortPublish = basePort + 1; // Or use: basePort + 1; + const zeroMqBindPortReply = basePort + 2; // Or use: basePort + 2; + + await replySocket.bind(`tcp://${ZERO_MQ_BIND_HOST}:${zeroMqBindPortReply}`); + await publishSocket.bind(`tcp://${ZERO_MQ_BIND_HOST}:${zeroMqBindPortPublish}`); console.log( `ZeroMQ server bound to ports ${zeroMqBindPortReply} (Reply) and ${zeroMqBindPortPublish} (Publish)` ); const serverPublish = async (message: string) => { - await publishSocket.send(["update", message]); - console.log("Server published:", message); + try { + await publishSocket.send(["update", message]); + console.log("Server published:", message); + } catch (pubError) { + console.error("Failed to publish message:", pubError); + } }; + // Process incoming messages on the Reply socket. for await (const [msg] of replySocket) { - let parsedMessage; - console.log("Received:", msg.toString()); + let parsedMessage: ProcessedError | any; + const rawMsg = msg.toString(); + console.log('===============') + console.log("Received:", rawMsg); + console.log('===============') try { - parsedMessage = JSON.parse(msg.toString()) as unknown as ProcessedError; - } catch (error) { - parsedMessage = msg.toString() as unknown as ProcessedError; - console.error("Failed to parse message:", error); + parsedMessage = JSON.parse(rawMsg); + + // If it's a single error message without an errors array, wrap it appropriately. + if (!parsedMessage.errors && parsedMessage.message !== undefined) { + parsedMessage = { + channelId: parsedMessage.channelId, + type: parsedMessage.type, + errors: [ + { + message: parsedMessage.message, + stack: parsedMessage.stack, + readableMessage: parsedMessage.readableMessage + }, + ], + timestamp: parsedMessage.timestamp, + } as ProcessedError; + } + } catch (parseError) { + console.error("Failed to parse message:", parseError); await replySocket.send( JSON.stringify({ status: "error", message: "Invalid message format" }) ); continue; } + if (!parsedMessage || !parsedMessage.channelId || !parsedMessage.errors) { console.warn("Invalid message format"); await replySocket.send( @@ -48,30 +109,43 @@ async function initializeServer() { continue; } - const errorSummary = parsedMessage.errors.map((err) => ({ - message: err.message, - stack: err.stack, - })); - - const message = ` - Errors: - ${errorSummary - .map( - (err, index) => ` - Error ${index + 1}: - Message: ${err.message} - Stack: ${err.stack} - ` - ) - .join("\n")} - `.trim(); + // Create a human-readable summary of errors using the enriched readableMessage. + const errorSummary = parsedMessage.errors + .map((err: ErrorObject, index: number) => { + // Prefer the enriched readableMessage, fall back to the original message. + const message = err.readableMessage || err.message || "N/A"; + return ` ${index + 1}. ${message}`; + }) + .join("\n"); + + // Format the current timestamp. If parsedMessage.timestamp exists, use it. + const formattedTime = parsedMessage.timestamp + ? new Date(parsedMessage.timestamp).toLocaleString() + : new Date().toLocaleString(); + + // Compute overall severity using the incoming payload or fallback + const overallSeverity = + parsedMessage.priority || computeOverallSeverity(parsedMessage.errors); + + // Build an aesthetically enhanced error report. (Channel name removed) + const formattedMessage = `🎉 *Error Report Accepted!* 🎉 + +------------------------------------------ +Type : ${parsedMessage.type} +Time : ${formattedTime} +Overall Severity: ${overallSeverity} +------------------------------------------ +Errors: +${errorSummary} +------------------------------------------`; const telexPayload = { event_name: "Code Error Monitor Agent", - message: message, + message: formattedMessage, status: "success", username: "Code Error Agent", }; + try { const response = await axios.post( `${webhookUrl}/${parsedMessage.channelId}`, @@ -84,10 +158,10 @@ async function initializeServer() { } ); - console.log("response data", response?.data); + console.log("Webhook response data:", response?.data); await replySocket.send(JSON.stringify({ status: "success" })); - } catch (error) { - console.error("Failed to send to webhook:", error); + } catch (webhookError) { + console.error("Failed to send to webhook:", webhookError); await replySocket.send( JSON.stringify({ status: "error", @@ -105,3 +179,181 @@ async function initializeServer() { } export const zeromqClient = initializeServer(); + +/** + * live version + */ + +// import * as zmq from "zeromq"; +// import axios from "axios"; +// import { ProcessedError } from "../controllers/errorController"; +// import { ENV_CONFIG } from "../utils/envConfig"; +// import { categorizeError } from "../services/categorizationService"; // Importing for fallback severity computation + +// // Define an interface for the error object structure +// interface ErrorObject { +// message?: string; +// stack?: string; +// readableMessage?: string; +// } + +// const webhookUrl = "https://ping.telex.im/v1/webhooks"; + +// /** +// * Determines the overall severity based on the errors of the report. +// * It uses the same categorization logic as in errorController. +// */ +// function computeOverallSeverity(errors: ErrorObject[]): string { +// return errors +// .map((err) => categorizeError(err.message || "")) +// .reduce((prev, current) => +// current === "High" +// ? current +// : prev === "High" +// ? prev +// : current === "Medium" +// ? current +// : prev, +// "Low" +// ); +// } + +// /** +// * Initializes the ZeroMQ server with a Reply socket for receiving error payloads +// * and a Publisher socket for broadcasting updates. +// */ +// async function initializeServer() { +// const replySocket = new zmq.Reply(); +// const publishSocket = new zmq.Publisher(); + +// try { +// // Use the port and configuration from the live version +// const zeroMqBindHost = "0.0.0.0"; +// const zeroMqBindPortPublish = ENV_CONFIG.PORT + 1; +// const zeroMqBindPortReply = zeroMqBindPortPublish + 1; +// await replySocket.bind(`tcp://${zeroMqBindHost}:${zeroMqBindPortReply}`); +// await publishSocket.bind( +// `tcp://${zeroMqBindHost}:${zeroMqBindPortPublish}` +// ); +// console.log( +// `ZeroMQ server bound to ports ${zeroMqBindPortReply} (Reply) and ${zeroMqBindPortPublish} (Publish)` +// ); + +// const serverPublish = async (message: string) => { +// try { +// await publishSocket.send(["update", message]); +// console.log("Server published:", message); +// } catch (pubError) { +// console.error("Failed to publish message:", pubError); +// } +// }; + +// // Process incoming messages on the Reply socket. +// for await (const [msg] of replySocket) { +// let parsedMessage: ProcessedError | any; +// const rawMsg = msg.toString(); +// console.log("Received:", rawMsg); + +// try { +// parsedMessage = JSON.parse(rawMsg); +// // If it's a single error message without an errors array, wrap it appropriately. +// if (!parsedMessage.errors && parsedMessage.message !== undefined) { +// parsedMessage = { +// channelId: parsedMessage.channelId, +// type: parsedMessage.type, +// errors: [ +// { +// message: parsedMessage.message, +// stack: parsedMessage.stack, +// readableMessage: parsedMessage.readableMessage +// }, +// ], +// timestamp: parsedMessage.timestamp, +// } as ProcessedError; +// } +// } catch (parseError) { +// console.error("Failed to parse message:", parseError); +// await replySocket.send( +// JSON.stringify({ status: "error", message: "Invalid message format" }) +// ); +// continue; +// } + +// if (!parsedMessage || !parsedMessage.channelId || !parsedMessage.errors) { +// console.warn("Invalid message format"); +// await replySocket.send( +// JSON.stringify({ status: "error", message: "Invalid message format" }) +// ); +// continue; +// } + +// // Create a human-readable summary of errors using the enriched readableMessage. +// const errorSummary = parsedMessage.errors +// .map((err: ErrorObject, index: number) => { +// // Prefer the enriched readableMessage, fall back to the original message. +// const message = err.readableMessage || err.message || "N/A"; +// return ` ${index + 1}. ${message}`; +// }) +// .join("\n"); + +// // Format the current timestamp. If parsedMessage.timestamp exists, use it. +// const formattedTime = parsedMessage.timestamp +// ? new Date(parsedMessage.timestamp).toLocaleString() +// : new Date().toLocaleString(); + +// // Compute overall severity using the incoming payload or fallback +// const overallSeverity = +// parsedMessage.priority || computeOverallSeverity(parsedMessage.errors); + +// // Build an aesthetically enhanced error report. (Channel name removed) +// const formattedMessage = `🎉 *Error Report Accepted!* 🎉 + +// ------------------------------------------ +// Type : ${parsedMessage.type} +// Time : ${formattedTime} +// Overall Severity: ${overallSeverity} +// ------------------------------------------ +// Errors: +// ${errorSummary} +// ------------------------------------------`; + +// const telexPayload = { +// event_name: "Code Error Monitor Agent", +// message: formattedMessage, +// status: "success", +// username: "Code Error Agent", +// }; + +// try { +// const response = await axios.post( +// `${webhookUrl}/${parsedMessage.channelId}`, +// telexPayload, +// { +// headers: { +// "Content-Type": "application/json", +// "User-Agent": "Code Error Agent/1.0.0", +// }, +// } +// ); + +// console.log("Webhook response data:", response?.data); +// await replySocket.send(JSON.stringify({ status: "success" })); +// } catch (webhookError) { +// console.error("Failed to send to webhook:", webhookError); +// await replySocket.send( +// JSON.stringify({ +// status: "error", +// message: "Failed to send to webhook", +// }) +// ); +// } +// } + +// return { serverPublish }; +// } catch (error) { +// console.error("ZeroMQ server error:", error); +// throw error; +// } +// } + +// export const zeromqClient = initializeServer(); \ No newline at end of file