From 228a61f2b9a22e296faaf25f8a79e8c8885fc235 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Fri, 16 Oct 2026 13:23:14 -0400 Subject: [PATCH 1/3] Implement configuration to enable/disable metrics --- ProcessMaker/Cache/AbstractCacheFactory.php | 2 +- ProcessMaker/Console/Kernel.php | 6 +- .../Exception/MetricsDisabledException.php | 15 +++++ ProcessMaker/Facades/Metrics.php | 2 + ProcessMaker/Services/MetricsService.php | 67 ++++++++++++++++--- config/metrics.php | 3 + tests/Feature/Metrics/TaskMetricsTest.php | 7 ++ tests/Feature/MetricsFacadeTest.php | 3 + tests/unit/MetricsServiceTest.php | 17 +++++ 9 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 ProcessMaker/Exception/MetricsDisabledException.php diff --git a/ProcessMaker/Cache/AbstractCacheFactory.php b/ProcessMaker/Cache/AbstractCacheFactory.php index bfcf09c3c0..645c00e765 100644 --- a/ProcessMaker/Cache/AbstractCacheFactory.php +++ b/ProcessMaker/Cache/AbstractCacheFactory.php @@ -40,7 +40,7 @@ public static function create(CacheManager $cacheManager, CacheMetricsInterface $cache = static::createInstance($cacheManager); // Wrap with metrics decorator - if (static::$storeMetrics) { + if (static::$storeMetrics && config('metrics.enabled', false)) { return new CacheMetricsDecorator($cache, $metrics); } diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index df4ce44dd8..353a452329 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -56,8 +56,9 @@ protected function schedule(Schedule $schedule) } } - $clearInterval = config('metrics.clear_interval', 10); - switch ((int) $clearInterval) { + if (config('metrics.enabled', false)) { + $clearInterval = config('metrics.clear_interval', 10); + switch ((int) $clearInterval) { case 1: $schedule->command('metrics:clear')->everyMinute(); break; @@ -85,6 +86,7 @@ protected function schedule(Schedule $schedule) default: $schedule->command('metrics:clear')->cron("*/{$clearInterval} * * * *"); break; + } } // evaluate cases retention policy diff --git a/ProcessMaker/Exception/MetricsDisabledException.php b/ProcessMaker/Exception/MetricsDisabledException.php new file mode 100644 index 0000000000..89cb6a93e8 --- /dev/null +++ b/ProcessMaker/Exception/MetricsDisabledException.php @@ -0,0 +1,15 @@ +namespace = config('app.prometheus_namespace', 'app'); + if (!$this->isEnabled() && $adapter === null) { + return; + } + + $this->initializeRegistry($adapter); + } + + public function isEnabled(): bool + { + return filter_var(config('metrics.enabled', false), FILTER_VALIDATE_BOOLEAN); + } + + private function initializeRegistry($adapter = null): void + { try { - // Set up Redis as the adapter if none is provided if ($adapter === null) { $redis = app('redis')->client(); $adapter = PrometheusRedis::fromExistingConnection($redis); @@ -57,6 +71,19 @@ public function __construct(private $adapter = null) } } + private function registry(): CollectorRegistry + { + if (!$this->isEnabled()) { + throw new MetricsDisabledException(); + } + + if (!isset($this->collectionRegistry)) { + $this->initializeRegistry($this->adapter); + } + + return $this->collectionRegistry; + } + /** * Get the collection registry. * @@ -64,7 +91,7 @@ public function __construct(private $adapter = null) */ public function getCollectionRegistry(): CollectorRegistry { - return $this->collectionRegistry; + return $this->registry(); } /** @@ -79,7 +106,7 @@ public function counter(string $name, string|null $help = null, array $labels = { $help = $help ?? $name; - return $this->collectionRegistry->getOrRegisterCounter( + return $this->registry()->getOrRegisterCounter( $this->namespace, $name, $help, @@ -99,7 +126,7 @@ public function gauge(string $name, string|null $help = null, array $labels = [] { $help = $help ?? $name; - return $this->collectionRegistry->getOrRegisterGauge( + return $this->registry()->getOrRegisterGauge( $this->namespace, $name, $help, @@ -120,7 +147,7 @@ public function histogram(string $name, string $help = null, array $labels = [], { $help = $help ?? $name; - return $this->collectionRegistry->getOrRegisterHistogram( + return $this->registry()->getOrRegisterHistogram( $this->namespace, $name, $help, @@ -138,7 +165,11 @@ public function histogram(string $name, string $help = null, array $labels = [], */ public function setGauge(string $name, float $value, array $labelValues = []): void { - $gauge = $this->collectionRegistry->getGauge($this->namespace, $name); + if (!$this->isEnabled()) { + return; + } + + $gauge = $this->registry()->getGauge($this->namespace, $name); $gauge->set($value, $labelValues); } @@ -149,6 +180,10 @@ public function setGauge(string $name, float $value, array $labelValues = []): v */ public function renderMetrics(): string { + if (!$this->isEnabled() || !isset($this->collectionRegistry)) { + return ''; + } + $renderer = new RenderTextFormat(); $metrics = $this->collectionRegistry->getMetricFamilySamples(); @@ -166,6 +201,10 @@ public function renderMetrics(): string */ public function counterInc(string $name, string $help = null, array $labels = []): void { + if (!$this->isEnabled()) { + return; + } + // Add system labels $labels = $this->addSystemLabels($labels); $labelKeys = array_keys($labels); @@ -185,6 +224,10 @@ public function counterInc(string $name, string $help = null, array $labels = [] */ public function histogramObserve(string $name, string $help = null, array $labels = [], array $buckets = [0.1, 1, 5, 10], float $executionTime = 0): void { + if (!$this->isEnabled()) { + return; + } + // Add system labels $labels = $this->addSystemLabels($labels); $labelKeys = array_keys($labels); @@ -220,6 +263,10 @@ public function addSystemLabels(array $labels) public function clearMetrics(): void { + if (!$this->isEnabled() || !isset($this->collectionRegistry)) { + return; + } + $this->collectionRegistry->wipeStorage(); } @@ -243,6 +290,10 @@ private function getApplicationVersion() */ public function collectQueueMetrics(): void { + if (!$this->isEnabled()) { + return; + } + $metricsRepository = app(MetricsRepository::class); $jobsRepository = app(JobRepository::class); $workloadRepository = app(WorkloadRepository::class); diff --git a/config/metrics.php b/config/metrics.php index 7e89219158..296d7f93a5 100644 --- a/config/metrics.php +++ b/config/metrics.php @@ -1,6 +1,9 @@ env('METRICS_ENABLED', false), + // Interval in minutes for clearing metrics. Can be set via METRICS_CLEAR_INTERVAL env variable. 'clear_interval' => env('METRICS_CLEAR_INTERVAL', 10), ]; diff --git a/tests/Feature/Metrics/TaskMetricsTest.php b/tests/Feature/Metrics/TaskMetricsTest.php index 9ffe37081c..eb49e44e76 100644 --- a/tests/Feature/Metrics/TaskMetricsTest.php +++ b/tests/Feature/Metrics/TaskMetricsTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Metrics; +use Illuminate\Support\Facades\Config; use ProcessMaker\Facades\Metrics; use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\Models\User; @@ -10,6 +11,12 @@ class TaskMetricsTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + Config::set('metrics.enabled', true); + } + public function test_task_metric_was_stored() { $user = User::factory()->create([ diff --git a/tests/Feature/MetricsFacadeTest.php b/tests/Feature/MetricsFacadeTest.php index 84baeecdb1..18708fa8c5 100644 --- a/tests/Feature/MetricsFacadeTest.php +++ b/tests/Feature/MetricsFacadeTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Config; use ProcessMaker\Facades\Metrics; use ProcessMaker\Services\MetricsService; use Prometheus\Storage\InMemory; @@ -19,6 +20,8 @@ protected function setUp(): void { parent::setUp(); + Config::set('metrics.enabled', true); + // Simulate the MetricsService with an InMemory adapter $adapter = new InMemory(); $mockService = new MetricsService($adapter); diff --git a/tests/unit/MetricsServiceTest.php b/tests/unit/MetricsServiceTest.php index b73f9ad831..c215543b72 100644 --- a/tests/unit/MetricsServiceTest.php +++ b/tests/unit/MetricsServiceTest.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; use Mockery; +use ProcessMaker\Exception\MetricsDisabledException; use ProcessMaker\Facades\Metrics; use ProcessMaker\Services\MetricsService; use Prometheus\CollectorRegistry; @@ -30,6 +31,8 @@ protected function setUp(): void { parent::setUp(); + Config::set('metrics.enabled', true); + // Use InMemory storage for testing $adapter = new InMemory(); $this->metricsService = new MetricsService($adapter); @@ -265,4 +268,18 @@ public function testClearMetrics() // Call clearMetrics which should call wipeStorage on the registry. $service->clearMetrics(); } + + public function testMetricsDisabledSkipsRegistration(): void + { + Config::set('metrics.enabled', false); + + $adapter = new InMemory(); + $service = new MetricsService($adapter); + + $service->counterInc('disabled_counter', 'Should not register', ['foo' => 'bar']); + $this->assertSame('', $service->renderMetrics()); + + $this->expectException(MetricsDisabledException::class); + $service->counter('disabled_counter_direct'); + } } From c4b52f63d3aa2c31068b66482634107d3727103e Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Fri, 16 Oct 2026 15:05:03 -0400 Subject: [PATCH 2/3] feat: enhance metrics:clear command with stagger option --- ProcessMaker/Console/Kernel.php | 55 ++++++++++++++++++++------------- routes/console.php | 7 ++++- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index 353a452329..d1b65d643d 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -56,48 +56,59 @@ protected function schedule(Schedule $schedule) } } - if (config('metrics.enabled', false)) { - $clearInterval = config('metrics.clear_interval', 10); - switch ((int) $clearInterval) { + $this->scheduleMetricsClear($schedule); + + // evaluate cases retention policy + $schedule->command('cases:retention:evaluate') + ->daily() + ->onOneServer() + ->withoutOverlapping() + ->runInBackground(); + + // 5 minutes is recommended in https://laravel.com/docs/12.x/horizon#metrics + $schedule->command('horizon:snapshot')->everyFiveMinutes(); + } + + protected function scheduleMetricsClear(Schedule $schedule): void + { + if (!config('metrics.enabled', false)) { + return; + } + + $clearInterval = (int) config('metrics.clear_interval', 10); + + $event = $schedule->command('metrics:clear --stagger'); + switch ($clearInterval) { case 1: - $schedule->command('metrics:clear')->everyMinute(); + $event->everyMinute(); break; case 2: - $schedule->command('metrics:clear')->everyTwoMinutes(); + $event->everyTwoMinutes(); break; case 5: - $schedule->command('metrics:clear')->everyFiveMinutes(); + $event->everyFiveMinutes(); break; case 10: - $schedule->command('metrics:clear')->everyTenMinutes(); + $event->everyTenMinutes(); break; case 15: - $schedule->command('metrics:clear')->everyFifteenMinutes(); + $event->everyFifteenMinutes(); break; case 30: - $schedule->command('metrics:clear')->everyThirtyMinutes(); + $event->everyThirtyMinutes(); break; case 60: - $schedule->command('metrics:clear')->hourly(); + $event->hourly(); break; case 1440: - $schedule->command('metrics:clear')->daily(); + $event->daily(); break; default: - $schedule->command('metrics:clear')->cron("*/{$clearInterval} * * * *"); + $event->cron("*/{$clearInterval} * * * *"); break; - } } - // evaluate cases retention policy - $schedule->command('cases:retention:evaluate') - ->daily() - ->onOneServer() - ->withoutOverlapping() - ->runInBackground(); - - // 5 minutes is recommended in https://laravel.com/docs/12.x/horizon#metrics - $schedule->command('horizon:snapshot')->everyFiveMinutes(); + $event->runInBackground()->withoutOverlapping($clearInterval); } /** diff --git a/routes/console.php b/routes/console.php index a996b9690f..470dbef211 100644 --- a/routes/console.php +++ b/routes/console.php @@ -41,7 +41,12 @@ } })->purpose('Display an inspiring quote'); -Artisan::command('metrics:clear', function () { +Artisan::command('metrics:clear {--stagger : Random delay before clear (scheduler only)}', function () { + if ($this->option('stagger')) { + $maxSeconds = (int) config('metrics.clear_interval', 10) * 60; + sleep(random_int(0, $maxSeconds)); + } + ProcessMaker\Facades\Metrics::clearMetrics(); echo "Metrics cleared.\n"; })->purpose('Clear all metrics using Metrics::clearMetrics'); From d6fb165f999dc691d809b4334ab9389abc04c1b9 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Fri, 16 Oct 2026 15:15:06 -0400 Subject: [PATCH 3/3] refactor: update metrics clear interval to default 24 hours and stagger to 60 sec --- ProcessMaker/Console/Kernel.php | 2 +- config/metrics.php | 7 +++++-- routes/console.php | 8 ++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index d1b65d643d..33a6f10e82 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -75,7 +75,7 @@ protected function scheduleMetricsClear(Schedule $schedule): void return; } - $clearInterval = (int) config('metrics.clear_interval', 10); + $clearInterval = (int) config('metrics.clear_interval', 1440); $event = $schedule->command('metrics:clear --stagger'); switch ($clearInterval) { diff --git a/config/metrics.php b/config/metrics.php index 296d7f93a5..cb23916dfd 100644 --- a/config/metrics.php +++ b/config/metrics.php @@ -4,6 +4,9 @@ // Enable Prometheus metric collection (counters, gauges, histograms). Default off. 'enabled' => env('METRICS_ENABLED', false), - // Interval in minutes for clearing metrics. Can be set via METRICS_CLEAR_INTERVAL env variable. - 'clear_interval' => env('METRICS_CLEAR_INTERVAL', 10), + // Interval in minutes for clearing metrics. Default 24 hours (1440 minutes). + 'clear_interval' => env('METRICS_CLEAR_INTERVAL', 1440), + + // Max random delay (seconds) before scheduled clear (--stagger). Capped at clear_interval × 60 at runtime. + 'clear_stagger_max_seconds' => env('METRICS_CLEAR_STAGGER_MAX_SECONDS', 60), ]; diff --git a/routes/console.php b/routes/console.php index 470dbef211..140dacac9b 100644 --- a/routes/console.php +++ b/routes/console.php @@ -43,8 +43,12 @@ Artisan::command('metrics:clear {--stagger : Random delay before clear (scheduler only)}', function () { if ($this->option('stagger')) { - $maxSeconds = (int) config('metrics.clear_interval', 10) * 60; - sleep(random_int(0, $maxSeconds)); + $intervalSeconds = max(0, (int) config('metrics.clear_interval', 1440) * 60); + $staggerMax = max(0, (int) config('metrics.clear_stagger_max_seconds', 60)); + $maxSeconds = min($staggerMax, $intervalSeconds); + if ($maxSeconds > 0) { + sleep(random_int(0, $maxSeconds)); + } } ProcessMaker\Facades\Metrics::clearMetrics();