Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | 1x 1x 1x 1x 1x 1x 1x 1x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 1x 1x 2x 1x 1x 4x 4x 2x 2x 2x 1x 1x 1x 5x 6x 6x 1x 1x 5x 4x 11x 1x 10x 10x 10x 10x 9x 10x 8x 8x 7x 6x 7x 4x 4x 3x 3x 3x 2x 1x 1x | import express, { type Request, type Response, type NextFunction, } from 'express'; import 'dotenv/config'; import os from 'os'; import cookieParser from 'cookie-parser'; import axios from 'axios'; import schedule from 'node-schedule'; import dayjs from 'dayjs'; import { type Alert, type AlertManagerPayload } from './types/alert.js'; /** * Main server class for handling AlertManager webhooks and forwarding alerts to Gotify. * * This server provides a complete solution for processing AlertManager alerts with features including: * - Alert deduplication to prevent notification spam * - Automatic cache cleanup of old alerts * - Status-based notification formatting (firing/resolved) * - Integration with Gotify for push notifications * - Built-in health checks and monitoring endpoints * * @class Server * @author Munir Mardinli <munir@mardinli.de> * @date 2025-06-06 * @version 1.1.0 * @since 1.0.0 * * @example * ```typescript * // Basic usage * const server = new Server(); * server.start(); * ``` */ export class Server { /** * Express application instance * @private * @readonly */ private readonly app: express.Express; /** * Gotify server URL loaded from environment variables * @private * @readonly */ private readonly GOTIFY_URL = process.env.GOTIFY_ALERT_URL; /** * Default priority level for firing alerts * @private * @readonly */ private readonly DEFAULT_PRIORITY = 5; /** * Time-to-live for alert deduplication cache in minutes * @private * @readonly */ private readonly TTL_MINUTES = 2; /** * Cache for tracking sent alerts to prevent duplicates * Maps alert fingerprint to timestamp * @private */ private sentAlerts = new Map<string, number>(); /** * Creates a new Server instance * Initializes Express app, middleware, routes and cleanup jobs * @constructor */ constructor() { this.app = express(); this.configureMiddleware(); this.setupRoutes(); this.setupAlertCleanupJob(); } /** * Configures Express middleware * @private * @returns {void} */ private configureMiddleware(): void { this.app.use(express.json({ limit: '50mb' })); this.app.use(express.urlencoded({ limit: '50mb', extended: true })); this.app.use(cookieParser()); } /** * Sets up all API routes * @private * @returns {void} */ private setupRoutes(): void { this.app.post('/alert', (req: Request, res: Response, next: NextFunction) => this.handleAlert(req, res, next), ); } /** * Sets up scheduled job to clean up old alerts from deduplication cache * Runs every second to check for expired alerts * @private * @returns {void} */ private setupAlertCleanupJob(): void { schedule.scheduleJob('* * * * * *', () => { const now = dayjs().valueOf(); for (const [id, timestamp] of this.sentAlerts.entries()) { if (now - timestamp >= this.TTL_MINUTES * 60 * 1000) { this.sentAlerts.delete(id); console.log(`๐งน Removed from cache: ${id}`); } } }); } /** * Handles incoming AlertManager webhook requests * @private * @param {Request<object, object, AlertManagerPayload>} req - Express request object * @param {Response} res - Express response object * @returns {Promise<Response>} Response with status and message */ private async handleAlert( req: Request<{}, {}, AlertManagerPayload>, res: Response, next: NextFunction, ): Promise<void> { try { if (!Array.isArray(req.body?.alerts)) { res.status(400).json({ error: 'Invalid payload: alerts missing or not an array', }); return; } await this.processAlerts(req.body.alerts); res.status(200).send('Alerts forwarded to Gotify'); } catch (error) { console.error(`โ Error sending to Gotify: ${error}`); next(error); } } /** * Processes an array of alerts with deduplication and forwarding * @private * @param {Alert[]} alerts - Array of Alert objects to process * @returns {Promise<void>} */ private async processAlerts(alerts: Alert[]): Promise<void> { await Promise.all( alerts.map(async (alert) => { const id = this.generateAlertId(alert); if (this.sentAlerts.has(id)) { console.info(`๐ Duplicate detected, skipped: ${id}`); return; } await this.sendGotifyNotification(alert, id); this.sentAlerts.set(id, dayjs().valueOf()); }), ); } /** * Sends notification to Gotify for a single alert * @private * @param {Alert} alert - Alert object to notify about * @param {string} id - Unique identifier for the alert * @returns {Promise<void>} * @throws {Error} When Gotify URL is not configured */ private async sendGotifyNotification( alert: Alert, id: string, ): Promise<void> { if (!this.GOTIFY_URL) { throw new Error('Gotify URL not configured'); } const title = alert.status === 'firing' ? '๐จ New alert' : 'โ Resolved'; const message = `${alert.labels['alertname'] || 'Alert'}: ${alert.annotations['description'] || alert.annotations['summary'] || 'No description' }`; const priority = alert.status === 'firing' ? this.DEFAULT_PRIORITY : 1; await axios.post(this.GOTIFY_URL, { title, message, priority }); console.info(`โ Sent: ${id}`); } /** * Generates unique identifier for an alert based on its properties * @private * @param {Alert} alert - Alert object to generate ID for * @returns {string} Unique identifier string */ private generateAlertId(alert: Alert): string { return [ alert.labels['alertname'] || '', alert.labels['instance'] || '', alert.status, ].join('|'); } /** * Gets the local IP address of the server * @private * @returns {string} Local IPv4 address or 'localhost' if not found */ private getLocalIP(): string { const interfaces = os.networkInterfaces(); for (const nets of Object.values(interfaces)) { if (!nets) continue; for (const net of nets) { if (net.family === 'IPv4' && !net.internal) { return net.address; } } } return 'localhost'; } /** * Starts the Express server * @public * @returns {void} */ public start(): void { const port = parseInt(process.env.GOTIFY_PORT || '9094', 10); const ip = this.getLocalIP(); this.app.listen(port, ip, () => { console.log({ server: `Server running on http://${ip}:${port}/`, alertEndpoint: `Alert endpoint at http://${ip}:${port}/alert`, environment: process.env.NODE_ENV || 'development', }); }); } } /** * Server instance * @type {Server} */ export const server = new Server(); /** * Starts the alert processing server * @function */ server.start(); |