From f0133bea681fa7619c6bbd499e80cf0e15884fb9 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Mon, 13 Jan 2025 09:33:19 -0400 Subject: [PATCH 1/5] Add timestamps in milliseconds to process_request_tokens table --- ...mps_ms_to_process_request_tokens_table.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 database/migrations/2025_01_13_212522_add_timestamps_ms_to_process_request_tokens_table.php diff --git a/database/migrations/2025_01_13_212522_add_timestamps_ms_to_process_request_tokens_table.php b/database/migrations/2025_01_13_212522_add_timestamps_ms_to_process_request_tokens_table.php new file mode 100644 index 0000000000..8fded45da5 --- /dev/null +++ b/database/migrations/2025_01_13_212522_add_timestamps_ms_to_process_request_tokens_table.php @@ -0,0 +1,30 @@ +bigInteger('created_at_ms')->nullable(); + $table->bigInteger('completed_at_ms')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('process_request_tokens', function (Blueprint $table) { + $table->dropColumn('created_at_ms'); + $table->dropColumn('completed_at_ms'); + }); + } +}; From 0f92336a622db0cf1412ac9e6b3e75c0cc30ced5 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Mon, 13 Jan 2025 12:33:48 -0400 Subject: [PATCH 2/5] Add MillisecondsToDateCast for handling milliseconds timestamps in ProcessRequestToken --- ProcessMaker/Casts/MillisecondsToDateCast.php | 30 +++++++++++++++++++ ProcessMaker/Models/ProcessRequestToken.php | 5 ++++ .../Observers/ProcessRequestTokenObserver.php | 8 +++++ 3 files changed, 43 insertions(+) create mode 100644 ProcessMaker/Casts/MillisecondsToDateCast.php diff --git a/ProcessMaker/Casts/MillisecondsToDateCast.php b/ProcessMaker/Casts/MillisecondsToDateCast.php new file mode 100644 index 0000000000..d2f5453774 --- /dev/null +++ b/ProcessMaker/Casts/MillisecondsToDateCast.php @@ -0,0 +1,30 @@ + $attributes + */ + public function get(Model $model, string $key, mixed $value, array $attributes): mixed + { + return $value ? Carbon::createFromTimestampMs($value) : null; + } + + /** + * Prepare the given value for storage. + * + * @param array $attributes + */ + public function set(Model $model, string $key, mixed $value, array $attributes): mixed + { + return $value ? Carbon::parse($value)->valueOf() : null; + } +} diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php index 9e266fe1cd..46b75c4575 100644 --- a/ProcessMaker/Models/ProcessRequestToken.php +++ b/ProcessMaker/Models/ProcessRequestToken.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Notification; use Laravel\Scout\Searchable; use Log; +use ProcessMaker\Casts\MillisecondsToDateCast; use ProcessMaker\Events\ActivityAssigned; use ProcessMaker\Events\ActivityReassignment; use ProcessMaker\Facades\WorkflowUserManager; @@ -45,6 +46,8 @@ * @property Carbon $riskchanges_at * @property Carbon $updated_at * @property Carbon $created_at + * @property Carbon $created_at_ms + * @property Carbon $completed_at_ms * @property ProcessRequest $processRequest * * @OA\Schema( @@ -154,6 +157,8 @@ class ProcessRequestToken extends ProcessMakerModel implements TokenInterface 'token_properties' => 'array', 'is_priority' => 'boolean', 'is_actionbyemail' => 'boolean', + 'created_at_ms' => MillisecondsToDateCast::class, + 'completed_at_ms' => MillisecondsToDateCast::class, ]; /** diff --git a/ProcessMaker/Observers/ProcessRequestTokenObserver.php b/ProcessMaker/Observers/ProcessRequestTokenObserver.php index 4a5dbd619b..09e6279272 100644 --- a/ProcessMaker/Observers/ProcessRequestTokenObserver.php +++ b/ProcessMaker/Observers/ProcessRequestTokenObserver.php @@ -20,6 +20,11 @@ public function saved(ProcessRequestToken $token) } } + public function creating(ProcessRequestToken $token) + { + $token->created_at_ms = now(); + } + /** * Once a token is saved, it also saves the version reference of the * screen or script executed @@ -29,5 +34,8 @@ public function saved(ProcessRequestToken $token) public function saving(ProcessRequestToken $token) { $token->saveVersion(); + if ($token->completed_at && $token->isDirty('completed_at')) { + $token->completed_at_ms = now(); + } } } From 12c28ec20c87006186bdb59b1d29bdc60a0e825a Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Mon, 13 Jan 2025 17:34:48 -0400 Subject: [PATCH 3/5] Add Prometheus metric for activity execution time --- ProcessMaker/Listeners/BpmnSubscriber.php | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ProcessMaker/Listeners/BpmnSubscriber.php b/ProcessMaker/Listeners/BpmnSubscriber.php index 6eef4e1010..239bc94659 100644 --- a/ProcessMaker/Listeners/BpmnSubscriber.php +++ b/ProcessMaker/Listeners/BpmnSubscriber.php @@ -8,6 +8,7 @@ use ProcessMaker\Events\ActivityAssigned; use ProcessMaker\Events\ActivityCompleted; use ProcessMaker\Events\ProcessCompleted; +use ProcessMaker\Facades\Metrics; use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\Jobs\TerminateRequestEndEvent; use ProcessMaker\Models\Comment; @@ -144,6 +145,32 @@ public function onActivityCompleted(ActivityCompletedEvent $event) } Log::info('Activity completed: ' . json_encode($token->getProperties())); + // Prometheus Metric: Activity Execution Time + $startTime = $token->created_at_ms; + $completedTime = $token->completed_at_ms; + $executionTime = $completedTime->diffInMilliseconds($startTime); + Metrics::histogram( + 'activity_execution_time_seconds', + 'Activity Execution Time', + [ + 'activity_id', + 'activity_name', + 'element_type', + 'process_id', + 'request_id', + ], + [1, 10, 3600, 86400] + )->observe( + $executionTime, + [ + 'activity_id' => $token->element_id, + 'activity_name' => $token->element_name, + 'element_type' => $token->element_type, + 'process_id' => $token->process_id, + 'request_id' => $token->process_request_id, + ] + ); + if ($token->element_type == 'task') { $notifiables = $token->getNotifiables('completed'); Notification::send($notifiables, new ActivityCompletedNotification($token)); From 2e6bfde5cbcd718064bbb5ad5abb5c2b6177c5c7 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Mon, 13 Jan 2025 18:39:23 -0400 Subject: [PATCH 4/5] Update Main Dashboard to include Task Completion Timing report --- README.md | 4 ++ resources/grafana/MainDashboard.json | 79 +++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e027ef7a63..e094ea0e60 100644 --- a/README.md +++ b/README.md @@ -525,6 +525,10 @@ To make things even easier, you can run `Metrics::counter('cases')->inc();` or ` You can provide an optional description, for example `Metrics::gauge('active_tasks', 'Total Active Tasks')->...` +### Import Grafana Dashboards + +Go to Grafana and import the dashboards from the `resources/grafana` folder. Each JSON file represents a configured dashboard that can be imported into Grafana to visualize metrics and data. + # License Distributed under the [AGPL Version 3](https://www.gnu.org/licenses/agpl-3.0.en.html) diff --git a/resources/grafana/MainDashboard.json b/resources/grafana/MainDashboard.json index 82201440bd..f6fb56c215 100644 --- a/resources/grafana/MainDashboard.json +++ b/resources/grafana/MainDashboard.json @@ -429,6 +429,83 @@ ], "title": "Tasks Completed", "type": "barchart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS-PM-SPRING-2025}" + }, + "description": "Time a task was completed", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "fieldMinMax": false, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "interval": "15s", + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS-PM-SPRING-2025}" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "topk(5, avg(processmaker_activity_execution_time_seconds_sum{element_type=\"task\"}) by (process_id, activity_name))", + "format": "time_series", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "interval": "", + "legendFormat": "{{activity_name}} (process={{process_id}})", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Task Execution Time", + "type": "gauge" } ], "schemaVersion": 40, @@ -444,6 +521,6 @@ "timezone": "browser", "title": "ProcessMaker Dashboard", "uid": "be96wxsnlmn7kc", - "version": 23, + "version": 29, "weekStart": "" } \ No newline at end of file From b7375b83ce52f9b718aa06aeee9fe747b1f9c482 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Tue, 14 Jan 2025 17:17:39 -0400 Subject: [PATCH 5/5] Update MillisecondsToDateCast to specify return types for get and set methods --- ProcessMaker/Casts/MillisecondsToDateCast.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/ProcessMaker/Casts/MillisecondsToDateCast.php b/ProcessMaker/Casts/MillisecondsToDateCast.php index d2f5453774..100f8673a0 100644 --- a/ProcessMaker/Casts/MillisecondsToDateCast.php +++ b/ProcessMaker/Casts/MillisecondsToDateCast.php @@ -11,9 +11,14 @@ class MillisecondsToDateCast implements CastsAttributes /** * Cast the given value. * - * @param array $attributes + * @param Model $model + * @param string $key + * @param mixed $value + * @param array $attributes + * + * @return Carbon|null */ - public function get(Model $model, string $key, mixed $value, array $attributes): mixed + public function get(Model $model, string $key, mixed $value, array $attributes): Carbon|null { return $value ? Carbon::createFromTimestampMs($value) : null; } @@ -21,9 +26,14 @@ public function get(Model $model, string $key, mixed $value, array $attributes): /** * Prepare the given value for storage. * - * @param array $attributes + * @param Model $model + * @param string $key + * @param mixed $value + * @param array $attributes + * + * @return float|null */ - public function set(Model $model, string $key, mixed $value, array $attributes): mixed + public function set(Model $model, string $key, mixed $value, array $attributes): float|null { return $value ? Carbon::parse($value)->valueOf() : null; }