Miles' Blog

天涯何處無幹話,何必要講實務話

Handler 的繼承關係如下(使用 YAML 表示):

- HandlerInterface:
- AbstractHandler:
- AbstractProcessingHandler:
- AbstractSyslogHandler:
- SyslogHandler
- SyslogUdpHandler
- AmqpHandler
- BrowserConsoleHandler
- ChromePHPHandler
- CouchDBHandler
- CubeHandler
- DoctrineCouchDBHandler
- DynamoDbHandler
- ElasticSearchHandler
- ErrorLogHandler
- FirePHPHandler
- GelfHandler
- IFTTTHandler
- LogglyHandler
- MailHandler:
- MandrillHandler
- NativeMailerHandler
- SwiftMailerHandler
- MongoDBHandler
- NewRelicHandler
- PHPConsoleHandler
- RavenHandler
- RedisHandler
- RollbarHandler
- SlackbotHandler
- SlackWebhookHandler
- SocketHandler:
- FleepHookHandler
- FlowdockHandler
- HipChatHandler
- LogEntriesHandler
- PushoverHandler
- SlackHandler
- StreamHandler:
- RotatingFileHandler
- TestHandler
- ZendMonitorHandler
- BufferHandler:
- DeduplicationHandler
- FilterHandler
- FingersCrossedHandler
- GroupHandler:
- WhatFailureGroupHandler
- NullHandler
- PsrHandler
- SamplingHandler
- HandlerWrapper

洋洋灑灑列出一長串的 Class 名稱,大部分是單純實作服務的串接,有另一小部分的 Handler 是有特殊用途的,撿幾個來介紹。

TestHandler

這個 Handler 是設計用來做測試的,比方說:

$logger = new \Monolog\Logger('name');

$handler = new \Monolog\Handler\TestHandler();

$logger->pushHandler($handler);

$logger->warning('test');

var_dump($handler->hasAlertRecords());
var_dump($handler->hasWarningRecords());

這樣輸出的結果會是:

bool(false)
bool(true)

它可以作為一個 spy,去確認寫到 Logger 的內容是正確的。

它的設計方法也很單純,在 write 去寫入一個陣列:

protected function write(array $record)
{
$this->recordsByLevel[$record['level']][] = $record;
$this->records[] = $record;
}

而在 assertion 的方法去找陣列有沒有對應的值即可,非常厲害。

GroupHandler

這可以把多個 Handler 集合成一個 Group,而對這個 Group 操作,就等於對全部的 Handler 操作。

NullHandler

它的說明很有趣:

Blackhole

所有的 Log 將會像是遇到無底洞一樣,全部被這個 Handler 吃光光。

這也是設計用來測試用的。

PsrHandler

覺得 Monolog 不好,但有中意其他 PSR Logger。雖然傷心寂寞覺得冷,但還是可以考慮用 Monolog 的 PsrHandler 包裝其他 PSR Logger 哦。

昨天提到 AbstractHandler 會實作存在 Processor 的方法,但實質上 AbstractHandler 是不會使用 Processor 的。

Monolog 的設計是另外寫一個 AbstractProcessingHandler 來繼承 AbstractHandler,在裡面處理 Processor

protected function processRecord(array $record)
{
if ($this->processors) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
}

return $record;
}

實作 AbstractProcessingHandler 只要實作 write 方法即可。

其他都是因實作功能需求,把迭代 Processor 的任務放到 handle 的開始、中間、或是後面。

比方說 BufferHandler 是把所有記錄全都放到記憶體裡,直到程序結束後,再一次性的往外送。這種情況下,就會需要在實際 Handler 裡,拿到真正的 record 後,才能跑 Processor,再存 buffer。

昨天了解 Formatter 的運作方法了,而資料夾還有另一個角色 Processor,今天來看看它到底裡面賣的是什麼藥。

從使用它到了解它

Processor 有兩個地方可以使用,分別在 Logger 實作與 HandlerInterface 定義,都有 Processor 的影子。

先來看看 Logger 實作的介面:

class Logger implements LoggerInterface
{
public function pushProcessor($callback)
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given');
}
array_unshift($this->processors, $callback);

return $this;
}

public function popProcessor()
{
if (!$this->processors) {
throw new \LogicException('You tried to pop from an empty processor stack.');
}

return array_shift($this->processors);
}
}

這裡可以看到,它跟 Handler 一樣是使用 Array Stack 實作儲存。而要成為 Processor 的一員條件是 is_callable

HandlerInterface 的定義也相差不遠:

interface HandlerInterface
{
public function pushProcessor($callback);

public function popProcessor();
}

HandlerInterface 可以依需求實作不同的邏輯,所以我們先來看 Logger 已經寫好的實作。Logger 是這樣使用 Processor 的:

public function addRecord($level, $message, array $context = array())
{
// ...

foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}

// ...
}

Processor 只要實作 Magic Function __invoke 即可當成 callable 來用,比方說 Processor\MemoryUsageProcessor 的內容如下:

class MemoryUsageProcessor extends MemoryProcessor
{
public function __invoke(array $record)
{
$bytes = memory_get_usage($this->realUsage);
$formatted = $this->formatBytes($bytes);

$record['extra']['memory_usage'] = $formatted;

return $record;
}
}

這樣我們就能動態為 $record 加上額外需要的系統資訊了。

而在 Logger 裡,是先跑 Processor,才跑 Handlerhandle 方法,因此在 Logger 的 Processor,實際上會作用在全部的 Handler。

Handler 的實作

使用 IDE 可以簡單找得到,實作的地方在 AbstractHandlerHandlerWrapper。後者是使用類似 Proxy Pattern 的方法在包裝其他的 Handler,所以本質上還是在使用 AbstractHandler 的實作。

AbstractHandler 的實作如下:

public function pushProcessor($callback)
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given');
}
array_unshift($this->processors, $callback);

return $this;
}

public function popProcessor()
{
if (!$this->processors) {
throw new \LogicException('You tried to pop from an empty processor stack.');
}

return array_shift($this->processors);
}

事實上與 Logger 完全一樣,重點會是在 $this->processors 如何被使用。不同的 Handler 的用法都有點不大一樣,我們留到明天再詳解吧。

做完交付後,下一個目標就是要做部署了!不過部署做簡單一點,在 Docker 上能跑就行了!

閱讀全文 »

打開 Monolog 的資料夾,會發現除了昨天提到的 Logger 與 Handler 之外,還有幾個沒提到的角色,如 Formatter 或是 Processor

今天我們就來看看 Formatter 裡面做了什麼事。

從使用它到了解它

首先當然先看看它如何使用!我們可以從先從 Handler\HandlerInterface 找到相關的實作:

interface HandlerInterface
{
public function setFormatter(FormatterInterface $formatter);
}

這裡可以注意到,它的設計是「抽象依賴抽象」,這符合依賴反轉原則;並且它的行為設計上,是做成抽象與抽象之間的一對一關係(因為它用 set,而不是 push),所以它實際上是實作了 Bridge Pattern。關係圖如下:

@startuml
Interface Handler\HandlerInterface {
+ setFormatter(f: FormatterInterface)
}
Interface Formatter\FormatterInterface
Class Handler\ConcreteHandler
Class Formatter\ConcreteFormatter
Handler\HandlerInterface -> Formatter\FormatterInterface
Handler\HandlerInterface <|-- Handler\ConcreteHandler
Formatter\FormatterInterface <|-- Formatter\ConcreteFormatter
@enduml

它們的實作之間並沒有直接耦合,而是透過抽象介面耦合;換句話說,只要物件有實作抽象介面的話,它就能正常的介接使用。

我們先來看昨天使用的 Handler\StreamHandler 裡面是使用了哪種 Formatter。追一下程式碼,會發現在 Handler\AbstractHandler 裡面:

abstract class AbstractHandler implements HandlerInterface
{
public function getFormatter()
{
if (!$this->formatter) {
$this->formatter = $this->getDefaultFormatter();
}

return $this->formatter;
}

protected function getDefaultFormatter()
{
return new LineFormatter();
}
}

因此預設會是 LineFormatter,當然不是發 Log 到 Line,而是指單行的 log。

再來我們來寫一個測試程式如下:

$logger = new \Monolog\Logger('name');

$handler = new \Monolog\Handler\StreamHandler('php://stdout', \Monolog\Logger::DEBUG);

$logger->pushHandler($handler);

$logger->warning('test');

這樣輸出的結果應該會如下:

[2018-01-15 18:10:12] name.WARNING: test [] []

剛有提到,我們應該能自由地抽換 Formatter 才對,我們來換成 JsonFormatter 試看看:

$handler->setFormatter(new \Monolog\Formatter\JsonFormatter());

輸出就會變成 JSON 了:

{"message":"test","context":[],"level":300,"level_name":"WARNING","channel":"name","datetime":{"date":"2018-01-15 18:16:28.438148","timezone_type":3,"timezone":"UTC"},"extra":[]}

自由組合 1+1 = $50

相信大家都去過黃色拱門,Formatter 與 Handler 之間的關係事實上就跟銅板輕鬆點很像,可以依需求任意搭配兩區的餐點,而店員都可以接受並結帳。

我們也可以使用 Slack 配 Json 或是 Mail 配 Html 等等,完全自由配。

參考資料

0%