Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI log command #24

Merged
merged 1 commit into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading