Skip to content

Commit 1d9aaf8

Browse files
committed
ErrorHandler
1 parent f002f80 commit 1d9aaf8

File tree

4 files changed

+377
-1
lines changed

4 files changed

+377
-1
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Prokl\BitrixOrdinaryToolsBundle\Services\ErrorHandler\Agent;
4+
5+
use Bitrix\Main\Application;
6+
7+
/**
8+
* Class ClearTableAgent
9+
* @package Prokl\BitrixOrdinaryToolsBundle\Services\ErrorHandler\Agent
10+
*
11+
* @since 01.08.2021
12+
*/
13+
class ClearTableAgent
14+
{
15+
/**
16+
* Очистка таблицы b_fatal_error_log раз в сутки.
17+
*
18+
* @return string
19+
*/
20+
public static function clear() : string
21+
{
22+
$connection = Application::getConnection();
23+
24+
if ($connection->isTableExists('b_fatal_error_log')) {
25+
$connection->truncateTable('b_fatal_error_log');
26+
}
27+
28+
return '\Proklung\Error\Notifier\ClearTableAgent::clear();';
29+
}
30+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Prokl\BitrixOrdinaryToolsBundle\Services\ErrorHandler\Entity;
4+
5+
use Bitrix\Main\Entity\DataManager;
6+
use Bitrix\Main\Entity\IntegerField;
7+
use Bitrix\Main\ORM\Fields\DatetimeField;
8+
use Bitrix\Main\ORM\Fields\StringField;
9+
use Bitrix\Main\ORM\Fields\TextField;
10+
11+
/**
12+
* Class ErrorLogTable
13+
* @package Prokl\BitrixOrdinaryToolsBundle\Services\ErrorHandler\Entity
14+
*/
15+
class ErrorLogTable extends DataManager
16+
{
17+
/**
18+
* @inheritdoc
19+
*/
20+
public static function getTableName()
21+
{
22+
return 'b_fatal_error_log';
23+
}
24+
25+
/**
26+
* @inheritdoc
27+
*/
28+
public static function getMap()
29+
{
30+
return [
31+
'ID' => new IntegerField('ID', [
32+
'primary' => true,
33+
'autocomplete' => true,
34+
'title' => '',
35+
]),
36+
'DATE_CREATE' => new DatetimeField('DATE_CREATE', [
37+
'title' => 'Дата создания',
38+
]),
39+
'MD5' => new StringField('MD5', [
40+
'title' => 'MD5 исключения',
41+
'required' => true
42+
]),
43+
'EXCEPTION' => new TextField('EXCEPTION', [
44+
'title' => 'Сериализованная ошибка',
45+
'serialized' => true
46+
]),
47+
];
48+
}
49+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
<?php
2+
3+
namespace Prokl\BitrixOrdinaryToolsBundle\Services\ErrorHandler;
4+
5+
use Bitrix\Main\Application;
6+
use Bitrix\Main\ArgumentException;
7+
use Bitrix\Main\Diag\ExceptionHandlerLog;
8+
use Bitrix\Main\Entity\Base;
9+
use Bitrix\Main\ObjectPropertyException;
10+
use Bitrix\Main\SystemException;
11+
use Bitrix\Main\Type\DateTime;
12+
use Exception;
13+
use Bitrix\Main\Config\Option;
14+
use Prokl\BitrixOrdinaryToolsBundle\Services\Email\EventBridge\Notifier\BitrixNotification;
15+
use Prokl\BitrixOrdinaryToolsBundle\Services\ErrorHandler\Entity\ErrorLogTable;
16+
use RuntimeException;
17+
use Symfony\Component\Notifier\Notification\Notification;
18+
use Symfony\Component\Notifier\NotifierInterface;
19+
use Symfony\Component\Notifier\Recipient\Recipient;
20+
21+
/**
22+
* Class ErrorHandler
23+
* @package Prokl\BitrixOrdinaryToolsBundle\Services\ErrorHandler
24+
*
25+
* @since 01.08.2021
26+
*/
27+
class ErrorHandler extends ExceptionHandlerLog
28+
{
29+
private const OPTION_TYPES = 'types';
30+
31+
/**
32+
* @var boolean[] $logTypeFlags
33+
*/
34+
private $logTypeFlags;
35+
36+
/**
37+
* @var array $options
38+
*/
39+
private $options = [];
40+
41+
/**
42+
* ErrorHandler constructor.
43+
*/
44+
public function __construct()
45+
{
46+
/**
47+
* По-умолчанию логируется всё, кроме LOW_PRIORITY_ERROR.
48+
* Этот тип ошибки засоряет логи и появляется не только часто,
49+
* но и происходит от ошибок в коде ядра Битрикс.
50+
*/
51+
$this->logTypeFlags = [
52+
self::UNCAUGHT_EXCEPTION => true,
53+
self::CAUGHT_EXCEPTION => true,
54+
self::ASSERTION => true,
55+
self::FATAL => true,
56+
];
57+
58+
$this->setUp();
59+
}
60+
61+
/**
62+
* @inheritdoc
63+
*/
64+
public function initialize(array $options)
65+
{
66+
$this->initTypes($options);
67+
68+
$this->options = $options;
69+
$this->initOptions();
70+
}
71+
72+
/**
73+
* @inheritdoc
74+
* @throws ArgumentException | SystemException
75+
* @throws Exception
76+
*/
77+
public function write($exception, $logType)
78+
{
79+
if ((!array_key_exists($logType, $this->logTypeFlags)
80+
|| true !== $this->logTypeFlags[$logType])
81+
||
82+
!in_array($this->options['env'], $this->options['allowed_env'], true)
83+
) {
84+
return;
85+
}
86+
87+
if ($exception instanceof Exception && !$this->has($exception)) {
88+
$this->send($exception);
89+
90+
ErrorLogTable::add(
91+
[
92+
'DATE_CREATE' => new DateTime(),
93+
'MD5' => md5(serialize($exception)),
94+
'EXCEPTION' => serialize($exception),
95+
]
96+
);
97+
}
98+
}
99+
100+
/**
101+
* Экземпляр notifier.
102+
*
103+
* @return NotifierInterface
104+
*/
105+
private function getNotifier() : NotifierInterface
106+
{
107+
return container()->get('notifier');
108+
}
109+
110+
/**
111+
* Отправка уведомлений.
112+
*
113+
* @param Exception $exception
114+
*
115+
* @return void
116+
* @throws Exception
117+
*/
118+
private function send(Exception $exception) : void
119+
{
120+
$notifier = $this->getNotifier();
121+
$importancy = $this->options['importancy'];
122+
123+
$emails = container()->getParameter('mailer_recipients');
124+
if (!$emails) {
125+
throw new RuntimeException('Email of recipitients not setting.');
126+
}
127+
128+
$email = $emails[0];
129+
130+
if ($this->options['recipient']) {
131+
$email = (string)$this->options['recipient'];
132+
}
133+
134+
$request = Application::getInstance()->getContext()->getRequest();
135+
$uriString = $request->getRequestUri();
136+
137+
$title = $request->getServer()->getServerName();
138+
if (!$title) {
139+
$title = Option::get('main', 'server_name', '');
140+
}
141+
142+
$body = 'Url: ' . $uriString . ' ';
143+
$body = $body . get_class($exception) .
144+
': ' . $exception->getMessage() .
145+
' in ' . $exception->getFile() .
146+
' at line ' . $exception->getLine();
147+
148+
$notification = (new BitrixNotification($title))
149+
->content($body)
150+
->importance($importancy);
151+
152+
$notifier->send($notification, new Recipient($email));
153+
}
154+
155+
/**
156+
* Есть запись о такой ошибке в таблице?
157+
*
158+
* @param Exception $e Ошибка.
159+
*
160+
* @return boolean
161+
*
162+
* @throws ArgumentException | SystemException | ObjectPropertyException ORM ошибки.
163+
*/
164+
private function has(Exception $e) : bool
165+
{
166+
$hash = md5(serialize($e));
167+
168+
$query = ErrorLogTable::getList([
169+
'filter' => [
170+
'=MD5' => $hash
171+
]
172+
]);
173+
174+
return count($query->fetchAll()) > 0;
175+
}
176+
177+
/**
178+
* Инициализация типов обрабатываемых ошибок.
179+
*
180+
* @param array $options Опции.
181+
*
182+
* @return void
183+
*/
184+
private function initTypes(array $options) : void
185+
{
186+
if (!array_key_exists(self::OPTION_TYPES, $options) || !is_array($options[self::OPTION_TYPES])) {
187+
return;
188+
}
189+
190+
$this->logTypeFlags = [];
191+
foreach ($options[self::OPTION_TYPES] as $logType) {
192+
if (is_int($logType)) {
193+
$this->logTypeFlags[$logType] = true;
194+
}
195+
}
196+
}
197+
198+
/**
199+
* Обработка параметров.
200+
*
201+
* @return void
202+
*/
203+
private function initOptions(): void
204+
{
205+
$this->options['env'] = $this->options['env'] ?? 'prod';
206+
207+
$this->options['allowed_env'] = $this->options['allowed_env'] ?? ['prod'];
208+
if (count($this->options['allowed_env']) === 0) {
209+
$this->options['allowed_env'] = ['prod'];
210+
}
211+
212+
$this->options['importancy'] = $this->options['importancy'] ?? Notification::IMPORTANCE_URGENT;
213+
}
214+
215+
/**
216+
* Создать при необходимости таблицу и агента.
217+
*
218+
* @return void
219+
* @throws ArgumentException
220+
* @throws SystemException
221+
*/
222+
private function setUp(): void
223+
{
224+
// Создать таблицу b_fatal_error_log, если еще не.
225+
if (!Application::getConnection()->isTableExists(
226+
Base::getInstance(ErrorLogTable::class)->getDBTableName()
227+
)) {
228+
Base::getInstance(ErrorLogTable::class)->createDBTable();
229+
}
230+
231+
// Добавить агента, если еще нет.
232+
$rsAgents = \CAgent::getList([],
233+
['NAME' => '\\Prokl\\BitrixOrdinaryToolsBundle\\Services\\ErrorHandler\\Agent\\ClearTableAgent::clear();']);
234+
if (!$rsAgents->fetch()) {
235+
\CAgent::AddAgent(
236+
'\\Prokl\\BitrixOrdinaryToolsBundle\\Services\\ErrorHandler\\Agent\\ClearTableAgent::clear();',
237+
'',
238+
'N',
239+
10 * 24 * 3600,
240+
'',
241+
'Y',
242+
''
243+
);
244+
}
245+
}
246+
}

readme.MD

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,55 @@ monolog:
152152
Нюанс:
153153

154154
- Telegram плохо переваривает html (даже в режиме `parse_mode = html`). Посему под капотом html шаблона превращается в markdown
155-
разметку.
155+
разметку.
156+
157+
### Отправка сообщений о фатальных ошибках на проекте согласно channel-policy нотификатора
158+
159+
`/bitrix/.settings.php`:
160+
161+
Работает при условии установки [бандла](https://github.com/ProklUng/core.framework.extension.bundle).
162+
163+
```php
164+
use Symfony\Component\Notifier\Notification\Notification;
165+
166+
return [
167+
'exception_handling' =>
168+
array(
169+
'value' =>
170+
array(
171+
'debug' => env('DEBUG', false),
172+
'handled_errors_types' => 4437,
173+
'exception_errors_types' => 4437,
174+
'ignore_silence' => false,
175+
'assertion_throws_exception' => true,
176+
'assertion_error_type' => 256,
177+
'log' => array (
178+
'class_name' => \Prokl\BitrixOrdinaryToolsBundle\Services\ErrorHandler\ErrorHandler::class,
179+
'required_file' => 'vendor/proklung/bitrix-tools-pack-bundle/Services/ErrorHandler/Entity/ErrorLogTable.php',
180+
'settings' => array (
181+
'types' => [
182+
\Bitrix\Main\Diag\ExceptionHandlerLog::UNCAUGHT_EXCEPTION,
183+
\Bitrix\Main\Diag\ExceptionHandlerLog::IGNORED_ERROR,
184+
\Bitrix\Main\Diag\ExceptionHandlerLog::FATAL,
185+
],
186+
// Получатель почты; перебивает параметры родительского модуля
187+
'recipient' => 'email@gmail.com',
188+
// Или какой-нибудь иной способ различения dev/prod среды
189+
// По умолчанию - dev
190+
'env' => env('DEBUG', false) ? 'dev' : 'prod',
191+
// В каком окружении работать. По умолчанию - prod.
192+
'allowed_env' => ['dev', 'prod'],
193+
// Уровень важности согласно channel_policy (см. документацию к модулю proklung.notifier)
194+
// По умолчанию - urgent
195+
'importancy' => Notification::IMPORTANCE_URGENT,
196+
),
197+
),
198+
),
199+
'readonly' => false,
200+
),
201+
];
202+
```
203+
#### Нюансы
204+
205+
1) Сообщение об ошибке рассылается всего один раз (иначе чревато флудом). Каждые сутки таблица с информацией
206+
об отправленных уведомлениях очищается посредством агента. Процесс начинается по новой.

0 commit comments

Comments
 (0)