From 1ed38e28cdef225650e404e55a387259fe982644 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 3 May 2024 17:35:50 +0300 Subject: [PATCH 1/3] side panel date fields if no by field --- client/src/views/record/panels/default-side.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/views/record/panels/default-side.js b/client/src/views/record/panels/default-side.js index e3fd6143a3..97c84f3008 100644 --- a/client/src/views/record/panels/default-side.js +++ b/client/src/views/record/panels/default-side.js @@ -55,11 +55,11 @@ class DefaultSidePanelView extends SidePanelView { const allFieldList = this.getFieldManager().getEntityTypeFieldList(this.model.entityType); this.hasComplexCreated = - allFieldList.includes('createdAt') && + allFieldList.includes('createdAt') || allFieldList.includes('createdBy'); this.hasComplexModified = - allFieldList.includes('modifiedAt') && + allFieldList.includes('modifiedAt') || allFieldList.includes('modifiedBy'); super.setup(); From e0909af3fe209c9d5b1d6a5f5fc689deeb4af1c2 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 3 May 2024 17:59:06 +0300 Subject: [PATCH 2/3] app log dev --- application/Espo/Controllers/AppLogRecord.php | 64 ++++++++++ .../Espo/Core/Log/Handler/DatabaseHandler.php | 120 ++++++++++++++++++ application/Espo/Core/Log/LogLoader.php | 19 ++- application/Espo/Entities/AppLogRecord.php | 91 +++++++++++++ .../Espo/Resources/defaults/config.php | 1 + .../Espo/Resources/defaults/systemConfig.php | 1 + .../Espo/Resources/i18n/en_US/Admin.json | 2 + .../Resources/i18n/en_US/AppLogRecord.json | 10 ++ .../Espo/Resources/i18n/en_US/Global.json | 2 + .../layouts/AppLogRecord/detail.json | 11 ++ .../layouts/AppLogRecord/detailSmall.json | 11 ++ .../layouts/AppLogRecord/filters.json | 8 ++ .../Resources/layouts/AppLogRecord/list.json | 16 +++ .../Resources/metadata/app/adminPanel.json | 6 + .../metadata/clientDefs/AppLogRecord.json | 5 + .../metadata/entityDefs/AppLogRecord.json | 59 +++++++++ .../metadata/scopes/AppLogRecord.json | 7 + client/src/controllers/admin.js | 23 ++++ 18 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 application/Espo/Controllers/AppLogRecord.php create mode 100644 application/Espo/Core/Log/Handler/DatabaseHandler.php create mode 100644 application/Espo/Entities/AppLogRecord.php create mode 100644 application/Espo/Resources/i18n/en_US/AppLogRecord.json create mode 100644 application/Espo/Resources/layouts/AppLogRecord/detail.json create mode 100644 application/Espo/Resources/layouts/AppLogRecord/detailSmall.json create mode 100644 application/Espo/Resources/layouts/AppLogRecord/filters.json create mode 100644 application/Espo/Resources/layouts/AppLogRecord/list.json create mode 100644 application/Espo/Resources/metadata/clientDefs/AppLogRecord.json create mode 100644 application/Espo/Resources/metadata/entityDefs/AppLogRecord.json create mode 100644 application/Espo/Resources/metadata/scopes/AppLogRecord.json diff --git a/application/Espo/Controllers/AppLogRecord.php b/application/Espo/Controllers/AppLogRecord.php new file mode 100644 index 0000000000..5f813e1c34 --- /dev/null +++ b/application/Espo/Controllers/AppLogRecord.php @@ -0,0 +1,64 @@ +. + * + * 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(); + } +} diff --git a/application/Espo/Core/Log/Handler/DatabaseHandler.php b/application/Espo/Core/Log/Handler/DatabaseHandler.php new file mode 100644 index 0000000000..d56449e53c --- /dev/null +++ b/application/Espo/Core/Log/Handler/DatabaseHandler.php @@ -0,0 +1,120 @@ +. + * + * 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()); + } +} diff --git a/application/Espo/Core/Log/LogLoader.php b/application/Espo/Core/Log/LogLoader.php index 7330ff9fa1..d609febea6 100644 --- a/application/Espo/Core/Log/LogLoader.php +++ b/application/Espo/Core/Log/LogLoader.php @@ -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); + } } diff --git a/application/Espo/Entities/AppLogRecord.php b/application/Espo/Entities/AppLogRecord.php new file mode 100644 index 0000000000..bae6bada64 --- /dev/null +++ b/application/Espo/Entities/AppLogRecord.php @@ -0,0 +1,91 @@ +. + * + * 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; + } +} diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php index 00d679ef0d..897d663316 100644 --- a/application/Espo/Resources/defaults/config.php +++ b/application/Espo/Resources/defaults/config.php @@ -295,5 +295,6 @@ return [ 'listPagination' => true, 'starsLimit' => 500, 'quickSearchFullTextAppendWildcard' => false, + 'loggerDatabase' => false, 'isInstalled' => false, ]; diff --git a/application/Espo/Resources/defaults/systemConfig.php b/application/Espo/Resources/defaults/systemConfig.php index bce77e79ba..373fbd367e 100644 --- a/application/Espo/Resources/defaults/systemConfig.php +++ b/application/Espo/Resources/defaults/systemConfig.php @@ -53,6 +53,7 @@ return [ 'database', 'crud', 'logger', + 'loggerDatabase', 'isInstalled', 'systemUser', 'defaultPermissions', diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 1b9a416d12..1c9225732d 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -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.", diff --git a/application/Espo/Resources/i18n/en_US/AppLogRecord.json b/application/Espo/Resources/i18n/en_US/AppLogRecord.json new file mode 100644 index 0000000000..8013998a2c --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/AppLogRecord.json @@ -0,0 +1,10 @@ +{ + "fields": { + "message": "Message", + "code": "Code", + "level": "Level", + "exceptionClass": "Exception Class", + "file": "File", + "line": "Line" + } +} diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 569798285d..b4970b445a 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -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", diff --git a/application/Espo/Resources/layouts/AppLogRecord/detail.json b/application/Espo/Resources/layouts/AppLogRecord/detail.json new file mode 100644 index 0000000000..7ad79dcea3 --- /dev/null +++ b/application/Espo/Resources/layouts/AppLogRecord/detail.json @@ -0,0 +1,11 @@ +[ + { + "rows": [ + [{"name": "level"}, {"name": "code"}], + [{"name": "message"}], + [{"name": "exceptionClass"}], + [{"name": "file"}], + [{"name": "line"}, false] + ] + } +] diff --git a/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json b/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json new file mode 100644 index 0000000000..c1c7a160dd --- /dev/null +++ b/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json @@ -0,0 +1,11 @@ +[ + { + "rows": [ + [{"name":"level"}, {"name": "code"}], + [{"name": "message"}], + [{"name": "exceptionClass"}], + [{"name": "file"}], + [{"name": "line"}, false] + ] + } +] diff --git a/application/Espo/Resources/layouts/AppLogRecord/filters.json b/application/Espo/Resources/layouts/AppLogRecord/filters.json new file mode 100644 index 0000000000..fd80cf2dc9 --- /dev/null +++ b/application/Espo/Resources/layouts/AppLogRecord/filters.json @@ -0,0 +1,8 @@ +[ + "createdAt", + "level", + "code", + "exceptionClass", + "file", + "line" +] diff --git a/application/Espo/Resources/layouts/AppLogRecord/list.json b/application/Espo/Resources/layouts/AppLogRecord/list.json new file mode 100644 index 0000000000..2aa72cc31a --- /dev/null +++ b/application/Espo/Resources/layouts/AppLogRecord/list.json @@ -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 + } +] diff --git a/application/Espo/Resources/metadata/app/adminPanel.json b/application/Espo/Resources/metadata/app/adminPanel.json index 2bfeb93d34..34eebf13b2 100644 --- a/application/Espo/Resources/metadata/app/adminPanel.json +++ b/application/Espo/Resources/metadata/app/adminPanel.json @@ -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 diff --git a/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json b/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json new file mode 100644 index 0000000000..72860f838c --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json @@ -0,0 +1,5 @@ +{ + "controller": "controllers/record", + "createDisabled": true, + "editDisabled": true +} diff --git a/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json b/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json new file mode 100644 index 0000000000..5cb6de4a02 --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json @@ -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 +} diff --git a/application/Espo/Resources/metadata/scopes/AppLogRecord.json b/application/Espo/Resources/metadata/scopes/AppLogRecord.json new file mode 100644 index 0000000000..a5280171da --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/AppLogRecord.json @@ -0,0 +1,7 @@ +{ + "entity": true, + "layouts": false, + "tab": false, + "acl": false, + "customizable": false +} diff --git a/client/src/controllers/admin.js b/client/src/controllers/admin.js index a675dee0eb..8ec31097b4 100644 --- a/client/src/controllers/admin.js +++ b/client/src/controllers/admin.js @@ -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; From b006349528d732a3910c0c75e14d7fe6e434f7a1 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 4 May 2024 13:05:08 +0300 Subject: [PATCH 3/3] app log dev --- application/Espo/Classes/Cleanup/AppLog.php | 65 +++++++++++++++++++ .../AppLogRecord/PrimaryFilters/Errors.php | 49 ++++++++++++++ application/Espo/Controllers/AppLogRecord.php | 14 +++- application/Espo/Core/Container.php | 4 +- .../Espo/Core/Container/ContainerBuilder.php | 2 + .../Espo/Core/Log/Handler/DatabaseHandler.php | 27 +++++++- application/Espo/Core/Log/LogLoader.php | 11 ++-- application/Espo/Entities/AppLogRecord.php | 20 ++++++ .../Espo/ORM/Executor/DefaultSqlExecutor.php | 4 +- .../Espo/Resources/defaults/config.php | 4 +- .../Espo/Resources/defaults/systemConfig.php | 4 +- .../Resources/i18n/en_US/AppLogRecord.json | 7 +- .../layouts/AppLogRecord/detail.json | 14 +++- .../layouts/AppLogRecord/detailSmall.json | 14 +++- .../layouts/AppLogRecord/filters.json | 4 +- .../Resources/layouts/AppLogRecord/list.json | 10 +++ .../Espo/Resources/metadata/app/cleanup.json | 3 + .../metadata/clientDefs/AppLogRecord.json | 11 +++- .../metadata/entityDefs/AppLogRecord.json | 35 ++++++++++ .../metadata/recordDefs/AppLogRecord.json | 8 +++ .../metadata/selectDefs/AppLogRecord.json | 5 ++ .../views/admin/app-log-record/record/list.js | 35 ++++++++++ 22 files changed, 332 insertions(+), 18 deletions(-) create mode 100644 application/Espo/Classes/Cleanup/AppLog.php create mode 100644 application/Espo/Classes/Select/AppLogRecord/PrimaryFilters/Errors.php create mode 100644 application/Espo/Resources/metadata/recordDefs/AppLogRecord.json create mode 100644 application/Espo/Resources/metadata/selectDefs/AppLogRecord.json create mode 100644 client/src/views/admin/app-log-record/record/list.js diff --git a/application/Espo/Classes/Cleanup/AppLog.php b/application/Espo/Classes/Cleanup/AppLog.php new file mode 100644 index 0000000000..a5ec3c8b87 --- /dev/null +++ b/application/Espo/Classes/Cleanup/AppLog.php @@ -0,0 +1,65 @@ +. + * + * 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\Classes\Cleanup; + +use Espo\Core\Cleanup\Cleanup; +use Espo\Core\Field\DateTime; +use Espo\Core\Utils\Config; +use Espo\Entities\AppLogRecord; +use Espo\ORM\EntityManager; +use Espo\ORM\Query\DeleteBuilder; + +class AppLog implements Cleanup +{ + private const PERIOD = '30 days'; + + public function __construct( + private EntityManager $entityManager, + private Config $config + ) {} + + public function process(): void + { + $query = DeleteBuilder::create() + ->from(AppLogRecord::ENTITY_TYPE) + ->where(['createdAt<' => $this->getBefore()->toString()]) + ->build(); + + $this->entityManager->getQueryExecutor()->execute($query); + } + + private function getBefore(): DateTime + { + /** @var string $period */ + $period = $this->config->get('cleanupAppLogPeriod') ?? self::PERIOD; + + return DateTime::createNow()->modify('-' . $period); + } +} diff --git a/application/Espo/Classes/Select/AppLogRecord/PrimaryFilters/Errors.php b/application/Espo/Classes/Select/AppLogRecord/PrimaryFilters/Errors.php new file mode 100644 index 0000000000..f586495b59 --- /dev/null +++ b/application/Espo/Classes/Select/AppLogRecord/PrimaryFilters/Errors.php @@ -0,0 +1,49 @@ +. + * + * 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\Classes\Select\AppLogRecord\PrimaryFilters; + +use Espo\Core\Select\Primary\Filter; +use Espo\ORM\Query\SelectBuilder as QueryBuilder; +use Psr\Log\LogLevel; + +class Errors implements Filter +{ + public function apply(QueryBuilder $queryBuilder): void + { + $queryBuilder->where([ + 'level' => [ + ucfirst(LogLevel::ERROR), + ucfirst(LogLevel::EMERGENCY), + ucfirst(LogLevel::CRITICAL), + ucfirst(LogLevel::ALERT), + ] + ]); + } +} diff --git a/application/Espo/Controllers/AppLogRecord.php b/application/Espo/Controllers/AppLogRecord.php index 5f813e1c34..86c28c5944 100644 --- a/application/Espo/Controllers/AppLogRecord.php +++ b/application/Espo/Controllers/AppLogRecord.php @@ -39,7 +39,19 @@ class AppLogRecord extends Record { protected function checkAccess(): bool { - return $this->user->isAdmin(); + if (!$this->user->isAdmin()) { + return false; + } + + if (!$this->config->get('restrictedMode')) { + return true; + } + + if ($this->config->get('appLogAdminAllowed')) { + return true; + } + + return $this->user->isSuperAdmin(); } public function postActionCreate(Request $request, Response $response): stdClass diff --git a/application/Espo/Core/Container.php b/application/Espo/Core/Container.php index c9fb9567cd..35e6d94ed5 100644 --- a/application/Espo/Core/Container.php +++ b/application/Espo/Core/Container.php @@ -308,7 +308,9 @@ class Container implements ContainerInterface return $loader; } - assert($this->configuration !== null); + if ($this->configuration === null) { + throw new RuntimeException("Container configuration is not ready."); + } return $this->configuration->getLoaderClassName($id); } diff --git a/application/Espo/Core/Container/ContainerBuilder.php b/application/Espo/Core/Container/ContainerBuilder.php index 66349d39f3..c02cb068f6 100644 --- a/application/Espo/Core/Container/ContainerBuilder.php +++ b/application/Espo/Core/Container/ContainerBuilder.php @@ -36,6 +36,7 @@ use Espo\Core\Binding\BindingContainer; use Espo\Core\Binding\BindingLoader; use Espo\Core\Binding\EspoBindingLoader; +use Espo\Core\Loaders\ApplicationState as ApplicationStateLoader; use Espo\Core\Utils\File\Manager as FileManager; use Espo\Core\Utils\Config\ConfigFileManager; use Espo\Core\Utils\Config; @@ -71,6 +72,7 @@ class ContainerBuilder 'log' => LogLoader::class, 'dataManager' => DataManagerLoader::class, 'metadata' => MetadataLoader::class, + 'applicationState' => ApplicationStateLoader::class, ]; public function withBindingLoader(BindingLoader $bindingLoader): self diff --git a/application/Espo/Core/Log/Handler/DatabaseHandler.php b/application/Espo/Core/Log/Handler/DatabaseHandler.php index d56449e53c..19a58929e1 100644 --- a/application/Espo/Core/Log/Handler/DatabaseHandler.php +++ b/application/Espo/Core/Log/Handler/DatabaseHandler.php @@ -29,6 +29,8 @@ namespace Espo\Core\Log\Handler; +use Espo\Core\Api\Request; +use Espo\Core\ApplicationState; use Espo\Core\ORM\EntityManagerProxy; use Espo\Entities\AppLogRecord; use Monolog\Handler\HandlerInterface; @@ -40,11 +42,20 @@ class DatabaseHandler implements HandlerInterface { public function __construct( private Level $level, - private EntityManagerProxy $entityManager + private EntityManagerProxy $entityManager, + private ApplicationState $applicationState ) {} public function isHandling(LogRecord $record): bool { + if (!$this->applicationState->hasUser()) { + return false; + } + + if ($record->context['isSql'] ?? false) { + return false; + } + return $record->level->value >= $this->level->value; } @@ -65,6 +76,7 @@ class DatabaseHandler implements HandlerInterface ->setMessage($message); $this->setException($record, $logRecord); + $this->setRequest($record, $logRecord); $this->entityManager->saveEntity($logRecord); } @@ -117,4 +129,17 @@ class DatabaseHandler implements HandlerInterface ->setLine($exception->getLine()) ->setCode($exception->getCode()); } + + private function setRequest(LogRecord $record, AppLogRecord $logRecord): void + { + $request = $record->context['request'] ?? null; + + if (!$request instanceof Request) { + return; + } + + $logRecord + ->setRequestMethod($request->getMethod()) + ->setRequestResourcePath($request->getResourcePath()); + } } diff --git a/application/Espo/Core/Log/LogLoader.php b/application/Espo/Core/Log/LogLoader.php index d609febea6..281d9c0378 100644 --- a/application/Espo/Core/Log/LogLoader.php +++ b/application/Espo/Core/Log/LogLoader.php @@ -29,6 +29,7 @@ namespace Espo\Core\Log; +use Espo\Core\ApplicationState; use Espo\Core\Log\Handler\DatabaseHandler; use Espo\Core\Log\Handler\EspoFileHandler; use Espo\Core\Log\Handler\EspoRotatingFileHandler; @@ -50,7 +51,8 @@ class LogLoader public function __construct( private readonly Config $config, private readonly HandlerListLoader $handlerListLoader, - private readonly EntityManagerProxy $entityManagerProxy + private readonly EntityManagerProxy $entityManagerProxy, + private readonly ApplicationState $applicationState ) {} public function load(): Log @@ -68,7 +70,7 @@ class LogLoader $handlerList = [$this->createDefaultHandler()]; } - if ($this->config->get('loggerDatabase')) { + if ($this->config->get('logger.databaseHandler')) { $handlerList[] = $this->createDatabaseHandler(); } @@ -115,11 +117,12 @@ class LogLoader private function createDatabaseHandler(): HandlerInterface { - $rawLevel = $this->config->get('logger.level') ?? + $rawLevel = $this->config->get('logger.databaseHandlerLevel') ?? + $this->config->get('logger.level') ?? self::DEFAULT_LEVEL; $level = Logger::toMonologLevel($rawLevel); - return new DatabaseHandler($level, $this->entityManagerProxy); + return new DatabaseHandler($level, $this->entityManagerProxy, $this->applicationState); } } diff --git a/application/Espo/Entities/AppLogRecord.php b/application/Espo/Entities/AppLogRecord.php index bae6bada64..bd2e5fca0c 100644 --- a/application/Espo/Entities/AppLogRecord.php +++ b/application/Espo/Entities/AppLogRecord.php @@ -88,4 +88,24 @@ class AppLogRecord extends Entity return $this; } + + public function setRequestMethod(?string $requestMethod): self + { + $this->set('requestMethod', $requestMethod); + + return $this; + } + + public function setRequestResourcePath(?string $requestResourcePath): self + { + $len = $this->getAttributeParam('requestResourcePath', 'len'); + + if ($requestResourcePath && strlen($requestResourcePath) > $len) { + $requestResourcePath = substr($requestResourcePath, $len); + } + + $this->set('requestResourcePath', $requestResourcePath); + + return $this; + } } diff --git a/application/Espo/ORM/Executor/DefaultSqlExecutor.php b/application/Espo/ORM/Executor/DefaultSqlExecutor.php index a28711a966..af8a720bfc 100644 --- a/application/Espo/ORM/Executor/DefaultSqlExecutor.php +++ b/application/Espo/ORM/Executor/DefaultSqlExecutor.php @@ -59,7 +59,7 @@ class DefaultSqlExecutor implements SqlExecutor public function execute(string $sql, bool $rerunIfDeadlock = false): PDOStatement { if ($this->logAll) { - $this->logger?->info("SQL: " . $sql); + $this->logger?->info("SQL: " . $sql, ['isSql' => true]); } if (!$rerunIfDeadlock) { @@ -81,7 +81,7 @@ class DefaultSqlExecutor implements SqlExecutor if ($counter === 0 || !$this->isExceptionIsDeadlock($e)) { if ($this->logFailed) { - $this->logger?->error("SQL failed: " . $sql); + $this->logger?->error("SQL failed: " . $sql, ['isSql' => true]); } /** @var PDOException $e */ diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php index 897d663316..4a44ce044d 100644 --- a/application/Espo/Resources/defaults/config.php +++ b/application/Espo/Resources/defaults/config.php @@ -236,6 +236,9 @@ return [ 'cleanupSubscribers' => true, 'cleanupAudit' => true, 'cleanupAuditPeriod' => '3 months', + 'cleanupAppLog' => true, + 'cleanupAppLogPeriod' => '30 days', + 'appLogAdminAllowed' => false, 'currencyFormat' => 2, 'currencyDecimalPlaces' => 2, 'aclAllowDeleteCreated' => false, @@ -295,6 +298,5 @@ return [ 'listPagination' => true, 'starsLimit' => 500, 'quickSearchFullTextAppendWildcard' => false, - 'loggerDatabase' => false, 'isInstalled' => false, ]; diff --git a/application/Espo/Resources/defaults/systemConfig.php b/application/Espo/Resources/defaults/systemConfig.php index 373fbd367e..e04793c937 100644 --- a/application/Espo/Resources/defaults/systemConfig.php +++ b/application/Espo/Resources/defaults/systemConfig.php @@ -53,7 +53,6 @@ return [ 'database', 'crud', 'logger', - 'loggerDatabase', 'isInstalled', 'systemUser', 'defaultPermissions', @@ -112,6 +111,8 @@ return [ 'passwordChangeRequestNewUserLifetime', 'passwordChangeRequestExistingUserLifetime', 'passwordRecoveryInternalIntervalPeriod', + 'cleanupAppLog', + 'cleanupAppLogPeriod', ], 'adminItems' => [ 'devMode', @@ -220,6 +221,7 @@ return [ 'jobRunInParallel', 'jobPoolConcurrencyNumber', 'jobPeriodForActiveProcess', + 'appLogAdminAllowed', 'cronMinInterval', 'daemonInterval', 'daemonProcessTimeout', diff --git a/application/Espo/Resources/i18n/en_US/AppLogRecord.json b/application/Espo/Resources/i18n/en_US/AppLogRecord.json index 8013998a2c..e17fca5920 100644 --- a/application/Espo/Resources/i18n/en_US/AppLogRecord.json +++ b/application/Espo/Resources/i18n/en_US/AppLogRecord.json @@ -5,6 +5,11 @@ "level": "Level", "exceptionClass": "Exception Class", "file": "File", - "line": "Line" + "line": "Line", + "requestMethod": "Request Method", + "requestResourcePath": "Request Resource Path" + }, + "presetFilters": { + "errors": "Errors" } } diff --git a/application/Espo/Resources/layouts/AppLogRecord/detail.json b/application/Espo/Resources/layouts/AppLogRecord/detail.json index 7ad79dcea3..1eb06731b0 100644 --- a/application/Espo/Resources/layouts/AppLogRecord/detail.json +++ b/application/Espo/Resources/layouts/AppLogRecord/detail.json @@ -1,11 +1,21 @@ [ { "rows": [ - [{"name": "level"}, {"name": "code"}], - [{"name": "message"}], + [{"name": "level"}, false], + [{"name": "message"}] + ] + }, + { + "rows": [ + [{"name": "code"}, false], [{"name": "exceptionClass"}], [{"name": "file"}], [{"name": "line"}, false] ] + }, + { + "rows": [ + [{"name": "requestMethod"}, {"name": "requestResourcePath"}] + ] } ] diff --git a/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json b/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json index c1c7a160dd..1eb06731b0 100644 --- a/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json +++ b/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json @@ -1,11 +1,21 @@ [ { "rows": [ - [{"name":"level"}, {"name": "code"}], - [{"name": "message"}], + [{"name": "level"}, false], + [{"name": "message"}] + ] + }, + { + "rows": [ + [{"name": "code"}, false], [{"name": "exceptionClass"}], [{"name": "file"}], [{"name": "line"}, false] ] + }, + { + "rows": [ + [{"name": "requestMethod"}, {"name": "requestResourcePath"}] + ] } ] diff --git a/application/Espo/Resources/layouts/AppLogRecord/filters.json b/application/Espo/Resources/layouts/AppLogRecord/filters.json index fd80cf2dc9..b62c075f4c 100644 --- a/application/Espo/Resources/layouts/AppLogRecord/filters.json +++ b/application/Espo/Resources/layouts/AppLogRecord/filters.json @@ -4,5 +4,7 @@ "code", "exceptionClass", "file", - "line" + "line", + "requestMethod", + "requestResourcePath" ] diff --git a/application/Espo/Resources/layouts/AppLogRecord/list.json b/application/Espo/Resources/layouts/AppLogRecord/list.json index 2aa72cc31a..628e1e5eb7 100644 --- a/application/Espo/Resources/layouts/AppLogRecord/list.json +++ b/application/Espo/Resources/layouts/AppLogRecord/list.json @@ -12,5 +12,15 @@ "name": "file", "width": 20, "hidden": true + }, + { + "name": "requestMethod", + "width": 10, + "hidden": true + }, + { + "name": "requestResourcePath", + "width": 20, + "hidden": true } ] diff --git a/application/Espo/Resources/metadata/app/cleanup.json b/application/Espo/Resources/metadata/app/cleanup.json index ed90e1cc5c..99ae71a734 100644 --- a/application/Espo/Resources/metadata/app/cleanup.json +++ b/application/Espo/Resources/metadata/app/cleanup.json @@ -27,5 +27,8 @@ }, "stars": { "className": "Espo\\Classes\\Cleanup\\Stars" + }, + "appLog": { + "className": "Espo\\Classes\\Cleanup\\AppLog" } } diff --git a/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json b/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json index 72860f838c..60c563c7f2 100644 --- a/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json +++ b/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json @@ -1,5 +1,14 @@ { "controller": "controllers/record", "createDisabled": true, - "editDisabled": true + "editDisabled": true, + "mergeDisabled": true, + "recordViews": { + "list": "views/admin/app-log-record/record/list" + }, + "filterList": [ + { + "name": "errors" + } + ] } diff --git a/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json b/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json index 5cb6de4a02..85502c3582 100644 --- a/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json +++ b/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json @@ -25,6 +25,15 @@ "Alert", "Emergency" ], + "style": { + "Info": "info", + "Notice": "primary", + "Warning": "warning", + "Error": "danger", + "Critical": "danger", + "Alert": "danger", + "Emergency": "danger" + }, "maxLength": 9, "readOnly": true, "index": true @@ -46,6 +55,32 @@ "line": { "type": "int", "readOnly": true + }, + "requestMethod": { + "type": "enum", + "maxLength": 7, + "options": [ + "GET", + "POST", + "PUT", + "UPDATE", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", + "TRACE" + ], + "readOnly": true + }, + "requestResourcePath": { + "type": "varchar", + "maxLength": 255, + "readOnly": true + }, + "requestUrl": { + "type": "varchar", + "maxLength": 512, + "readOnly": true } }, "links": {}, diff --git a/application/Espo/Resources/metadata/recordDefs/AppLogRecord.json b/application/Espo/Resources/metadata/recordDefs/AppLogRecord.json new file mode 100644 index 0000000000..c2bbb32733 --- /dev/null +++ b/application/Espo/Resources/metadata/recordDefs/AppLogRecord.json @@ -0,0 +1,8 @@ +{ + "massActions": { + "delete": { + "allowed": true + } + }, + "actionHistoryDisabled": true +} diff --git a/application/Espo/Resources/metadata/selectDefs/AppLogRecord.json b/application/Espo/Resources/metadata/selectDefs/AppLogRecord.json new file mode 100644 index 0000000000..ba0a68b965 --- /dev/null +++ b/application/Espo/Resources/metadata/selectDefs/AppLogRecord.json @@ -0,0 +1,5 @@ +{ + "primaryFilterClassNameMap": { + "errors": "Espo\\Classes\\Select\\AppLogRecord\\PrimaryFilters\\Errors" + } +} diff --git a/client/src/views/admin/app-log-record/record/list.js b/client/src/views/admin/app-log-record/record/list.js new file mode 100644 index 0000000000..0851cb4227 --- /dev/null +++ b/client/src/views/admin/app-log-record/record/list.js @@ -0,0 +1,35 @@ +/************************************************************************ + * 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 . + * + * 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. + ************************************************************************/ + +import ListRecordView from 'views/record/list'; + +export default class extends ListRecordView { + + forceSettings = true +} +