Skip to content

Commit

Permalink
Merge pull request #2 from wol-soft/CustomDebugFormatter
Browse files Browse the repository at this point in the history
Add debug output formatter
  • Loading branch information
wol-soft committed Mar 23, 2022
2 parents 9b26f04 + dae0e7c commit f21bcef
Show file tree
Hide file tree
Showing 26 changed files with 701 additions and 237 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Bonus: you will get an execution log for each executed workflow - if you want to
* [Nested workflows](#Nested-workflows)
* [Loops](#Loops)
* [Error handling, logging and debugging](#Error-handling-logging-and-debugging)
* [Custom output formatter](#Custom-output-formatter)
* [Tests](#Tests)

## Installation
Expand Down Expand Up @@ -438,7 +439,7 @@ public function getWarnings(): array;
// get the exception which caused the workflow to fail
public function getException(): ?Exception;
// get the debug execution log of the workflow
public function debug(): string;
public function debug(?OutputFormat $formatter = null);
// access the container which was used for the workflow
public function getContainer(): WorkflowContainer;
// get the last executed step
Expand All @@ -455,7 +456,7 @@ The **debug** method provides an execution log including all processed steps wit

Some example outputs for our example workflow may look like the following.

### Successful execution
#### Successful execution

```
Process log for workflow 'AddSongToPlaylist':
Expand All @@ -481,7 +482,7 @@ Summary:

Note the additional data added to the debug log for the **Process** stage and the **NotifySubscribers** step via the **attachStepInfo** method of the **WorkflowControl**.

### Failed workflow
#### Failed workflow

```
Process log for workflow 'AddSongToPlaylist':
Expand All @@ -495,7 +496,7 @@ Summary:

In this example the **CurrentUserIsAllowedToEditPlaylistValidator** step threw an exception with the message `playlist locked`.

### Workflow skipped
#### Workflow skipped

```
Process log for workflow 'AddSongToPlaylist':
Expand All @@ -513,6 +514,18 @@ Summary:
In this example the **AcceptOpenSuggestionForSong** step found a matching open suggestion and successfully accepted the suggestion.
Consequently, the further workflow execution is skipped.

### Custom-output-formatter

The output of the `debug` method can be controlled via an implementation of the `OutputFormat` interface.
By default a string representation of the execution will be returned (just like the example outputs).

Currently the following additional formatters are implemented:

| Formatter | Description |
| --------------- | ------------- |
| `StringLog` | The default formatter. Creates a string representation. <br />Example:<br />`$result->debug();` |
| `WorkflowGraph` | Creates a SVG file containing a graph which represents the workflow execution. The generated image will be stored in the provided target directory. Requires `dot` executable.<br />Example:<br />`$result->debug(new WorkflowGraph('/var/log/workflow/graph'));` |
| `GraphViz` | Returns a string containing [GraphViz](https://graphviz.org/) code for a graph representing the workflow execution. <br />Example:<br />`$result->debug(new GraphViz());`|

## Tests ##

Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<include>
<directory>src</directory>
</include>
<exclude>
<directory>src/State/ExecutionLog/OutputFormat</directory>
</exclude>
</coverage>
<testsuite name="PHPWorkflow">
<directory>tests</directory>
Expand Down
5 changes: 3 additions & 2 deletions src/Stage/Next/AllowNextExecuteWorkflow.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPWorkflow\Exception\WorkflowControl\SkipWorkflowException;
use PHPWorkflow\Exception\WorkflowException;
use PHPWorkflow\State\ExecutionLog\ExecutionLog;
use PHPWorkflow\State\ExecutionLog\Summary;
use PHPWorkflow\State\WorkflowContainer;
use PHPWorkflow\State\WorkflowResult;
use PHPWorkflow\State\WorkflowState;
Expand Down Expand Up @@ -38,12 +39,12 @@ public function executeWorkflow(

$workflowState->getExecutionLog()->stopExecution();
$workflowState->setStage(WorkflowState::STAGE_SUMMARY);
$workflowState->addExecutionLog('Workflow execution');
$workflowState->addExecutionLog(new Summary('Workflow execution'));
} catch (Exception $exception) {
$workflowState->getExecutionLog()->stopExecution();
$workflowState->setStage(WorkflowState::STAGE_SUMMARY);
$workflowState->addExecutionLog(
'Workflow execution',
new Summary('Workflow execution'),
$exception instanceof SkipWorkflowException ? ExecutionLog::STATE_SKIPPED : ExecutionLog::STATE_FAILED,
$exception->getMessage(),
);
Expand Down
18 changes: 18 additions & 0 deletions src/State/ExecutionLog/Describable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace PHPWorkflow\State\ExecutionLog;

/**
* Interface Describable
*
* @package PHPWorkflow\State\ExecutionLog
*/
interface Describable
{
/**
* Describe in a few words what this step does
*/
public function getDescription(): string;
}
38 changes: 14 additions & 24 deletions src/State/ExecutionLog/ExecutionLog.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PHPWorkflow\State\ExecutionLog;

use PHPWorkflow\State\ExecutionLog\OutputFormat\OutputFormat;
use PHPWorkflow\State\WorkflowState;
use PHPWorkflow\Step\WorkflowStep;

Expand All @@ -15,7 +16,7 @@ class ExecutionLog

/** @var Step[][] */
private array $stages = [];
/** @var string[] Collect additional debug info concerning the current step */
/** @var StepInfo[] Collect additional debug info concerning the current step */
private array $stepInfo = [];
/** @var string[][] Collect all warnings which occurred during the workflow execution */
private array $warnings = [];
Expand All @@ -30,37 +31,25 @@ public function __construct(WorkflowState $workflowState)
$this->workflowState = $workflowState;
}

public function addStep(int $stage, string $step, string $state, ?string $reason): void {
$stage = $this->mapStage($stage);

public function addStep(int $stage, Describable $step, string $state, ?string $reason): void {
$this->stages[$stage][] = new Step($step, $state, $reason, $this->stepInfo, $this->warningsDuringStep);
$this->stepInfo = [];
$this->warningsDuringStep = 0;
}

public function __toString(): string
public function debug(OutputFormat $formatter)
{
$debug = "Process log for workflow '{$this->workflowState->getWorkflowName()}':\n";

foreach ($this->stages as $stage => $steps) {
$debug .= "$stage:\n";

foreach ($steps as $step) {
$debug .= ' - ' . $step . "\n";
}
}

return trim($debug);
return $formatter->format($this->workflowState->getWorkflowName(), $this->stages);
}

public function attachStepInfo(string $info): void
public function attachStepInfo(string $info, array $context = []): void
{
$this->stepInfo[] = $info;
$this->stepInfo[] = new StepInfo($info, $context);
}

public function addWarning(string $message, bool $workflowReportWarning = false): void
{
$this->warnings[$this->mapStage($this->workflowState->getStage())][] = $message;
$this->warnings[$this->workflowState->getStage()][] = $message;

if (!$workflowReportWarning) {
$this->warningsDuringStep++;
Expand All @@ -74,11 +63,11 @@ public function startExecution(): void

public function stopExecution(): void
{
$this->attachStepInfo("Execution time: " . number_format(1000 * (microtime(true) - $this->startAt), 5) . 'ms');
$this->attachStepInfo('Execution time: ' . number_format(1000 * (microtime(true) - $this->startAt), 5) . 'ms');

if ($this->warnings) {
$warnings = sprintf(
"Got %s warning%s during the execution:",
'Got %s warning%s during the execution:',
$amount = count($this->warnings, COUNT_RECURSIVE) - count($this->warnings),
$amount > 1 ? 's' : '',
);
Expand All @@ -87,7 +76,8 @@ public function stopExecution(): void
$warnings .= implode(
'',
array_map(
fn (string $warning): string => sprintf("\n %s: %s", $stage, $warning),
fn (string $warning): string =>
sprintf(PHP_EOL . ' %s: %s', self::mapStage($stage), $warning),
$stageWarnings,
),
);
Expand All @@ -97,7 +87,7 @@ public function stopExecution(): void
}
}

private function mapStage(int $stage): string
public static function mapStage(int $stage): string
{
switch ($stage) {
case WorkflowState::STAGE_PREPARE: return 'Prepare';
Expand All @@ -107,7 +97,7 @@ private function mapStage(int $stage): string
case WorkflowState::STAGE_ON_ERROR: return 'On Error';
case WorkflowState::STAGE_ON_SUCCESS: return 'On Success';
case WorkflowState::STAGE_AFTER: return 'After';
case WorkflowState::STAGE_SUMMARY: return "\nSummary";
case WorkflowState::STAGE_SUMMARY: return 'Summary';
}
}

Expand Down
141 changes: 141 additions & 0 deletions src/State/ExecutionLog/OutputFormat/GraphViz.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace PHPWorkflow\State\ExecutionLog\OutputFormat;

use PHPWorkflow\State\ExecutionLog\ExecutionLog;
use PHPWorkflow\State\ExecutionLog\Step;
use PHPWorkflow\State\ExecutionLog\StepInfo;
use PHPWorkflow\State\WorkflowResult;

class GraphViz implements OutputFormat
{
private static int $stepIndex = 0;
private static int $clusterIndex = 0;

private static int $loopIndex = 0;
private static array $loopInitialElement = [];
private static array $loopLinks = [];

public function format(string $workflowName, array $steps): string
{
$dotScript = "digraph \"$workflowName\" {\n";

$dotScript .= $this->renderWorkflowGraph($workflowName, $steps);

for ($i = 0; $i < self::$stepIndex - 1; $i++) {
if (isset(self::$loopLinks[$i + 1])) {
continue;
}

$dotScript .= sprintf(" %s -> %s\n", $i, $i + 1);
}

foreach (self::$loopLinks as $loopElement => $loopRoot) {
$dotScript .= sprintf(" %s -> %s\n", $loopRoot, $loopElement);
}

$dotScript .= '}';

return $dotScript;
}

private function renderWorkflowGraph(string $workflowName, array $steps): string
{
$dotScript = sprintf(" %s [label=\"$workflowName\"]\n", self::$stepIndex++);
foreach ($steps as $stage => $stageSteps) {
$dotScript .= sprintf(
" subgraph cluster_%s {\n label = \"%s\"\n",
self::$clusterIndex++,
ExecutionLog::mapStage($stage)
);

/** @var Step $step */
foreach ($stageSteps as $step) {
foreach ($step->getStepInfo() as $info) {
switch ($info->getInfo()) {
case StepInfo::LOOP_START:
$dotScript .= sprintf(
" subgraph cluster_loop_%s {\n label = \"Loop\"\n",
self::$clusterIndex++
);

self::$loopInitialElement[++self::$loopIndex] = self::$stepIndex;

continue 2;
case StepInfo::LOOP_ITERATION:
self::$loopLinks[self::$stepIndex + 1] = self::$loopInitialElement[self::$loopIndex];

continue 2;
case StepInfo::LOOP_END:
$dotScript .= "\n}\n";
array_pop(self::$loopLinks);
self::$loopIndex--;

continue 2;
case StepInfo::NESTED_WORKFLOW:
/** @var WorkflowResult $nestedWorkflowResult */
$nestedWorkflowResult = $info->getContext()['result'];
$nestedWorkflowGraph = $nestedWorkflowResult->debug($this);

$lines = explode("\n", $nestedWorkflowGraph);
array_shift($lines);
array_pop($lines);

$dotScript .=
sprintf(
" subgraph cluster_%s {\n label = \"Nested workflow\"\n",
self::$clusterIndex++,
)
. preg_replace('/\d+ -> \d+\s*/m', '', join("\n", $lines))
. "\n}\n";

// TODO: additional infos. Currently skipped
continue 3;
}
}

$dotScript .= sprintf(
' %s [label=%s shape="box" color="%s"]' . "\n",
self::$stepIndex++,
"<{$step->getDescription()} ({$step->getState()})"
. ($step->getReason() ? "<BR/><FONT POINT-SIZE=\"10\">{$step->getReason()}</FONT>" : '')
. join('', array_map(
fn (StepInfo $info): string => "<BR/><FONT POINT-SIZE=\"10\">{$info->getInfo()}</FONT>",
array_filter(
$step->getStepInfo(),
fn (StepInfo $info): bool => !in_array(
$info->getInfo(),
[
StepInfo::LOOP_START,
StepInfo::LOOP_ITERATION,
StepInfo::LOOP_END,
StepInfo::NESTED_WORKFLOW,
],
)
),
))
. ">",
$this->mapColor($step),
);
}
$dotScript .= " }\n";
}

return $dotScript;
}

private function mapColor(Step $step): string
{
if ($step->getState() === ExecutionLog::STATE_SUCCESS && $step->getWarnings()) {
return 'yellow';
}

return [
ExecutionLog::STATE_SUCCESS => 'green',
ExecutionLog::STATE_SKIPPED => 'grey',
ExecutionLog::STATE_FAILED => 'red',
][$step->getState()];
}
}
18 changes: 18 additions & 0 deletions src/State/ExecutionLog/OutputFormat/OutputFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace PHPWorkflow\State\ExecutionLog\OutputFormat;

use PHPWorkflow\State\ExecutionLog\Step;

interface OutputFormat
{
/**
* @param string $workflowName
* @param Step[][] $steps Contains a list of the executed steps, grouped by the executed stages
*
* @return mixed
*/
public function format(string $workflowName, array $steps);
}
Loading

0 comments on commit f21bcef

Please sign in to comment.