├── .gitignore ├── lla-saasscaleup.png ├── .github └── FUNDING.yml ├── src ├── LogAlarmServiceProvider.php ├── config │ └── log-alarm.php ├── LogHandler.php └── NotificationService.php ├── LICENSE ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lla-saasscaleup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasscaleup/laravel-log-alarm/HEAD/lla-saasscaleup.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: saasscaleup 4 | ko_fi: scaleupsaas 5 | custom: https://buymeacoffee.com/scaleupsaas 6 | -------------------------------------------------------------------------------- /src/LogAlarmServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 16 | // Publish the configuration file. 17 | $this->publishes([ 18 | __DIR__ . '/config/log-alarm.php' => config_path('log-alarm.php'), 19 | ]); 20 | } 21 | 22 | 23 | if (config('log-alarm.enabled')){ 24 | Log::listen(function (MessageLogged $event) { 25 | app(LogHandler::class)->handle($event); 26 | }); 27 | } 28 | } 29 | 30 | public function register(){ 31 | 32 | } 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Scale-Up SaaS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saasscaleup/laravel-log-alarm", 3 | "description": "Laravel log Alarm help you to set up alarm when errors occur in your system and send you a notification via Slack and email", 4 | "license": "MIT", 5 | "homepage": "https://github.com/saasscaleup/laravel-log-alarm", 6 | "keywords": [ 7 | "laravel", 8 | "log", 9 | "alarm", 10 | "error", 11 | "notification", 12 | "slack", 13 | "email", 14 | "logging", 15 | "monitoring", 16 | "alert", 17 | "laravel-log", 18 | "laravel-error", 19 | "laravel-notification", 20 | "laravel-slack", 21 | "laravel-email", 22 | "laravel-monitoring", 23 | "laravel-alert", 24 | "laravel alarm" 25 | ], 26 | "autoload": { 27 | "psr-4": { 28 | "Saasscaleup\\LogAlarm\\": "src/" 29 | } 30 | }, 31 | "extra": { 32 | "laravel":{ 33 | "providers":[ 34 | "Saasscaleup\\LogAlarm\\LogAlarmServiceProvider" 35 | ] 36 | } 37 | }, 38 | "authors": [ 39 | { 40 | "name": "Scale-Up SaaS", 41 | "email": "saasscaleup@gmail.com" 42 | } 43 | ], 44 | "require": {} 45 | } 46 | -------------------------------------------------------------------------------- /src/config/log-alarm.php: -------------------------------------------------------------------------------- 1 | env('LA_ENABLED', true), 7 | 8 | // log listener for specific log type 9 | 'log_type' => env('LA_LOG_TYPE', 'error'), // also possible: 'error,warning,debug' 10 | 11 | // log time frame - log time frame to listen - in minutes 12 | "log_time_frame" => (int)env('LA_LOG_TIME_FRAME', 1), 13 | 14 | // log per time frame - How many log to count per time frame until alarm trigger 15 | "log_per_time_frame" => (int)env('LA_LOG_PER_TIME_FRAME', 5), 16 | 17 | // delay between alarms in minutes - How many minutes to delay between alarms 18 | 'delay_between_alarms' => (int)env('LA_DELAY_BETWEEN_ALARMS', 5), 19 | 20 | // log listener for specific word inside log messages 21 | 'specific_string' => env('LA_SPECIFIC_STRING', ''), // also possible: 'table lock' or 'foo' or 'bar' or leave empty '' to enable any word 22 | 23 | // notification message for log alarm 24 | 'notification_message' => env('LA_NOTIFICATION_MESSAGE', ''), // Leave empty '' to enable error log triggered alarm 25 | 26 | // Slack webhook url for log alarm 27 | 'slack_webhook_url' => env('LA_SLACK_WEBHOOK_URL', ''), 28 | 29 | 30 | // Discord webhook url for log alarm 31 | 'discord_webhook_url' => env('LA_DISCORD_WEBHOOK_URL', ''), 32 | 33 | // notification email address for log alarm 34 | 'notification_email' => env('LA_NOTIFICATION_EMAIL', 'admin@example.com'), // also possible: 'admin@example.com,admin2@example.com' 35 | 36 | // notification email subject for log alarm 37 | 'notification_email_subject' => env('LA_NOTIFICATION_EMAIL_SUBJECT', 'Log Alarm Notification'), 38 | 39 | // Telegram Bot notification 40 | 'telegram_bot_token' => env('LA_TELEGRAM_BOT_TOKEN', ''), 41 | 'telegram_chat_id' => env('LA_TELEGRAM_CHAT_ID', ''), 42 | ]; 43 | -------------------------------------------------------------------------------- /src/LogHandler.php: -------------------------------------------------------------------------------- 1 | level,$log_types)) { 30 | 31 | // Check if the message contains the specific string specified in the config file 32 | if ($this->containsSpecificString($event->message)) { 33 | 34 | // Call the logError method to handle the error 35 | $this->logError($event); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * containsSpecificString 42 | * 43 | * This method checks if the provided log message contains the specific string 44 | * specified in the config file. It does this by using the strpos() function 45 | * to search for the position of the specific string within the message. If the 46 | * specific string is found, the method returns true. If the specific string is 47 | * not found or if the message is empty, the method returns false. 48 | * 49 | * @param string $message The log message to search for the specific string. 50 | * @return bool Returns true if the specific string is found in the message, 51 | * false otherwise. 52 | */ 53 | protected function containsSpecificString($message) 54 | { 55 | // Get the specific string from the config file. If the specific string is not 56 | // specified in the config file, an empty string is used. 57 | $specificString = config('log-alarm.specific_string', ''); 58 | 59 | // Check if the specific string is found within the message using the strpos() function. 60 | // If the specific string is found, strpos() returns the position of the first occurrence 61 | // of the specific string within the message. If the specific string is not found, 62 | // strpos() returns false. 63 | return empty($specificString) ? true : strpos($message, $specificString) !== false; 64 | } 65 | 66 | /** 67 | * Constructs a detailed log alarm message from a MessageLogged event. 68 | * 69 | * This method extracts information such as the log level, log message, 70 | * file, and line number from the provided MessageLogged event. If an 71 | * exception is present in the event's context, the file and line number 72 | * are retrieved from the exception; otherwise, they default to 'Unknown'. 73 | * 74 | * @param MessageLogged $event The log event containing details such as 75 | * level, message, and context. 76 | * @return string A formatted string containing the log level, message, 77 | * file, and line number. 78 | */ 79 | protected function getLogAlarmMessage(MessageLogged $event){ 80 | 81 | $log_level = $event->level; 82 | $log_message = $event->message; 83 | $log_file = 'Unknown file'; 84 | $log_line = 'Unknown line'; 85 | 86 | try { 87 | // Check if 'exception' exists in the context 88 | if (isset($event->context['exception']) && $event->context['exception'] instanceof \Exception) { 89 | $exception = $event->context['exception']; 90 | $log_file = $exception->getFile(); 91 | $log_line = $exception->getLine(); 92 | } 93 | } catch (\Exception $e) { 94 | // Do nothing 95 | } 96 | 97 | return "LOG_LEVEL: {$log_level}\r\nLOG_MESSAGE: {$log_message}\r\nLOG_FILE: {$log_file}\r\nLOG_LINE: {$log_line}"; 98 | } 99 | /** 100 | * logError 101 | * 102 | * This method handles the logging of an error. It retrieves the current error logs 103 | * from the cache, adds a new log with the current timestamp, filters out logs that are 104 | * older than the specified time, saves the updated logs back to the cache, checks if 105 | * there have been a specified number of error logs in the last minute, and if so, sends 106 | * a notification and updates the time of the last notification. 107 | * 108 | * @param MessageLogged $event The event that triggered the logging of the error. 109 | * @return void 110 | */ 111 | protected function logError(MessageLogged $event) 112 | { 113 | // Get the log alarm message 114 | $log_message = $this->getLogAlarmMessage($event); 115 | 116 | // Generate a unique cache key based on the log message 117 | $log_alarm_cache_key_enc = md5($log_message); 118 | 119 | // Retrieve the current error logs from the cache or initialize an empty array if no logs exist 120 | $errorLogs = Cache::get($log_alarm_cache_key_enc, []); 121 | 122 | // Add a new log with the current timestamp to the array of error logs 123 | $errorLogs[] = Carbon::now(); 124 | 125 | // Get the time in minutes to consider an error log as recent 126 | $log_time_frame = config('log-alarm.log_time_frame'); 127 | 128 | // Get specified number of error logs in time frame 129 | $log_per_time_frame = config('log-alarm.log_per_time_frame'); 130 | 131 | // Filter out logs that are older than the specified time frame 132 | $errorLogs = array_filter($errorLogs, function ($timestamp) use ($log_time_frame) { 133 | return $timestamp >= Carbon::now()->subMinutes($log_time_frame); 134 | }); 135 | 136 | // Save the updated logs back to the cache with an expiration time of 1 minute 137 | Cache::put($log_alarm_cache_key_enc, $errorLogs, Carbon::now()->addMinutes($log_time_frame)); 138 | 139 | // Check if there have been a specified number of error logs in time frame (in the last minute for example) 140 | if (count($errorLogs) >= $log_per_time_frame) { 141 | 142 | // Retrieve the time of the last notification from the cache or initialize null if no notification time exists 143 | $last_notification_time = Cache::get($this->notification_cache_key.'_'.$log_alarm_cache_key_enc); 144 | 145 | // Get the delay between notifications from the config file 146 | $delay_between_alarms = config('log-alarm.delay_between_alarms'); 147 | 148 | // Send notification only if last notification was sent more than 5 minutes ago 149 | // The Carbon library is used to compare the current time with the time of the last notification 150 | if (!$last_notification_time || Carbon::now()->diffInMinutes($last_notification_time) >= $delay_between_alarms) { 151 | 152 | // Get the message to be sent in the notification from the config file 153 | $message = empty(config('log-alarm.notification_message')) ? $log_message : config('log-alarm.notification_message'); 154 | 155 | $message = "The Error was occurred {$log_per_time_frame} times in the last {$log_time_frame} minutes: \r\n\r\n{$message}"; 156 | 157 | // Send the notification 158 | NotificationService::send($message); 159 | 160 | // Update the time of the last notification in the cache 161 | // The notification is set to expire in the delay between alarms specified in the config file 162 | Cache::put($this->notification_cache_key.'_'.$log_alarm_cache_key_enc, Carbon::now(), Carbon::now()->addMinutes($delay_between_alarms)); 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/NotificationService.php: -------------------------------------------------------------------------------- 1 | config('log-alarm.notification_email_subject') , 79 | 'attachments' => [ 80 | [ 81 | 'title' => config('log-alarm.notification_email_subject'), 82 | 'text' => $message, 83 | 'color' => '#FF0000', 84 | 'fields' => [ 85 | [ 86 | 'title' => 'Priority', 87 | 'value' => 'High', 88 | 'short' => true 89 | ] 90 | ] 91 | ] 92 | ] 93 | ]; 94 | 95 | // Initialize the cURL session 96 | $ch = curl_init($webhookUrl); 97 | 98 | // Set the request method, request body, and options 99 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); 100 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); 101 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 102 | curl_setopt($ch, CURLOPT_HTTPHEADER, [ 103 | 'Content-Type: application/json', 104 | 'Content-Length: ' . strlen(json_encode($data)) 105 | ]); 106 | 107 | // Execute the request and get the response 108 | $result = curl_exec($ch); 109 | 110 | // Close the cURL session 111 | curl_close($ch); 112 | 113 | // If the request failed, log the error 114 | if ($result === false) { 115 | Log::info("LogAlarm::sendSlackNotification->error: " . curl_error($ch)); 116 | } 117 | } 118 | 119 | /** 120 | * Send a notification via email. 121 | * 122 | * This function sends a message via email to the recipients specified in the configuration. 123 | * 124 | * @param string $message The message to be sent. 125 | * @return void 126 | */ 127 | public static function sendEmailNotification($message) 128 | { 129 | // Get the email addresses to send the notification to 130 | $to = config('log-alarm.notification_email'); 131 | 132 | // If no email addresses are configured, return without sending the notification 133 | if (empty($to)) { 134 | return; 135 | } 136 | 137 | // Split the email addresses into an array 138 | $to_emails = explode(',', $to); 139 | 140 | try { 141 | // Send the email notification 142 | Mail::raw($message, function ($msg) use ($to_emails) { 143 | // Set the recipients and subject of the email 144 | $msg->to($to_emails)->subject(config('log-alarm.notification_email_subject')); 145 | }); 146 | } catch (\Exception $e) { 147 | // If the email sending fails, log the error 148 | Log::info("LogAlarm::sendEmailNotification->error: " . $e->getMessage()); 149 | } 150 | } 151 | 152 | /** 153 | * Send a notification to Discord. 154 | * 155 | * This function sends a message to a Discord channel via a webhook URL. 156 | * 157 | * @param string $message The message to be sent. 158 | * @return void 159 | */ 160 | public static function sendDiscordNotification($message) 161 | { 162 | // Get the webhook URL from the configuration 163 | $webhookUrl = config('log-alarm.discord_webhook_url'); 164 | 165 | // If the webhook URL is not configured, return without sending the notification 166 | if (empty($webhookUrl)) { 167 | return; 168 | } 169 | 170 | // Prepare the data for the request 171 | $data = [ 172 | 'embeds' => [ 173 | [ 174 | 'title' => config('log-alarm.notification_email_subject'), 175 | 'description' => $message, 176 | 'color' => 16711680, // Red color in decimal 177 | 'fields' => [ 178 | [ 179 | 'name' => 'Priority', 180 | 'value' => 'High', 181 | 'inline' => true 182 | ] 183 | ] 184 | ] 185 | ] 186 | ]; 187 | 188 | try { 189 | // Send the request using HTTP facade 190 | $response = Http::post($webhookUrl, $data); 191 | 192 | if (!$response->successful()) { 193 | Log::info("LogAlarm::sendDiscordNotification->error: " . $response->body()); 194 | } 195 | } catch (\Exception $e) { 196 | Log::info("LogAlarm::sendDiscordNotification->error: " . $e->getMessage()); 197 | } 198 | } 199 | 200 | /** 201 | * Send a notification to Telegram. 202 | * 203 | * This function sends a message to a Telegram chat via the Telegram Bot API. 204 | * 205 | * @param string $message The message to be sent. 206 | * @return void 207 | */ 208 | public static function sendTelegramNotification($message) 209 | { 210 | // Get the bot token and chat ID from the configuration 211 | $botToken = config('log-alarm.telegram_bot_token'); 212 | $chatId = config('log-alarm.telegram_chat_id'); 213 | 214 | // If either the bot token or chat ID is not configured, return without sending the notification 215 | if (empty($botToken) || empty($chatId)) { 216 | return; 217 | } 218 | 219 | // Prepare the message text with formatting 220 | $text = "*" . config('log-alarm.notification_email_subject') . "*\n\n" . 221 | $message . "\n\n" . 222 | "*Priority:* High"; 223 | 224 | // Prepare the data for the request 225 | $data = [ 226 | 'chat_id' => $chatId, 227 | 'text' => $text, 228 | 'parse_mode' => 'Markdown', 229 | 'disable_web_page_preview' => true 230 | ]; 231 | 232 | // Construct the API URL 233 | $apiUrl = "https://api.telegram.org/bot{$botToken}/sendMessage"; 234 | 235 | try { 236 | // Send the request using HTTP facade 237 | $response = Http::post($apiUrl, $data); 238 | 239 | if (!$response->successful()) { 240 | Log::info("LogAlarm::sendTelegramNotification->error: " . $response->body()); 241 | } 242 | } catch (\Exception $e) { 243 | Log::info("LogAlarm::sendTelegramNotification->error: " . $e->getMessage()); 244 | } 245 | } 246 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |