延續之前 Laravel Log 的小筆記 ,後來有歷經兩次實作,今天來談談當時的設計方法。
這會分作兩段來討論,一個是 Slack + Proxy 怎麼實作在 Monolog 上,以及新實作的 Logger 怎麼套用在 Laravel Log 上。
Slack + Proxy 官方的 SlackWebhookHandler 是沒有 proxy 設定的。雖然我覺得這是個很基本的需求,也有發過 PR ,但最終作者還是 close 了。不過這問題還好,可以自己額外寫套件繼承後再處理,程式碼大概會長這樣:
關鍵在 write()
方法,其他必要的參數等就不看了。
protected function write (array $record ) { $postData = $this ->getSlackRecord ()->getSlackData ($record ); $postString = json_encode ($postData ); $ch = curl_init (); $options = [ CURLOPT_URL => $this ->getWebhookUrl (), CURLOPT_POST => true , CURLOPT_RETURNTRANSFER => true , CURLOPT_HTTPHEADER => ['Content-type: application/json' ], CURLOPT_POSTFIELDS => $postString , ]; if (defined ('CURLOPT_SAFE_UPLOAD' )) { $options [CURLOPT_SAFE_UPLOAD] = true ; } if (null !== $this ->proxy) { $options [CURLOPT_PROXY] = $this ->proxy; } curl_setopt_array ($ch , $options ); Util ::execute ($ch ); }
write()
都是從官方複製出來的,僅僅只為了加入 proxy 設定判斷,滿滿地違反 DRY 原則,哭哭。
後來改寫成 Psr18SlackWebhookHandler :簡單來說,是把 HTTP client 換成 PSR18 的實作,要不要 proxy 就由該物件決定,這樣寫就變得很單純:
protected function write (array $record ): void { $postData = $this ->getSlackRecord ()->getSlackData ($record ); $postString = json_encode ($postData ); $request = $this ->httpRequestFactory->createRequest ('POST' , $this ->getWebhookUrl ()) ->withHeader ('Content-type' , 'application/json' ) ->withBody ($this ->httpStreamFactory->createStream ($postString )); $this ->httpClient->sendRequest ($request ); }
這裡面有很多物件是過去 curl 所沒有的,要套用在 Laravel 上,就會有初始化問題,如 $this->httpClient
的實例該如何產生。
以下討論 Laravel 6+ 之後的版本。
直接 pushHandler()
加入自定 Handler 簡單、直接、明暸!
使用 Monolog 的 pushHandler()
,將新的 Handler 加入即可。使用時機點除了之前文章 提到的 boot()
外,也可以使用 register()
+ Container::extend()
達成目的,範例程式如下:
public function register ( ) { $this ->app->extend ('log' , function (LogManager $log ) { $log ->pushHandler ($this ->createSlackWebhookHandler ()); return $log ; }); } private function createSlackWebhookHandler ( ): HandlerInterface { $handler = new Psr18SlackWebhookHandler (config ('webhook' )); $handler ->setDriver ( $this ->app->make (ClientInterface ::class ), $this ->app->make (RequestFactoryInterface ::class ), $this ->app->make (StreamFactoryInterface ::class ) ); return $handler ; }
夠直接,缺點也很明確:不容易套件化,因為這個方法只有處理到 new Handler() 行為,而沒有考慮 Laravel Log 設計整個生命週期,因此不管到哪,還是需要重寫這段 ServiceProvider 與設定的整合。
應用 LogManager::extend()
最好的結果就是能結合 logging.php 設定檔,讓使用這個 Handler 只要改改設定就解決了。
首先來看單一 logger 的設定寫法如下:
'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => 'Laravel Log', 'emoji' => ':boom:', 'level' => env('LOG_LEVEL', 'critical'), ]
這個設定檔會交由 LogManager 產生對應的 logger 實例。Laravel 四處都存在著擴充實作的可能,如 LogManager::extend()
:
public function extend ($driver , Closure $callback ) { $this ->customCreators[$driver ] = $callback ->bindTo ($this , $this ); return $this ; }
這裡只有單純把 $callback
保存起來,因此還要追 $this->customCreators[$driver]
在哪裡被呼叫,實際上是在 LogManager::callCustomCreator()
呼叫的:
protected function callCustomCreator (array $config ) { return $this ->customCreators[$config ['driver' ]]($this ->app, $config ); }
從這段程式可以知道,Closure 會收到的參數有 Container 與該 logger 的 config array(從 $config['driver']
可以看得出來)。參考其他 Driver,這裡要回傳的可以是 Monolog instance。
實作套件 接著以我實作的 monoex 為例,我取名為 psr18slack
:
$logManager ->extend ('psr18slack' , function (Container $app , array $config ) { return (new Par18SlackFactory ($app ))->__invoke ($config ); });
Par18SlackFactory 任務很單純,為產生 Monolog:
public function __invoke(array $config) { return new Monolog($this->parseChannel($config), [ $this->prepareHandler($this->createHandler($config), $config), ]); } private function createHandler(array $config): HandlerInterface { $handler = new Psr18SlackWebhookHandler( $config['url'], $config['channel'] ?? null, $config['username'] ?? 'Laravel', $config['attachment'] ?? true, $config['emoji'] ?? ':boom:', $config['short'] ?? false, $config['context'] ?? true, $this->level($config), $config['bubble'] ?? true, $config['exclude_fields'] ?? [] ); $handler->setDriver( $this->app->make(ClientInterface::class), $this->app->make(RequestFactoryInterface::class), $this->app->make(StreamFactoryInterface::class) ); return $handler; }
$config 是從 LogManager 那邊拿到的 logger 完整設定,所以可以直接用來 createHandler()
。
最後這是可以把 auto discovery 加上的套件--project 頂多就沒使用 psr18slack
的 driver 而已,不會對整體流程造成影響:
{ "extra" : { "laravel" : { "providers" : [ "MilesChou\\Monoex\\ServiceProvider" ] } } }
最後,當開發者安裝完 monoex 後,只要在 logging.php 設定以下的資訊,就能正常使用 PSR-18 專用的 slack 了。
'stack' => [ 'driver' => 'psr18slack' , 'url' => env ('LOG_SLACK_WEBHOOK_URL' ), 'username' => 'Laravel Log' , 'emoji' => ':boom:' , 'level' => 'critical' , ]
當然 ServiceProvider 必須先準備好 PSR-17 Factory / PSR-18 Client,讓 container 能正常取用下面這三個物件:
$app ->singleton (RequestFactoryInterface ::class , new \Laminas\Diactoros\RequestFactory ());$app ->singleton (ResponseFactoryInterface ::class , new \Laminas\Diactoros\ResponseFactory ());$app ->singleton (StreamFactoryInterface ::class , new \Laminas\Diactoros\StreamFactory ());$app ->singleton (ClientInterface ::class , function($app ) { return new \Symfony\Component\HttpClient\Psr18Client ( null , $app ->make (ResponseFactoryInterface ::class ), $app ->make (StreamFactoryInterface ::class ) ); });