├── README.md ├── tg_logmon.service ├── tg_mon.cpp └── tg_mon.py /README.md: -------------------------------------------------------------------------------- 1 | This program monitors the log file(s) and sends new lines from the log file(s) containing keyword(s) to the Telegram chat/bot. 2 | 3 | 4 | g++ -o tg_send tg_send.cpp -lcurl 5 | / 6 | g++ -std=c++17 -o tg_send tg_send.cpp -lcurl 7 | 8 | 9 | 10 | Options: 11 | 12 | 13 | --filename Path to the log file(s) 14 | 15 | --keyword Keyword(s) to watch for in the log file(s) 16 | 17 | --n Number of words to include in the message 18 | 19 | --bot-id Telegram Bot ID 20 | 21 | --chat-id Telegram Chat ID(s) 22 | 23 | -------------------------------------------------------------------------------- /tg_logmon.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Telegram Log Monitor Service 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/path/to/bin 7 | Restart=on-failure 8 | User=root 9 | Environment="HOME=/root" 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /tg_mon.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // Function to split a string by a delimiter 14 | std::vector split(const std::string& s, char delimiter) { 15 | std::vector tokens; 16 | std::string token; 17 | std::istringstream tokenStream(s); 18 | while (std::getline(tokenStream, token, delimiter)) { 19 | tokens.push_back(token); 20 | } 21 | return tokens; 22 | } 23 | 24 | // CURL write callback function 25 | size_t writeCallback(char* contents, size_t size, size_t nmemb, void* userp) { 26 | return size * nmemb; 27 | } 28 | 29 | // Function to send a message to multiple Telegram chat IDs 30 | void sendTextToTelegram(const std::string& botId, const std::vector& chatIds, const std::string& message, bool debugMode) { 31 | CURL* curl = curl_easy_init(); 32 | if (curl) { 33 | std::string url = "https://api.telegram.org/bot" + botId + "/sendMessage"; 34 | std::string escapedMessage = curl_easy_escape(curl, message.c_str(), 0); 35 | 36 | for (const auto& chatId : chatIds) { 37 | std::string data = "chat_id=" + chatId + "&text=" + escapedMessage; 38 | curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); 39 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); 40 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 41 | 42 | std::string response_string; 43 | std::string header_string; 44 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 45 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_string); 46 | curl_easy_setopt(curl, CURLOPT_HEADERDATA, &header_string); 47 | 48 | CURLcode res = curl_easy_perform(curl); 49 | if (res != CURLE_OK) { 50 | std::cerr << "Failed to send message to Telegram: " << curl_easy_strerror(res) << std::endl; 51 | } 52 | 53 | if (debugMode) { 54 | std::cout << "Response: " << response_string << std::endl; 55 | std::cout << "Headers: " << header_string << std::endl; 56 | std::cout << "Text message sent successfully to chat ID: " << chatId << std::endl; 57 | } 58 | } 59 | 60 | curl_easy_cleanup(curl); 61 | } 62 | } 63 | 64 | // Function to print the usage information 65 | void printUsage(const std::string& programName) { 66 | std::cerr << "Usage: " << programName << " --filename --keyword --n --bot-id --chat-id [--debug]" << std::endl; 67 | std::cerr << "Options:" << std::endl; 68 | std::cerr << " --filename Path to the log file(s), separated by commas" << std::endl; 69 | std::cerr << " --keyword Keyword(s) to watch for in the log file, separated by commas" << std::endl; 70 | std::cerr << " --n Number of words to include in the message" << std::endl; 71 | std::cerr << " --bot-id Telegram Bot ID" << std::endl; 72 | std::cerr << " --chat-id Telegram Chat IDs, separated by commas" << std::endl; 73 | std::cerr << " --debug Enable debug mode (optional)" << std::endl; 74 | std::cerr << std::endl; 75 | std::cerr << "If no command-line arguments are provided, the program will read configuration from ~/.config/tg_log.ini" << std::endl; 76 | std::cerr << "Ensure the configuration file exists with the following format:" << std::endl; 77 | std::cerr << "filename=" << std::endl; 78 | std::cerr << "keyword=" << std::endl; 79 | std::cerr << "n=" << std::endl; 80 | std::cerr << "bot_id=" << std::endl; 81 | std::cerr << "chat_id=" << std::endl; 82 | std::cerr << "debug=" << std::endl; 83 | } 84 | 85 | // Function to read configuration from a file 86 | bool readConfig(const std::string& configPath, std::vector& filenames, std::vector& keywords, int& n, std::string& botId, std::vector& chatIds, bool& debug) { 87 | std::ifstream config(configPath); 88 | if (!config.is_open()) { 89 | return false; 90 | } 91 | 92 | std::string line; 93 | while (std::getline(config, line)) { 94 | if (line.find("filename=") != std::string::npos) { 95 | filenames = split(line.substr(line.find("=") + 1), ','); 96 | } else if (line.find("keyword=") != std::string::npos) { 97 | keywords = split(line.substr(line.find("=") + 1), ','); 98 | } else if (line.find("n=") != std::string::npos) { 99 | n = std::stoi(line.substr(line.find("=") + 1)); 100 | } else if (line.find("bot_id=") != std::string::npos) { 101 | botId = line.substr(line.find("=") + 1); 102 | } else if (line.find("chat_id=") != std::string::npos) { 103 | chatIds = split(line.substr(line.find("=") + 1), ','); 104 | } else if (line.find("debug=") != std::string::npos) { 105 | debug = (line.substr(line.find("=") + 1) == "true"); 106 | } 107 | } 108 | 109 | config.close(); 110 | return true; 111 | } 112 | 113 | // Function to create a default configuration file 114 | void createDefaultConfig(const std::string& configPath) { 115 | std::ofstream config(configPath); 116 | config << "filename=\n"; 117 | config << "keyword=\n"; 118 | config << "n=0\n"; 119 | config << "bot_id=\n"; 120 | config << "chat_id=\n"; 121 | config << "debug=false\n"; 122 | config.close(); 123 | } 124 | 125 | int main(int argc, char* argv[]) { 126 | const char* homeEnv = getenv("HOME"); 127 | if (!homeEnv) { 128 | std::cerr << "Error: HOME environment variable is not set." << std::endl; 129 | return 1; 130 | } 131 | 132 | std::string configPath = std::string(homeEnv) + "/.config/tg_log.ini"; 133 | std::vector filenames; 134 | std::vector keywords; 135 | int n = 0; 136 | std::string botId; 137 | std::vector chatIds; 138 | bool debug = false; 139 | 140 | // Check if no command-line arguments are provided 141 | bool noArguments = (argc == 1); 142 | 143 | if (noArguments) { 144 | if (!std::filesystem::exists(configPath)) { 145 | createDefaultConfig(configPath); 146 | std::cerr << "Configuration file created at " << configPath << ". Please fill in the required parameters." << std::endl; 147 | std::cerr << "Configuration format:" << std::endl; 148 | std::cerr << "filename=" << std::endl; 149 | std::cerr << "keyword=" << std::endl; 150 | std::cerr << "n=" << std::endl; 151 | std::cerr << "bot_id=" << std::endl; 152 | std::cerr << "chat_id=" << std::endl; 153 | std::cerr << "debug=" << std::endl; 154 | return 1; 155 | } 156 | 157 | if (!readConfig(configPath, filenames, keywords, n, botId, chatIds, debug)) { 158 | std::cerr << "Failed to read configuration file." << std::endl; 159 | return 1; 160 | } 161 | } else { 162 | // Parsing command-line arguments 163 | for (int i = 1; i < argc; ++i) { 164 | std::string arg = argv[i]; 165 | if (arg == "--filename") { 166 | filenames = split(argv[++i], ','); 167 | } else if (arg == "--keyword") { 168 | keywords = split(argv[++i], ','); 169 | } else if (arg == "--n") { 170 | n = std::stoi(argv[++i]); 171 | } else if (arg == "--bot-id") { 172 | botId = argv[++i]; 173 | } else if (arg == "--chat-id") { 174 | chatIds = split(argv[++i], ','); 175 | } else if (arg == "--debug") { 176 | debug = true; 177 | } 178 | } 179 | } 180 | 181 | // Checking for missing arguments 182 | if (filenames.empty() || keywords.empty() || n == 0 || botId.empty() || chatIds.empty()) { 183 | std::cerr << "Missing arguments!" << std::endl; 184 | printUsage(argv[0]); 185 | return 1; 186 | } 187 | 188 | // Print parsed arguments for debugging 189 | if (debug) { 190 | std::cerr << "Parsed arguments:" << std::endl; 191 | std::cerr << " filenames: "; 192 | for (const auto& filename : filenames) { 193 | std::cerr << filename << " "; 194 | } 195 | std::cerr << std::endl; 196 | 197 | std::cerr << " keywords: "; 198 | for (const auto& keyword : keywords) { 199 | std::cerr << keyword << " "; 200 | } 201 | std::cerr << std::endl; 202 | 203 | std::cerr << " n: " << n << std::endl; 204 | std::cerr << " botId: " << botId << std::endl; 205 | std::cerr << " chatIds: "; 206 | for (const auto& chatId : chatIds) { 207 | std::cerr << chatId << " "; 208 | } 209 | std::cerr << std::endl; 210 | 211 | std::cerr << " debug: " << std::boolalpha << debug << std::endl; 212 | } 213 | 214 | // Initialize inotify 215 | int inotifyFd = inotify_init(); 216 | if (inotifyFd == -1) { 217 | std::cerr << "Failed to initialize inotify." << std::endl; 218 | return 1; 219 | } 220 | 221 | std::vector watchFds; 222 | for (const auto& filename : filenames) { 223 | int watchFd = inotify_add_watch(inotifyFd, filename.c_str(), IN_MODIFY | IN_MOVE_SELF | IN_DELETE_SELF); 224 | if (watchFd == -1) { 225 | std::cerr << "Failed to add inotify watch for " << filename << std::endl; 226 | close(inotifyFd); 227 | return 1; 228 | } 229 | watchFds.push_back(watchFd); 230 | } 231 | 232 | std::vector files(filenames.size()); 233 | std::vector lastPositions(filenames.size()); 234 | 235 | for (size_t i = 0; i < filenames.size(); ++i) { 236 | files[i].open(filenames[i]); 237 | if (!files[i].is_open()) { 238 | std::cerr << "Unable to open file " << filenames[i] << std::endl; 239 | return 1; 240 | } 241 | files[i].seekg(0, std::ios::end); 242 | lastPositions[i] = files[i].tellg(); 243 | } 244 | 245 | // Monitoring files in an infinite loop 246 | while (true) { 247 | char buffer[1024]; 248 | ssize_t length = read(inotifyFd, buffer, sizeof(buffer)); 249 | if (length == -1) { 250 | std::cerr << "Error reading from inotify file descriptor." << std::endl; 251 | continue; 252 | } 253 | 254 | for (char* ptr = buffer; ptr < buffer + length; ) { 255 | struct inotify_event* event = (struct inotify_event*) ptr; 256 | ptr += sizeof(struct inotify_event) + event->len; 257 | 258 | std::string filename; 259 | for (size_t i = 0; i < watchFds.size(); ++i) { 260 | if (watchFds[i] == event->wd) { 261 | filename = filenames[i]; 262 | break; 263 | } 264 | } 265 | 266 | if (event->mask & (IN_MOVE_SELF | IN_DELETE_SELF)) { 267 | if (debug) { 268 | std::cerr << "File moved or deleted: " << filename << std::endl; 269 | } 270 | for (size_t i = 0; i < watchFds.size(); ++i) { 271 | if (watchFds[i] == event->wd) { 272 | inotify_rm_watch(inotifyFd, watchFds[i]); 273 | watchFds[i] = inotify_add_watch(inotifyFd, filenames[i].c_str(), IN_MODIFY | IN_MOVE_SELF | IN_DELETE_SELF); 274 | if (watchFds[i] == -1) { 275 | std::cerr << "Failed to add inotify watch for " << filenames[i] << std::endl; 276 | close(inotifyFd); 277 | return 1; 278 | } 279 | files[i].close(); 280 | files[i].clear(); 281 | files[i].open(filenames[i]); 282 | if (!files[i].is_open()) { 283 | std::cerr << "Unable to open file " << filenames[i] << std::endl; 284 | return 1; 285 | } 286 | files[i].seekg(0, std::ios::end); 287 | lastPositions[i] = files[i].tellg(); 288 | } 289 | } 290 | } else if (event->mask & IN_MODIFY) { 291 | if (debug) { 292 | std::cerr << "File modified: " << filename << std::endl; 293 | } 294 | 295 | for (size_t i = 0; i < watchFds.size(); ++i) { 296 | if (watchFds[i] == event->wd) { 297 | if (files[i].tellg() < lastPositions[i]) { 298 | if (debug) { 299 | std::cerr << "File truncated: " << filenames[i] << std::endl; 300 | } 301 | files[i].seekg(0, std::ios::end); 302 | lastPositions[i] = files[i].tellg(); 303 | } 304 | 305 | // Checking each line for keywords 306 | std::string line; 307 | while (std::getline(files[i], line)) { 308 | for (const auto& keyword : keywords) { 309 | if (line.find(keyword) != std::string::npos) { 310 | std::vector words = split(line, ' '); 311 | std::ostringstream messageToSend; 312 | for (int j = 0; j < std::min(static_cast(words.size()), n); ++j) { 313 | messageToSend << words[j] << " "; 314 | } 315 | sendTextToTelegram(botId, chatIds, messageToSend.str(), debug); 316 | if (debug) { 317 | std::cerr << "Sent message to Telegram: " << messageToSend.str() << std::endl; 318 | } 319 | break; 320 | } 321 | } 322 | } 323 | files[i].clear(); 324 | } 325 | } 326 | } 327 | } 328 | std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 329 | } 330 | 331 | close(inotifyFd); 332 | return 0; 333 | } 334 | -------------------------------------------------------------------------------- /tg_mon.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import time 4 | import requests 5 | import logging 6 | from watchdog.observers import Observer 7 | from watchdog.events import FileSystemEventHandler 8 | 9 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 10 | 11 | def send_text_to_telegram(bot_id, chat_ids, message, debug_mode): 12 | url = f"https://api.telegram.org/bot{bot_id}/sendMessage" 13 | for chat_id in chat_ids: 14 | payload = { 15 | 'chat_id': chat_id, 16 | 'text': message 17 | } 18 | response = requests.post(url, data=payload) 19 | if debug_mode: 20 | logging.info(f"Sent message to chat ID {chat_id}: {message}") 21 | logging.info(f"Response: {response.text}") 22 | 23 | class LogFileEventHandler(FileSystemEventHandler): 24 | def __init__(self, keywords, bot_id, chat_ids, n, debug_mode): 25 | self.keywords = keywords 26 | self.bot_id = bot_id 27 | self.chat_ids = chat_ids 28 | self.n = n 29 | self.debug_mode = debug_mode 30 | self.files = {} 31 | self.positions = {} 32 | 33 | def on_modified(self, event): 34 | if event.is_directory: 35 | return 36 | if self.debug_mode: 37 | logging.info(f"File modified: {event.src_path}") 38 | self.check_file(event.src_path) 39 | 40 | def check_file(self, file_path): 41 | if file_path not in self.files: 42 | self.files[file_path] = open(file_path, 'r') 43 | self.files[file_path].seek(0, os.SEEK_END) # Move to the end of the file 44 | self.positions[file_path] = self.files[file_path].tell() 45 | 46 | file = self.files[file_path] 47 | file.seek(self.positions[file_path]) 48 | for line in file: 49 | for keyword in self.keywords: 50 | if keyword in line: 51 | words = line.split() 52 | message = ' '.join(words[:self.n]) 53 | send_text_to_telegram(self.bot_id, self.chat_ids, message, self.debug_mode) 54 | if self.debug_mode: 55 | logging.info(f"Detected keyword '{keyword}' in line: {line.strip()}") 56 | break 57 | self.positions[file_path] = file.tell() 58 | 59 | def read_config(config_path): 60 | config = {} 61 | with open(config_path, 'r') as f: 62 | for line in f: 63 | if '=' in line: 64 | key, value = line.strip().split('=', 1) 65 | config[key.strip()] = value.strip() 66 | 67 | filenames = config['filename'].split(',') 68 | keywords = config['keyword'].split(',') 69 | n = int(config['n']) 70 | bot_id = config['bot_id'] 71 | chat_ids = config['chat_id'].split(',') 72 | debug = config['debug'].lower() == 'true' 73 | return filenames, keywords, n, bot_id, chat_ids, debug 74 | 75 | def create_default_config(config_path): 76 | with open(config_path, 'w') as configfile: 77 | configfile.write('filename=\n') 78 | configfile.write('keyword=\n') 79 | configfile.write('n=0\n') 80 | configfile.write('bot_id=\n') 81 | configfile.write('chat_id=\n') 82 | configfile.write('debug=false\n') 83 | 84 | def parse_arguments(): 85 | parser = argparse.ArgumentParser(description='Monitor log files and send alerts to Telegram.') 86 | parser.add_argument('--filename', type=str, help='Path to the log file(s), separated by commas') 87 | parser.add_argument('--keyword', type=str, help='Keyword(s) to watch for in the log file, separated by commas') 88 | parser.add_argument('--n', type=int, help='Number of words to include in the message') 89 | parser.add_argument('--bot-id', type=str, help='Telegram Bot ID') 90 | parser.add_argument('--chat-id', type=str, help='Telegram Chat IDs, separated by commas') 91 | parser.add_argument('--debug', action='store_true', help='Enable debug mode (optional)') 92 | return parser.parse_args() 93 | 94 | def main(): 95 | home = os.path.expanduser("~") 96 | config_path = os.path.join(home, '.config', 'tg_log.ini') 97 | 98 | args = parse_arguments() 99 | 100 | if not any(vars(args).values()): 101 | if not os.path.exists(config_path): 102 | create_default_config(config_path) 103 | logging.error(f"Configuration file created at {config_path}. Please fill in the required parameters.") 104 | return 105 | 106 | filenames, keywords, n, bot_id, chat_ids, debug = read_config(config_path) 107 | else: 108 | filenames = args.filename.split(',') 109 | keywords = args.keyword.split(',') 110 | n = args.n 111 | bot_id = args.bot_id 112 | chat_ids = args.chat_id.split(',') 113 | debug = args.debug 114 | 115 | if not filenames or not keywords or not n or not bot_id or not chat_ids: 116 | logging.error("Missing arguments! Please provide all required arguments.") 117 | return 118 | 119 | event_handler = LogFileEventHandler(keywords, bot_id, chat_ids, n, debug) 120 | observer = Observer() 121 | 122 | for filename in filenames: 123 | observer.schedule(event_handler, path=filename, recursive=False) 124 | 125 | observer.start() 126 | try: 127 | while True: 128 | time.sleep(1) 129 | except KeyboardInterrupt: 130 | observer.stop() 131 | observer.join() 132 | 133 | if __name__ == "__main__": 134 | main() 135 | --------------------------------------------------------------------------------