Skip to content

Commit

Permalink
Add CLI log command
Browse files Browse the repository at this point in the history
  • Loading branch information
apfelbox committed Jun 19, 2024
1 parent 8d8c094 commit ae2f45a
Show file tree
Hide file tree
Showing 3 changed files with 306 additions and 1 deletion.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"require": {
"php": ">= 8.3",
"21torr/bundle-helpers": "^2.1",
"21torr/cli": "^1.0",
"21torr/cli": "^1.2.3",
"doctrine/orm": "^2.19",
"dragonmantank/cron-expression": "^3.3",
"symfony/clock": "^7.1",
Expand Down
275 changes: 275 additions & 0 deletions src/Command/TaskLogCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
<?php declare(strict_types=1);

namespace Torr\TaskManager\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Torr\Cli\Console\Style\TorrStyle;
use Torr\TaskManager\Model\TaskLogModel;

#[AsCommand("task-manager:log")]
final class TaskLogCommand extends Command
{
/**
*/
public function __construct (
private readonly TaskLogModel $model,
)
{
parent::__construct();
}

/**
*
*/
#[\Override]
protected function configure () : void
{
$this
->addArgument(
"task",
InputArgument::OPTIONAL,
"The ID of the task to show the details of",
)
->addOption(
"limit",
null,
InputOption::VALUE_REQUIRED,
"The maximum count of listed log entries",
"100",
);
}

/**
*
*/
#[\Override]
protected function execute (InputInterface $input, OutputInterface $output) : int
{
$io = new TorrStyle($input, $output);
$io->title("Task Manager: Log");

$taskId = $input->getArgument("task");

if (null !== $taskId)
{
$taskId = $this->validateInt($taskId);

if (null === $taskId)
{
$io->error("Invalid argument: taskId must be an integer and must be >0");

return self::FAILURE;
}

return $this->showTaskDetails($io, $taskId);
}

$limit = $this->validateInt($input->getOption("limit"));

if (null === $limit)
{
$io->error("Invalid option: limit must be an integer and must be >0");

return self::FAILURE;
}

$this->showList($io, $limit);

return self::SUCCESS;
}

/**
* @return positive-int|null
*/
private function validateInt (mixed $value) : ?int
{
return (ctype_digit($value) && ((int) $value) > 0)
? (int) $value
: null;
}

/**
*
*/
private function showTaskDetails (TorrStyle $io, int $taskId) : int
{
$task = $this->model->findById($taskId);

if (null === $task)
{
$io->error(sprintf("No task found with id '%d'", $taskId));

return self::FAILURE;
}

$status = "<fg=yellow>queued</>";

if ($task->isFinished())
{
$status = $task->isSuccess()
? "<fg=green>succeeded</>"
: "<fg=red>failed</>";
}

$handled = [];

if (null !== $task->getHandledBy())
{
$handled[] = sprintf(
"<fg=blue>%s</>",
$task->getHandledBy(),
);
}

if (null !== $task->getTransport())
{
$handled[] = sprintf(
"<fg=blue>%s</>",
$task->getTransport(),
);
}

$io->definitionList(
["Task ID" => $task->getId()],
["Task" => $task->getTaskLabel()],
["Status" => $status],
["Task Class" => $task->getTaskClass() ?? "<fg=gray>—</>"],
["Runs" => \count($task->getRuns())],
["Total Duration" => $this->formatDuration($task->getTotalDuration())],
["Handled by" => implode(" on ", $handled)],
["Registered" => $task->getTimeQueued()->format("c")],
);

$index = \count($task->getRuns());

foreach ($task->getRuns() as $run)
{
$status = "<fg=yellow>running</>";

if ($run->isFinished())
{
$status = $run->isSuccess()
? "<fg=green>succeeded</>"
: "<fg=red>failed</>";
}

$io->section(sprintf(
"Run %d (%s)",
$index,
$status,
));
$io->writeln(sprintf(
"Started: %s",
$run->getTimeStarted()->format("c"),
));

if ($run->isFinished())
{
$io->writeln(sprintf(
"Duration: %s",
$this->formatDuration((float) $run->getDuration()),
));
$io->writeln("Output:");
$io->newLine();
$io->writeln("------------------");
$io->writeln((string) $run->getOutput());
$io->writeln("------------------");
}

if ($index > 1)
{
$io->newLine(2);
}

--$index;
}

return self::SUCCESS;
}

/**
*
*/
private function showList (TorrStyle $io, int $limit) : void
{
$rows = [];

foreach ($this->model->getMostRecentEntries($limit) as $task)
{
$status = "<fg=yellow>queued</>";

if ($task->isFinished())
{
$status = $task->isSuccess()
? "<fg=green>succeeded</>"
: "<fg=red>failed</>";
}

$rows[] = [
$task->getId(),
sprintf(
"<fg=%s>%s</>",
null !== $task->getTaskLabel() ? "yellow" : "gray",
$task->getTaskLabel() ?? "",
),
$status,
$task->getTaskClass() ?? "<fg=gray>—</>",
\count($task->getRuns()),
$this->formatDuration($task->getTotalDuration()),
$task->getTimeQueued()->format("c"),
];
}
$rows[] = [
"ID",
42,
45.2,
null,
"Runs",
"Duration",
"Registered",
];

$io->table([
"ID",
"Task",
"Status",
"Class",
"Runs",
"Duration",
"Registered",
], $rows);
}

/**
*
*/
private function formatDuration (float $value) : string
{
$scales = [
[1e9, "s", 1e9],
[1e4, "ms", 1e6],
];

foreach ($scales as $scale)
{
[$minimumValue, $unit, $scaleBy] = $scale;

if ($value < $minimumValue)
{
continue;
}

return number_format(
$value / $scaleBy,
2,
) . $unit;
}

return $value . "ns";
}
}
30 changes: 30 additions & 0 deletions src/Model/TaskLogModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Psr\Clock\ClockInterface;
use Torr\TaskManager\Entity\TaskLog;
use Torr\TaskManager\Entity\TaskRun;
Expand All @@ -26,6 +27,14 @@ public function __construct (
$this->repository = $repository;
}

/**
*
*/
public function findById (int $id) : ?TaskLog
{
return $this->repository->find($id);
}

/**
* Gets or creates the log entry for the given task
*/
Expand All @@ -47,6 +56,26 @@ public function getLogForTask (Task $task) : TaskLog
return $log;
}

/**
* Returns the latest task log entries
*
* @return TaskLog[]
*/
public function getMostRecentEntries (int $limit = 100) : array
{
$query = $this->repository->createQueryBuilder("task")
->select("task, run")
->leftJoin("task.runs", "run")
->addOrderBy("task.timeQueued", "DESC")
->setMaxResults($limit)
->getQuery();

/** @var TaskLog[] */
return (new Paginator($query))
->getQuery()
->getResult();
}

/**
* Creates a new run for the given task (lok) and marks it as persisted.
*/
Expand All @@ -68,6 +97,7 @@ public function fetchOutdatedTasks (int $maxAgeInDays) : array

/** @var TaskLog[] $entries */
$entries = $this->repository->createQueryBuilder("task")
->select("task, run")
->leftJoin("task.runs", "run")
->where("task.timeQueued <= :oldestTimestamp")
->setParameter("oldestTimestamp", $oldestTimeQueued)
Expand Down

0 comments on commit ae2f45a

Please sign in to comment.