diff --git a/ProcessMaker/Casts/MillisecondsToDateCast.php b/ProcessMaker/Casts/MillisecondsToDateCast.php new file mode 100644 index 0000000000..100f8673a0 --- /dev/null +++ b/ProcessMaker/Casts/MillisecondsToDateCast.php @@ -0,0 +1,40 @@ + $attributes + * + * @return Carbon|null + */ + public function get(Model $model, string $key, mixed $value, array $attributes): Carbon|null + { + return $value ? Carbon::createFromTimestampMs($value) : null; + } + + /** + * Prepare the given value for storage. + * + * @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): float|null + { + return $value ? Carbon::parse($value)->valueOf() : null; + } +} 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)); 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(); + } } } 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/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'); + }); + } +}; 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