Initial Release
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { AlertConfiguration } from "../alertConfigService";
|
||||
import { NotificationTemplate } from "../templateService";
|
||||
import { NotificationPayload } from "./types";
|
||||
import { processTemplate, generateDefaultMessage } from "./templateProcessor";
|
||||
import { sendTelegramNotification, testSendTelegramMessage } from "./telegramService";
|
||||
import { sendSignalNotification } from "./signalService";
|
||||
|
||||
// Track last notification times for services to implement cooldown
|
||||
const lastNotifications: Record<string, {
|
||||
timestamp: Date;
|
||||
count: number;
|
||||
lastMessageTime: Date; // Track when the last message was actually sent
|
||||
}> = {};
|
||||
|
||||
// Cooldown period in milliseconds (5 minutes)
|
||||
const NOTIFICATION_COOLDOWN = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Notification service responsible for sending notifications based on service status changes
|
||||
*/
|
||||
export const notificationService = {
|
||||
/**
|
||||
* Send notification based on service status change
|
||||
*/
|
||||
async sendNotification(payload: NotificationPayload): Promise<boolean> {
|
||||
try {
|
||||
console.log("=== NOTIFICATION SERVICE TRIGGER ===");
|
||||
console.log("Sending notification for service:", payload.service.name, "Status:", payload.status);
|
||||
console.log("Service type:", payload.service.type);
|
||||
console.log("Service retries:", payload.service.retries || 3);
|
||||
console.log("Alerts status:", payload.service.alerts);
|
||||
|
||||
// First check: Strictly check if alerts are muted for this specific service
|
||||
if (payload.service.alerts === "muted") {
|
||||
console.log(`BLOCKING NOTIFICATION: Alerts are explicitly muted for service ${payload.service.name} (ID: ${payload.service.id})`);
|
||||
return true; // Return true as this is expected behavior, not a failure
|
||||
}
|
||||
|
||||
// For DOWN status, implement the cooldown and max retries logic
|
||||
if (payload.status === "down") {
|
||||
const serviceId = payload.service.id;
|
||||
// Get max retries from service config or default to 3
|
||||
const maxRetries = typeof payload.service.retries === 'number' ? payload.service.retries : 3;
|
||||
const now = new Date();
|
||||
|
||||
console.log(`DOWN notification for ${payload.service.name} - Max retries: ${maxRetries}`);
|
||||
|
||||
if (lastNotifications[serviceId]) {
|
||||
const lastNotif = lastNotifications[serviceId];
|
||||
const timeSinceLastNotif = now.getTime() - lastNotif.timestamp.getTime();
|
||||
|
||||
console.log(`Time since last notification tracking for ${payload.service.name}: ${Math.round(timeSinceLastNotif/1000)}s`);
|
||||
console.log(`Current notification count: ${lastNotif.count}/${maxRetries}`);
|
||||
|
||||
// Check if we're within the cooldown period
|
||||
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
|
||||
// If max retries is set to 1, we should only send one notification during the cooldown period
|
||||
if (maxRetries === 1) {
|
||||
console.log(`Max retries is 1 - BLOCKING duplicate DOWN notification for ${payload.service.name}`);
|
||||
return true; // Skip notification but return success
|
||||
}
|
||||
|
||||
// Check if we've reached max retries
|
||||
if (lastNotif.count >= maxRetries) {
|
||||
console.log(`DOWN notification for ${payload.service.name} skipped: Max retries (${maxRetries}) reached. Next notification after cooldown.`);
|
||||
return true; // Skip notification but return success
|
||||
}
|
||||
|
||||
// Increment count since we're sending another notification
|
||||
console.log(`DOWN notification for ${payload.service.name}: ${lastNotif.count + 1}/${maxRetries}`);
|
||||
lastNotifications[serviceId].count += 1;
|
||||
lastNotifications[serviceId].lastMessageTime = now;
|
||||
} else {
|
||||
// Reset count after cooldown period
|
||||
console.log(`Cooldown period elapsed for ${payload.service.name}. Resetting notification count.`);
|
||||
lastNotifications[serviceId] = { timestamp: now, count: 1, lastMessageTime: now };
|
||||
}
|
||||
} else {
|
||||
// First notification for this service
|
||||
console.log(`First DOWN notification for ${payload.service.name}: 1/${maxRetries}`);
|
||||
lastNotifications[serviceId] = { timestamp: now, count: 1, lastMessageTime: now };
|
||||
}
|
||||
}
|
||||
|
||||
// Check if service has notification channel configured
|
||||
const notificationChannel = payload.service.notificationChannel;
|
||||
|
||||
if (!notificationChannel || notificationChannel === "none") {
|
||||
console.log("No notification channel configured for service", payload.service.name);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Using notification channel ID:", notificationChannel);
|
||||
|
||||
try {
|
||||
// Get the alert configuration details
|
||||
console.log("Fetching alert configuration with ID:", notificationChannel);
|
||||
const alertConfig = await pb.collection('alert_configurations').getOne(notificationChannel);
|
||||
console.log("Found alert configuration:", JSON.stringify({
|
||||
...alertConfig,
|
||||
bot_token: alertConfig.bot_token ? "[REDACTED]" : undefined
|
||||
}, null, 2));
|
||||
|
||||
// Check enabled status - skip if explicitly disabled
|
||||
if (alertConfig.enabled === false || alertConfig.enabled === "false") {
|
||||
console.log("Alert configuration is disabled, skipping notification");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare message content
|
||||
let messageContent = await this.prepareMessageContent(
|
||||
payload.service,
|
||||
payload.status,
|
||||
payload.responseTime,
|
||||
payload.message
|
||||
);
|
||||
|
||||
// For DOWN status, add retry information to the message
|
||||
if (payload.status === "down" && lastNotifications[payload.service.id]) {
|
||||
const retryInfo = lastNotifications[payload.service.id];
|
||||
const maxRetries = typeof payload.service.retries === 'number' ? payload.service.retries : 3;
|
||||
if (maxRetries > 1) {
|
||||
messageContent += `\n\nAlert ${retryInfo.count}/${maxRetries}`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Prepared message for ${payload.status} notification:`, messageContent);
|
||||
|
||||
// Send notification based on type
|
||||
if (alertConfig.notification_type === "telegram") {
|
||||
console.log("Sending Telegram notification for service status change");
|
||||
return await sendTelegramNotification(alertConfig as AlertConfiguration, messageContent);
|
||||
} else if (alertConfig.notification_type === "signal") {
|
||||
console.log("Sending Signal notification with message:", messageContent);
|
||||
return await sendSignalNotification(alertConfig as AlertConfiguration, messageContent);
|
||||
}
|
||||
|
||||
// Add other notification types here
|
||||
console.log("Notification type not supported:", alertConfig.notification_type);
|
||||
toast({
|
||||
title: "Notification Error",
|
||||
description: `Notification type '${alertConfig.notification_type}' not supported`,
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error processing notification channel:", error);
|
||||
console.error("Error details:", error instanceof Error ? error.message : "Unknown error");
|
||||
|
||||
toast({
|
||||
title: "Notification Error",
|
||||
description: `Error processing notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending notification:", error);
|
||||
toast({
|
||||
title: "Notification Error",
|
||||
description: `Failed to send notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare message content for notification
|
||||
*/
|
||||
async prepareMessageContent(
|
||||
service: any,
|
||||
status: string,
|
||||
responseTime?: number,
|
||||
defaultMessage?: string
|
||||
): Promise<string> {
|
||||
// Use provided message if available
|
||||
if (defaultMessage) {
|
||||
return defaultMessage;
|
||||
}
|
||||
|
||||
// Check for a template
|
||||
let templateId = service.alertTemplate;
|
||||
|
||||
if (templateId && templateId !== "default") {
|
||||
try {
|
||||
console.log("Using specified template ID:", templateId);
|
||||
const template = await pb.collection('notification_templates').getOne(templateId);
|
||||
console.log("Template data:", template);
|
||||
return processTemplate(
|
||||
template as unknown as NotificationTemplate,
|
||||
service,
|
||||
status,
|
||||
responseTime
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching template:", error);
|
||||
// Fall back to default message format
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Using default message template");
|
||||
return generateDefaultMessage(service.name, status, responseTime);
|
||||
},
|
||||
|
||||
/**
|
||||
* Test method to directly send a Telegram message
|
||||
*/
|
||||
async testTelegramNotification(message?: string): Promise<boolean> {
|
||||
return await testSendTelegramMessage(
|
||||
"-1002471970362",
|
||||
"7581526325:AAFZgmn9hzc3dpBWl9uLUhcqXRDx5D16e48",
|
||||
message || "This is a test notification from the monitoring system."
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a test notification for a specific service status change
|
||||
*/
|
||||
async testServiceStatusNotification(serviceName: string, status: "up" | "down", responseTime?: number): Promise<boolean> {
|
||||
const emoji = status === "up" ? "🟢" : "🔴";
|
||||
const rtText = responseTime ? ` (Response time: ${responseTime}ms)` : "";
|
||||
const message = `${emoji} Test notification: Service ${serviceName} is ${status.toUpperCase()}${rtText}`;
|
||||
|
||||
console.log("Sending test service status notification:", message);
|
||||
return await this.testTelegramNotification(message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset notification count for a service
|
||||
* This is called when a service recovers from DOWN to UP
|
||||
*/
|
||||
resetNotificationCount(serviceId: string): void {
|
||||
if (lastNotifications[serviceId]) {
|
||||
console.log(`Resetting notification count for service ${serviceId}`);
|
||||
delete lastNotifications[serviceId];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Re-export the types for easier imports
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { AlertConfiguration } from "../alertConfigService";
|
||||
import api from "@/api";
|
||||
|
||||
/**
|
||||
* Send a notification via Signal
|
||||
*/
|
||||
export async function sendSignalNotification(
|
||||
config: AlertConfiguration,
|
||||
message: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!config.signal_number) {
|
||||
console.log("Missing Signal configuration - Signal number:", config.signal_number);
|
||||
toast({
|
||||
title: "Configuration Error",
|
||||
description: "Missing Signal number",
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Sending Signal notification to number:", config.signal_number);
|
||||
console.log("Message content:", message);
|
||||
|
||||
// Prepare payload for the API call
|
||||
const payload = {
|
||||
type: "signal",
|
||||
signalNumber: config.signal_number,
|
||||
message: message
|
||||
};
|
||||
|
||||
console.log("Prepared payload for Signal notification:", payload);
|
||||
|
||||
try {
|
||||
// Call our client-side API handler
|
||||
console.log("Sending request to /api/realtime endpoint for Signal");
|
||||
const response = await api.handleRequest('/api/realtime', 'POST', payload);
|
||||
|
||||
console.log("API response status:", response.status);
|
||||
|
||||
// Check if response is ok
|
||||
if (response.status !== 200) {
|
||||
console.error("Error response from Signal notification API:", response.status);
|
||||
toast({
|
||||
title: "Notification Failed",
|
||||
description: `Server returned error ${response.status}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const responseData = response.json;
|
||||
console.log("API response data:", responseData);
|
||||
|
||||
if (responseData && responseData.ok === false) {
|
||||
console.error("Error sending Signal notification:", responseData);
|
||||
toast({
|
||||
title: "Notification Failed",
|
||||
description: responseData.description || "Failed to send Signal notification",
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Signal notification sent successfully");
|
||||
toast({
|
||||
title: "Notification Sent",
|
||||
description: "Signal notification sent successfully"
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error calling Signal notification API:", error);
|
||||
toast({
|
||||
title: "API Error",
|
||||
description: `Failed to communicate with Signal notification service: ${error instanceof Error ? error.message : "Network error"}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in sendSignalNotification:", error);
|
||||
toast({
|
||||
title: "Notification Error",
|
||||
description: `Error sending Signal notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { AlertConfiguration } from "../alertConfigService";
|
||||
import api from "@/api";
|
||||
|
||||
/**
|
||||
* Send a notification via Telegram
|
||||
*/
|
||||
export async function sendTelegramNotification(
|
||||
config: AlertConfiguration,
|
||||
message: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
console.log("====== TELEGRAM NOTIFICATION ATTEMPT ======");
|
||||
console.log("Config:", JSON.stringify({
|
||||
...config,
|
||||
notify_name: config.notify_name,
|
||||
telegram_chat_id: config.telegram_chat_id,
|
||||
bot_token: config.bot_token ? "[REDACTED]" : undefined,
|
||||
enabled: config.enabled
|
||||
}, null, 2));
|
||||
|
||||
// Use provided credentials if available, otherwise use config
|
||||
const chatId = config.telegram_chat_id || "-1002471970362";
|
||||
const botToken = config.bot_token || "7581526325:AAFZgmn9hzc3dpBWl9uLUhcqXRDx5D16e48";
|
||||
|
||||
if (!chatId || !botToken) {
|
||||
console.error("Missing Telegram configuration - Chat ID:", chatId, "Bot token present:", !!botToken);
|
||||
toast({
|
||||
title: "Configuration Error",
|
||||
description: "Missing Telegram chat ID or bot token",
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Sending Telegram notification to chat ID:", chatId);
|
||||
console.log("Bot token available:", !!botToken);
|
||||
console.log("Message content:", message);
|
||||
|
||||
// Prepare payload for the API call
|
||||
const payload = {
|
||||
type: "telegram",
|
||||
chatId: chatId,
|
||||
botToken: botToken,
|
||||
message: message
|
||||
};
|
||||
|
||||
console.log("Prepared payload for notification:", {
|
||||
...payload,
|
||||
botToken: "[REDACTED]"
|
||||
});
|
||||
|
||||
try {
|
||||
// Call our client-side API handler
|
||||
console.log("Sending request to /api/realtime endpoint");
|
||||
const response = await api.handleRequest('/api/realtime', 'POST', payload);
|
||||
|
||||
console.log("API response status:", response.status);
|
||||
console.log("API response data:", JSON.stringify(response.json, null, 2));
|
||||
|
||||
// Check if response is ok
|
||||
if (response.status !== 200) {
|
||||
console.error("Error response from notification API:", response.status);
|
||||
toast({
|
||||
title: "Notification Failed",
|
||||
description: `Server returned error ${response.status}: ${response.json?.description || "Unknown error"}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const responseData = response.json;
|
||||
|
||||
if (responseData && responseData.ok === false) {
|
||||
console.error("Error sending notification:", responseData);
|
||||
toast({
|
||||
title: "Notification Failed",
|
||||
description: responseData.description || "Failed to send notification",
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Notification sent successfully");
|
||||
toast({
|
||||
title: "Notification Sent",
|
||||
description: "Telegram notification sent successfully"
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error calling notification API:", error);
|
||||
toast({
|
||||
title: "API Error",
|
||||
description: `Failed to communicate with notification service: ${error instanceof Error ? error.message : "Network error"}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in sendTelegramNotification:", error);
|
||||
toast({
|
||||
title: "Notification Error",
|
||||
description: `Error sending Telegram notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test function to send a direct Telegram message
|
||||
* without requiring configuration from the database
|
||||
*/
|
||||
export async function testSendTelegramMessage(
|
||||
chatId: string = "-1002471970362",
|
||||
botToken: string = "7581526325:AAFZgmn9hzc3dpBWl9uLUhcqXRDx5D16e48",
|
||||
message: string = "This is a test message from the monitoring system"
|
||||
): Promise<boolean> {
|
||||
console.log("====== DIRECT TELEGRAM TEST ======");
|
||||
console.log(`Testing Telegram notification with chat ID: ${chatId}`);
|
||||
|
||||
// Create a minimal config with just the required fields
|
||||
const testConfig: AlertConfiguration = {
|
||||
service_id: "test",
|
||||
notification_type: "telegram",
|
||||
telegram_chat_id: chatId,
|
||||
bot_token: botToken,
|
||||
notify_name: "Test Direct Notification",
|
||||
enabled: true
|
||||
};
|
||||
|
||||
console.log("Sending test message with content:", message);
|
||||
return await sendTelegramNotification(testConfig, message);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
import { NotificationTemplate } from "../templateService";
|
||||
|
||||
/**
|
||||
* Process a notification template with service data
|
||||
*/
|
||||
export function processTemplate(
|
||||
template: NotificationTemplate,
|
||||
service: any,
|
||||
status: string,
|
||||
responseTime?: number
|
||||
): string {
|
||||
try {
|
||||
console.log(`Processing template for status: ${status}`);
|
||||
|
||||
let templateText = "";
|
||||
|
||||
// Select the appropriate message template based on status
|
||||
if (status === "up") {
|
||||
templateText = template.up_message || `Service ${service.name} is now UP`;
|
||||
} else if (status === "down") {
|
||||
templateText = template.down_message || `Service ${service.name} is DOWN`;
|
||||
} else if (status === "warning") {
|
||||
templateText = template.incident_message || `Warning: Service ${service.name} has an incident`;
|
||||
} else if (status === "maintenance" || status === "paused") {
|
||||
templateText = template.maintenance_message || `Service ${service.name} is in maintenance mode`;
|
||||
} else if (status === "resolved") {
|
||||
templateText = template.resolved_message || `Issue with service ${service.name} has been resolved`;
|
||||
} else {
|
||||
templateText = `Service ${service.name} status changed to: ${status}`;
|
||||
}
|
||||
|
||||
// Skip replacement if template is empty
|
||||
if (!templateText) {
|
||||
console.log("Empty template for status:", status);
|
||||
return generateDefaultMessage(service.name, status, responseTime);
|
||||
}
|
||||
|
||||
console.log("Using template text:", templateText);
|
||||
|
||||
// Replace placeholders with actual values
|
||||
let message = templateText
|
||||
.replace(/\${service_name}/g, service.name || 'Unknown Service')
|
||||
.replace(/\${status}/g, status.toUpperCase());
|
||||
|
||||
// Add response time if available
|
||||
if (responseTime !== undefined) {
|
||||
message = message.replace(/\${response_time}/g, `${responseTime}ms`);
|
||||
} else {
|
||||
message = message.replace(/\${response_time}/g, 'N/A');
|
||||
}
|
||||
|
||||
// Replace any other placeholders
|
||||
message = message
|
||||
.replace(/\${threshold}/g, service.threshold || 'N/A')
|
||||
.replace(/\${url}/g, service.url || 'N/A')
|
||||
.replace(/\${time}/g, new Date().toLocaleString());
|
||||
|
||||
console.log("Processed template message:", message);
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.error("Error processing template:", error);
|
||||
return generateDefaultMessage(service.name, status, responseTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a default message when no template is available
|
||||
*/
|
||||
export function generateDefaultMessage(
|
||||
serviceName: string,
|
||||
status: string,
|
||||
responseTime?: number
|
||||
): string {
|
||||
const statusText = status.toUpperCase();
|
||||
|
||||
let message = `Service ${serviceName || 'Unknown'} is ${statusText}`;
|
||||
|
||||
if (responseTime !== undefined) {
|
||||
message += `. Response time: ${responseTime}ms`;
|
||||
}
|
||||
|
||||
message += `. Time: ${new Date().toLocaleString()}`;
|
||||
|
||||
return message;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
import { Service } from "@/types/service.types";
|
||||
|
||||
export interface NotificationPayload {
|
||||
service: Service;
|
||||
status: string;
|
||||
responseTime?: number;
|
||||
timestamp: string;
|
||||
message?: string;
|
||||
_notificationSource?: string; // Internal flag to prevent duplicate notifications
|
||||
}
|
||||
Reference in New Issue
Block a user