From b74d74f2f3a0388e45d6c92811ff44a7dfd5dcfb Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Wed, 10 Sep 2025 12:18:59 -0400 Subject: [PATCH 1/2] Add tenant-aware logging functionality --- ProcessMaker/Logging/TenantAwareHandler.php | 125 ++++++++++++++++++ .../Logging/TenantAwareLogManager.php | 86 ++++++++++++ .../TenantLoggingServiceProvider.php | 32 +++++ config/app.php | 1 + config/logging.php | 15 ++- 5 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 ProcessMaker/Logging/TenantAwareHandler.php create mode 100644 ProcessMaker/Logging/TenantAwareLogManager.php create mode 100644 ProcessMaker/Providers/TenantLoggingServiceProvider.php diff --git a/ProcessMaker/Logging/TenantAwareHandler.php b/ProcessMaker/Logging/TenantAwareHandler.php new file mode 100644 index 0000000000..7b9733a0a5 --- /dev/null +++ b/ProcessMaker/Logging/TenantAwareHandler.php @@ -0,0 +1,125 @@ +baseFilename = $filename; + $this->maxFiles = $maxFiles; + + parent::__construct($level, $bubble); + + // Set default formatter + $this->setFormatter(new LineFormatter( + "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", + 'Y-m-d H:i:s', + true, + true + )); + } + + /** + * Handle a log record. + * + * @param LogRecord $record + * @return bool + */ + public function handle(LogRecord $record): bool + { + // Get current tenant ID from context + $tenantId = Context::get('tenantId') ?? 'no-tenant'; + + // Get or create handler for this tenant + $handler = $this->getHandlerForTenant($tenantId); + + // Remove context from the log record (e.g. {"tenantId":1}) + $record->extra = []; + + return $handler->handle($record); + } + + /** + * Get handler for specific tenant. + * + * @param string $tenantId + * @return \Monolog\Handler\RotatingFileHandler + */ + private function getHandlerForTenant(string $tenantId): \Monolog\Handler\RotatingFileHandler + { + if (!isset($this->handlers[$tenantId])) { + $filename = $this->getTenantFilename($tenantId); + + $this->handlers[$tenantId] = new \Monolog\Handler\RotatingFileHandler( + $filename, + $this->maxFiles, + $this->level, + $this->bubble + ); + + $this->handlers[$tenantId]->setFormatter($this->getFormatter()); + } + + return $this->handlers[$tenantId]; + } + + /** + * Get tenant-specific filename. + * + * @param string $tenantId + * @return string + */ + private function getTenantFilename(string $tenantId): string + { + $pathInfo = pathinfo($this->baseFilename); + $directory = $pathInfo['dirname']; + $filename = $pathInfo['filename']; + $extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : ''; + + // Define the log directory + $logDir = $directory; + if (!is_dir($logDir)) { + mkdir($logDir, 0755, true); + } + + // If tenant ID is no-tenant, return the base filename + if ($tenantId === 'no-tenant') { + return $logDir . '/' . $filename . $extension; + } + // Create tenant-specific filename + return $logDir . '/' . $filename . '_tenant_' . $tenantId . $extension; + } + + /** + * Write a log record. + * + * @param LogRecord $record + * @return void + */ + protected function write(LogRecord $record): void + { + // This method is not used since we override handle() + } +} diff --git a/ProcessMaker/Logging/TenantAwareLogManager.php b/ProcessMaker/Logging/TenantAwareLogManager.php new file mode 100644 index 0000000000..2e0948bcb1 --- /dev/null +++ b/ProcessMaker/Logging/TenantAwareLogManager.php @@ -0,0 +1,86 @@ +getDefaultDriver(); + + // If we're using the default channel and have tenant context, use tenant channel + if ($channel === $this->getDefaultDriver() && Context::get('tenantId')) { + return $this->get('tenant'); + } + + return parent::channel($channel); + } + + /** + * Get the default log driver name. + * + * @return string + */ + public function getDefaultDriver() + { + return $this->app['config']['logging.default']; + } + + /** + * Create a custom log channel instance. + * + * @param array $config + * @return \Psr\Log\LoggerInterface + */ + protected function createCustomDriver(array $config) + { + $driver = $config['driver'] ?? null; + + if ($driver === 'tenant-aware') { + return $this->createTenantAwareDriver($config); + } + + return parent::createCustomDriver($config); + } + + /** + * Create a tenant-aware driver. + * + * @param array $config + * @return \Psr\Log\LoggerInterface + */ + protected function createTenantAwareDriver(array $config) + { + $tenantId = Context::get('tenantId') ?? 'no-tenant'; + + // Create tenant-specific log path (use base_path to avoid tenant storage path issues) + $logPath = base_path("storage/logs/tenants/tenant_{$tenantId}.log"); + + // Ensure the directory exists + $logDir = dirname($logPath); + if (!is_dir($logDir)) { + mkdir($logDir, 0755, true); + } + + // Create the logger with tenant-specific configuration + return $this->app->make('log')->build([ + 'driver' => 'daily', + 'path' => $logPath, + 'level' => $config['level'] ?? 'debug', + 'days' => $config['days'] ?? 7, + 'processors' => [ + \Monolog\Processor\PsrLogMessageProcessor::class, + \ProcessMaker\Logging\TenantContextProcessor::class, + ], + ]); + } +} diff --git a/ProcessMaker/Providers/TenantLoggingServiceProvider.php b/ProcessMaker/Providers/TenantLoggingServiceProvider.php new file mode 100644 index 0000000000..a3bdecfc6c --- /dev/null +++ b/ProcessMaker/Providers/TenantLoggingServiceProvider.php @@ -0,0 +1,32 @@ +app->singleton('log', function ($app) { + return new TenantAwareLogManager($app); + }); + } + + /** + * Bootstrap services. + * + * @return void + */ + public function boot() + { + // + } +} diff --git a/config/app.php b/config/app.php index f94062de38..7e2aa7a303 100644 --- a/config/app.php +++ b/config/app.php @@ -183,6 +183,7 @@ * ProcessMaker Service Providers */ ProcessMaker\Providers\ProcessMakerServiceProvider::class, + ProcessMaker\Providers\TenantLoggingServiceProvider::class, ProcessMaker\Providers\RecommendationsServiceProvider::class, ProcessMaker\Providers\AuthServiceProvider::class, ProcessMaker\Providers\EventServiceProvider::class, diff --git a/config/logging.php b/config/logging.php index 8f68eeced3..f7c4b655a3 100644 --- a/config/logging.php +++ b/config/logging.php @@ -68,11 +68,16 @@ ], 'daily' => [ - 'driver' => 'daily', - 'path' => base_path('storage/logs/processmaker.log'), - 'level' => env('LOG_LEVEL', 'debug'), - 'days' => 7, - 'replace_placeholders' => true, + 'driver' => 'monolog', + 'handler' => \ProcessMaker\Logging\TenantAwareHandler::class, + 'handler_with' => [ + 'filename' => base_path('storage/logs/processmaker.log'), + 'maxFiles' => 7, + 'level' => env('LOG_LEVEL', 'debug'), + ], + 'processors' => [ + \Monolog\Processor\PsrLogMessageProcessor::class, + ], ], 'slack' => [ From eaea4138954f61f3d4c335d9458ff211547780b9 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Wed, 10 Sep 2025 12:27:14 -0400 Subject: [PATCH 2/2] Fix tenant settings database readiness check to support non-multitenancy configurations --- ProcessMaker/Repositories/SettingsConfigRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProcessMaker/Repositories/SettingsConfigRepository.php b/ProcessMaker/Repositories/SettingsConfigRepository.php index d392504c54..a111c7c307 100644 --- a/ProcessMaker/Repositories/SettingsConfigRepository.php +++ b/ProcessMaker/Repositories/SettingsConfigRepository.php @@ -118,7 +118,7 @@ private function readyToUseSettingsDatabase() { if (!$this->readyToUseSettingsDatabase) { $this->readyToUseSettingsDatabase = - app('tenant-resolved') && + (!config('app.multitenancy') || app('tenant-resolved')) && $this->databaseAvailable() && $this->redisAvailable() && $this->settingsTableExists();