Showing
6 changed files
with
181 additions
and
1 deletions
| ... | @@ -257,6 +257,93 @@ DAITO_GOOGLE_CHAT_VERIFY_SSL=false | ... | @@ -257,6 +257,93 @@ DAITO_GOOGLE_CHAT_VERIFY_SSL=false |
| 257 | 257 | ||
| 258 | Use this only for temporary debugging on local environment. Never disable SSL verification on production. | 258 | Use this only for temporary debugging on local environment. Never disable SSL verification on production. |
| 259 | 259 | ||
| 260 | +## DaitoExceptionNotifier (separate reusable module) | ||
| 261 | + | ||
| 262 | +### 1) Publish config | ||
| 263 | +```bash | ||
| 264 | +php artisan vendor:publish --tag=daito-exception-notifier-config | ||
| 265 | +``` | ||
| 266 | + | ||
| 267 | +This creates `config/daito-exception-notifier.php`. | ||
| 268 | + | ||
| 269 | +### 2) Minimal `.env` setup | ||
| 270 | +```dotenv | ||
| 271 | +DAITO_EXCEPTION_NOTIFIER_ENABLED=true | ||
| 272 | +DAITO_EXCEPTION_NOTIFIER_SEND_MODE=queue | ||
| 273 | +DAITO_EXCEPTION_NOTIFIER_LOOP_GUARD_ENABLED=true | ||
| 274 | +DAITO_EXCEPTION_NOTIFIER_LOOP_GUARD_TTL_SECONDS=30 | ||
| 275 | +DAITO_EXCEPTION_NOTIFIER_TRACE_MODE=smart | ||
| 276 | +DAITO_EXCEPTION_NOTIFIER_TRACE_MAX_LINES=8 | ||
| 277 | +DAITO_EXCEPTION_NOTIFIER_TRACE_SKIP_VENDOR=true | ||
| 278 | +DAITO_EXCEPTION_NOTIFIER_TRACE_INCLUDE_FIRST_APP_FRAME=true | ||
| 279 | +``` | ||
| 280 | + | ||
| 281 | +### 3) Auto notify on exception (HTTP + CLI) | ||
| 282 | + | ||
| 283 | +Use Laravel exception handler to automatically send exception cardV2: | ||
| 284 | + | ||
| 285 | +```php | ||
| 286 | +<?php | ||
| 287 | + | ||
| 288 | +namespace App\Exceptions; | ||
| 289 | + | ||
| 290 | +use Daito\Lib\DaitoExceptionNotifier; | ||
| 291 | +use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; | ||
| 292 | +use Throwable; | ||
| 293 | + | ||
| 294 | +class Handler extends ExceptionHandler | ||
| 295 | +{ | ||
| 296 | + public function register() | ||
| 297 | + { | ||
| 298 | + $this->reportable(function (Throwable $throwable) { | ||
| 299 | + DaitoExceptionNotifier::notify($throwable); | ||
| 300 | + }); | ||
| 301 | + } | ||
| 302 | +} | ||
| 303 | +``` | ||
| 304 | + | ||
| 305 | +`DaitoExceptionNotifier::notify()` supports both route/controller errors and CLI command errors because Laravel routes both through the same exception handler. | ||
| 306 | + | ||
| 307 | +If you want explicit mode: | ||
| 308 | + | ||
| 309 | +```php | ||
| 310 | +DaitoExceptionNotifier::queue($throwable); // queue | ||
| 311 | +DaitoExceptionNotifier::send($throwable); // sync immediate | ||
| 312 | +``` | ||
| 313 | + | ||
| 314 | +Exception cardV2 includes: | ||
| 315 | + | ||
| 316 | +- `time` | ||
| 317 | +- `action` | ||
| 318 | +- `file` | ||
| 319 | +- `line` | ||
| 320 | +- `message` | ||
| 321 | +- compact `trace` (top useful frames only, configurable) | ||
| 322 | +- `first_app_frame` (when available) | ||
| 323 | + | ||
| 324 | +Trace filter strategy (`DAITO_EXCEPTION_NOTIFIER_TRACE_MODE`): | ||
| 325 | + | ||
| 326 | +- `smart` (recommended): prefer app frames, then non-vendor frames, then fallback | ||
| 327 | +- `app_only`: only frames that belong to `app/` or class prefix `App\` | ||
| 328 | +- `no_vendor`: remove `vendor` frames | ||
| 329 | +- `class_prefix_only`: only frames by configured class prefixes (`trace_class_prefixes`) | ||
| 330 | + | ||
| 331 | +### 4) Built-in loop guard (recommended default) | ||
| 332 | + | ||
| 333 | +To avoid recursive notify loops when Google Chat send itself throws exceptions, this module has a built-in circuit-breaker: | ||
| 334 | + | ||
| 335 | +- re-entrant guard in the same process/request | ||
| 336 | +- short TTL dedupe by exception fingerprint (file+line+message+action) | ||
| 337 | +- optional cache-based dedupe across workers/processes | ||
| 338 | + | ||
| 339 | +Main config keys: | ||
| 340 | + | ||
| 341 | +- `loop_guard_enabled` | ||
| 342 | +- `loop_guard_ttl_seconds` | ||
| 343 | +- `loop_guard_use_cache` | ||
| 344 | +- `loop_guard_cache_prefix` | ||
| 345 | +- `loop_guard_skip_if_notifier_in_trace` | ||
| 346 | + | ||
| 260 | ## QueryLog (Laravel shared package) | 347 | ## QueryLog (Laravel shared package) |
| 261 | 348 | ||
| 262 | > **Provider registration note** | 349 | > **Provider registration note** | ... | ... |
| ... | @@ -21,7 +21,8 @@ | ... | @@ -21,7 +21,8 @@ |
| 21 | "laravel": { | 21 | "laravel": { |
| 22 | "providers": [ | 22 | "providers": [ |
| 23 | "Daito\\Lib\\DaitoQueryLog\\Providers\\DaitoQueryLogProvider", | 23 | "Daito\\Lib\\DaitoQueryLog\\Providers\\DaitoQueryLogProvider", |
| 24 | - "Daito\\Lib\\DaitoGoogleChat\\Providers\\DaitoGoogleChatProvider" | 24 | + "Daito\\Lib\\DaitoGoogleChat\\Providers\\DaitoGoogleChatProvider", |
| 25 | + "Daito\\Lib\\DaitoExceptionNotifier\\Providers\\DaitoExceptionNotifierProvider" | ||
| 25 | ] | 26 | ] |
| 26 | } | 27 | } |
| 27 | } | 28 | } | ... | ... |
src/DaitoExceptionNotifier.php
0 → 100644
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +namespace Daito\Lib; | ||
| 4 | + | ||
| 5 | +use Daito\Lib\DaitoExceptionNotifier\DaitoExceptionNotifierManager; | ||
| 6 | +use RuntimeException; | ||
| 7 | +use Throwable; | ||
| 8 | + | ||
| 9 | +class DaitoExceptionNotifier | ||
| 10 | +{ | ||
| 11 | + public static function send(Throwable $throwable, array $arrContext = array(), $webhookUrl = null): array | ||
| 12 | + { | ||
| 13 | + return self::manager()->send($throwable, $arrContext, $webhookUrl); | ||
| 14 | + } | ||
| 15 | + | ||
| 16 | + public static function queue(Throwable $throwable, array $arrContext = array(), $webhookUrl = null): void | ||
| 17 | + { | ||
| 18 | + self::manager()->queue($throwable, $arrContext, $webhookUrl); | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + public static function notify(Throwable $throwable, array $arrContext = array(), $webhookUrl = null) | ||
| 22 | + { | ||
| 23 | + return self::manager()->notify($throwable, $arrContext, $webhookUrl); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + private static function manager(): DaitoExceptionNotifierManager | ||
| 27 | + { | ||
| 28 | + if (!function_exists('app')) { | ||
| 29 | + throw new RuntimeException('Laravel app container is required for DaitoExceptionNotifier.'); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + $manager = app(DaitoExceptionNotifierManager::class); | ||
| 33 | + if (!$manager instanceof DaitoExceptionNotifierManager) { | ||
| 34 | + throw new RuntimeException('Can not resolve DaitoExceptionNotifierManager from container.'); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + return $manager; | ||
| 38 | + } | ||
| 39 | +} |
This diff is collapsed.
Click to expand it.
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +namespace Daito\Lib\DaitoExceptionNotifier\Providers; | ||
| 4 | + | ||
| 5 | +use Daito\Lib\DaitoExceptionNotifier\DaitoExceptionNotifierManager; | ||
| 6 | +use Daito\Lib\DaitoGoogleChat\DaitoGoogleChatManager; | ||
| 7 | +use Illuminate\Support\ServiceProvider; | ||
| 8 | + | ||
| 9 | +class DaitoExceptionNotifierProvider extends ServiceProvider | ||
| 10 | +{ | ||
| 11 | + public function register(): void | ||
| 12 | + { | ||
| 13 | + $this->mergeConfigFrom( | ||
| 14 | + __DIR__ . '/../config/daito-exception-notifier.php', | ||
| 15 | + 'daito-exception-notifier' | ||
| 16 | + ); | ||
| 17 | + | ||
| 18 | + $this->app->singleton(DaitoExceptionNotifierManager::class, function ($app) { | ||
| 19 | + return new DaitoExceptionNotifierManager( | ||
| 20 | + $app->make(DaitoGoogleChatManager::class) | ||
| 21 | + ); | ||
| 22 | + }); | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + public function boot(): void | ||
| 26 | + { | ||
| 27 | + $this->publishes( | ||
| 28 | + array( | ||
| 29 | + __DIR__ . '/../config/daito-exception-notifier.php' => config_path('daito-exception-notifier.php'), | ||
| 30 | + ), | ||
| 31 | + 'daito-exception-notifier-config' | ||
| 32 | + ); | ||
| 33 | + } | ||
| 34 | +} |
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +return array( | ||
| 4 | + 'enabled' => env('DAITO_EXCEPTION_NOTIFIER_ENABLED', true), | ||
| 5 | + 'send_mode' => env('DAITO_EXCEPTION_NOTIFIER_SEND_MODE', 'queue'), // queue|sync | ||
| 6 | + 'card_title' => env('DAITO_EXCEPTION_NOTIFIER_CARD_TITLE', 'Exception Alert'), | ||
| 7 | + 'loop_guard_enabled' => env('DAITO_EXCEPTION_NOTIFIER_LOOP_GUARD_ENABLED', true), | ||
| 8 | + 'loop_guard_ttl_seconds' => env('DAITO_EXCEPTION_NOTIFIER_LOOP_GUARD_TTL_SECONDS', 30), | ||
| 9 | + 'loop_guard_use_cache' => env('DAITO_EXCEPTION_NOTIFIER_LOOP_GUARD_USE_CACHE', true), | ||
| 10 | + 'loop_guard_cache_prefix' => env('DAITO_EXCEPTION_NOTIFIER_LOOP_GUARD_CACHE_PREFIX', 'daito-exception-notifier:loop'), | ||
| 11 | + 'loop_guard_skip_if_notifier_in_trace' => env('DAITO_EXCEPTION_NOTIFIER_LOOP_GUARD_SKIP_NOTIFIER_TRACE', true), | ||
| 12 | + 'message_max_length' => env('DAITO_EXCEPTION_NOTIFIER_MESSAGE_MAX_LENGTH', 1000), | ||
| 13 | + 'trace_mode' => env('DAITO_EXCEPTION_NOTIFIER_TRACE_MODE', 'smart'), // smart|app_only|no_vendor|class_prefix_only | ||
| 14 | + 'trace_class_prefixes' => array('App\\'), | ||
| 15 | + 'trace_include_first_app_frame' => env('DAITO_EXCEPTION_NOTIFIER_TRACE_INCLUDE_FIRST_APP_FRAME', true), | ||
| 16 | + 'trace_max_lines' => env('DAITO_EXCEPTION_NOTIFIER_TRACE_MAX_LINES', 8), | ||
| 17 | + 'trace_max_length' => env('DAITO_EXCEPTION_NOTIFIER_TRACE_MAX_LENGTH', 2500), | ||
| 18 | + 'trace_skip_vendor' => env('DAITO_EXCEPTION_NOTIFIER_TRACE_SKIP_VENDOR', true), | ||
| 19 | +); |
-
Please register or sign in to post a comment