app log dev

This commit is contained in:
Yuri Kuznetsov
2024-05-03 17:59:06 +03:00
parent 1ed38e28cd
commit e0909af3fe
18 changed files with 455 additions and 1 deletions
@@ -0,0 +1,64 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Controllers\Record;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use stdClass;
class AppLogRecord extends Record
{
protected function checkAccess(): bool
{
return $this->user->isAdmin();
}
public function postActionCreate(Request $request, Response $response): stdClass
{
throw new Forbidden();
}
public function putActionUpdate(Request $request, Response $response): stdClass
{
throw new Forbidden();
}
public function postActionCreateLink(Request $request): bool
{
throw new Forbidden();
}
public function deleteActionRemoveLink(Request $request): bool
{
throw new Forbidden();
}
}
@@ -0,0 +1,120 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Log\Handler;
use Espo\Core\ORM\EntityManagerProxy;
use Espo\Entities\AppLogRecord;
use Monolog\Handler\HandlerInterface;
use Monolog\Level;
use Monolog\LogRecord;
use Throwable;
class DatabaseHandler implements HandlerInterface
{
public function __construct(
private Level $level,
private EntityManagerProxy $entityManager
) {}
public function isHandling(LogRecord $record): bool
{
return $record->level->value >= $this->level->value;
}
public function handle(LogRecord $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
try {
$logRecord = $this->entityManager->getRDBRepositoryByClass(AppLogRecord::class)->getNew();
$level = ucfirst($record->level->toPsrLogLevel());
$message = $this->interpolate($record, $record->message);
$logRecord
->setLevel($level)
->setMessage($message);
$this->setException($record, $logRecord);
$this->entityManager->saveEntity($logRecord);
}
catch (Throwable) {
// Nowhere to log.
return false;
}
return true;
}
private function interpolate(LogRecord $record, string $line): string
{
$replace = [];
foreach ($record->context as $key => $val) {
if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
$replace['{' . $key . '}'] = $val;
}
}
return strtr($line, $replace);
}
/**
* @param LogRecord[] $records
*/
public function handleBatch(array $records): void
{
foreach ($records as $record) {
$this->handle($record);
}
}
public function close(): void
{}
private function setException(LogRecord $record, AppLogRecord $logRecord): void
{
$exception = $record->context['exception'] ?? null;
if (!$exception instanceof Throwable) {
return;
}
$logRecord
->setExceptionClass(get_class($exception))
->setFile($exception->getFile())
->setLine($exception->getLine())
->setCode($exception->getCode());
}
}
+18 -1
View File
@@ -29,8 +29,10 @@
namespace Espo\Core\Log;
use Espo\Core\Log\Handler\DatabaseHandler;
use Espo\Core\Log\Handler\EspoFileHandler;
use Espo\Core\Log\Handler\EspoRotatingFileHandler;
use Espo\Core\ORM\EntityManagerProxy;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
@@ -47,7 +49,8 @@ class LogLoader
public function __construct(
private readonly Config $config,
private readonly HandlerListLoader $handlerListLoader
private readonly HandlerListLoader $handlerListLoader,
private readonly EntityManagerProxy $entityManagerProxy
) {}
public function load(): Log
@@ -65,6 +68,10 @@ class LogLoader
$handlerList = [$this->createDefaultHandler()];
}
if ($this->config->get('loggerDatabase')) {
$handlerList[] = $this->createDatabaseHandler();
}
foreach ($handlerList as $handler) {
$log->pushHandler($handler);
}
@@ -105,4 +112,14 @@ class LogLoader
{
return (bool) $this->config->get('logger.printTrace');
}
private function createDatabaseHandler(): HandlerInterface
{
$rawLevel = $this->config->get('logger.level') ??
self::DEFAULT_LEVEL;
$level = Logger::toMonologLevel($rawLevel);
return new DatabaseHandler($level, $this->entityManagerProxy);
}
}
@@ -0,0 +1,91 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Entities;
use Espo\Core\ORM\Entity;
class AppLogRecord extends Entity
{
public const ENTITY_TYPE = 'AppLogRecord';
public function setMessage(string $message): self
{
$this->set('message', $message);
return $this;
}
public function setLevel(string $level): self
{
$this->set('level', $level);
return $this;
}
public function setCode(?int $code): self
{
$this->set('code', $code);
return $this;
}
public function setExceptionClass(?string $exceptionClass): self
{
$len = $this->getAttributeParam('exceptionClass', 'len');
if ($exceptionClass && strlen($exceptionClass) > $len) {
$exceptionClass = substr($exceptionClass, $len);
}
$this->set('exceptionClass', $exceptionClass);
return $this;
}
public function setFile(?string $file): self
{
$len = $this->getAttributeParam('file', 'len');
if ($file && strlen($file) > $len) {
$file = substr($file, $len);
}
$this->set('file', $file);
return $this;
}
public function setLine(?int $code): self
{
$this->set('line', $code);
return $this;
}
}
@@ -295,5 +295,6 @@ return [
'listPagination' => true,
'starsLimit' => 500,
'quickSearchFullTextAppendWildcard' => false,
'loggerDatabase' => false,
'isInstalled' => false,
];
@@ -53,6 +53,7 @@ return [
'database',
'crud',
'logger',
'loggerDatabase',
'isInstalled',
'systemUser',
'defaultPermissions',
@@ -37,6 +37,7 @@
"User Interface": "User Interface",
"Auth Tokens": "Auth Tokens",
"Auth Log": "Auth Log",
"App Log": "App Log",
"Authentication": "Authentication",
"Currency": "Currency",
"Integrations": "Integrations",
@@ -293,6 +294,7 @@
"labelManager": "Customize application labels.",
"templateManager": "Customize message templates.",
"authLog": "Login history.",
"appLog": "Application log.",
"leadCapture": "API entry points for Web-to-Lead.",
"attachments": "All file attachments stored in the system.",
"systemRequirements": "System Requirements for EspoCRM.",
@@ -0,0 +1,10 @@
{
"fields": {
"message": "Message",
"code": "Code",
"level": "Level",
"exceptionClass": "Exception Class",
"file": "File",
"line": "Line"
}
}
@@ -47,6 +47,7 @@
"Preferences": "Preferences",
"EmailAddress": "Email Address",
"PhoneNumber": "Phone Number",
"AppLogRecord": "App Log Record",
"AuthLogRecord": "Auth Log Record",
"AuthFailLogRecord": "Auth Fail Log Record",
"LeadCapture": "Lead Capture Entry Point",
@@ -100,6 +101,7 @@
"AuthToken": "Auth Tokens",
"UniqueId": "Unique IDs",
"LastViewed": "Last Viewed",
"AppLogRecord": "App Log",
"AuthLogRecord": "Auth Log",
"AuthFailLogRecord": "Auth Fail Log",
"LeadCapture": "Lead Capture",
@@ -0,0 +1,11 @@
[
{
"rows": [
[{"name": "level"}, {"name": "code"}],
[{"name": "message"}],
[{"name": "exceptionClass"}],
[{"name": "file"}],
[{"name": "line"}, false]
]
}
]
@@ -0,0 +1,11 @@
[
{
"rows": [
[{"name":"level"}, {"name": "code"}],
[{"name": "message"}],
[{"name": "exceptionClass"}],
[{"name": "file"}],
[{"name": "line"}, false]
]
}
]
@@ -0,0 +1,8 @@
[
"createdAt",
"level",
"code",
"exceptionClass",
"file",
"line"
]
@@ -0,0 +1,16 @@
[
{"name": "level", "widthPx": 100},
{"name": "code", "widthPx": 80},
{"name": "message"},
{"name": "createdAt", "width": 13},
{
"name": "exceptionClass",
"width": 20,
"hidden": true
},
{
"name": "file",
"width": 20,
"hidden": true
}
]
@@ -331,6 +331,12 @@
"label": "Phone Numbers",
"iconClass": "fas fa-phone",
"description": "phoneNumbers"
},
{
"url": "#Admin/appLog",
"label": "App Log",
"iconClass": "fas fa-list",
"description": "appLog"
}
],
"order": 25
@@ -0,0 +1,5 @@
{
"controller": "controllers/record",
"createDisabled": true,
"editDisabled": true
}
@@ -0,0 +1,59 @@
{
"fields": {
"number": {
"type": "autoincrement",
"dbType": "bigint"
},
"createdAt": {
"type": "datetime",
"readOnly": true
},
"message": {
"type": "text",
"readOnly": true,
"orderDisabled": true
},
"level": {
"type": "enum",
"options": [
"Debug",
"Info",
"Notice",
"Warning",
"Error",
"Critical",
"Alert",
"Emergency"
],
"maxLength": 9,
"readOnly": true,
"index": true
},
"code": {
"type": "int",
"readOnly": true
},
"exceptionClass": {
"type": "varchar",
"maxLength": 512,
"readOnly": true
},
"file": {
"type": "varchar",
"maxLength": 512,
"readOnly": true
},
"line": {
"type": "int",
"readOnly": true
}
},
"links": {},
"collection": {
"orderBy": "number",
"order": "desc",
"textFilterFields": ["message"]
},
"indexes": {},
"hooksDisabled": true
}
@@ -0,0 +1,7 @@
{
"entity": true,
"layouts": false,
"tab": false,
"acl": false,
"customizable": false
}
+23
View File
@@ -412,6 +412,29 @@ class AdminController extends Controller {
});
}
// noinspection JSUnusedGlobalSymbols
actionAppLog() {
this.collectionFactory.create('AppLogRecord', collection => {
const searchManager = new SearchManager(
collection,
'list',
this.getStorage(),
this.getDateTime()
);
searchManager.loadStored();
collection.where = searchManager.getWhere();
collection.maxSize = this.getConfig().get('recordsPerPage') || collection.maxSize;
this.main('views/list', {
scope: 'AppLogRecord',
collection: collection,
searchManager: searchManager
});
});
}
// noinspection JSUnusedGlobalSymbols
actionIntegrations(options) {
const integration = options.name || null;