app/Support/RuntimeLogTargets.php
Rama main · réplica de lectura del repositorio original.
<?php
namespace App\Support;
use Illuminate\Support\Collection;
/**
* Groups running and stopped containers for the runtime logs page.
*
* A service is one stream. Every container of that service, on every server and
* from a rollout that left the previous container behind, is a source of that stream.
* Deployments are production, then preview pull requests from newest to oldest.
* Services are alphabetical.
*/
class RuntimeLogTargets
{
/**
* @param list<array{uuid: string, name: string, containers: list<array<string, mixed>>}> $servers
* @return array{
* deployments: list<array{value: string, label: string, dot: string, state?: string}>,
* services: list<array{value: string, label: string, dot: string, description?: string, state?: string}>,
* deployment: string,
* service: string,
* sources: list<array{server: string, container: string, label: string}>,
* streamKey: string,
* }
*/
public static function select(array $servers, string $deployment, string $service, bool $groupDeployments): array
{
$containers = self::containers($servers, $groupDeployments);
if ($containers === []) {
return self::emptySelection();
}
$deploymentIds = $groupDeployments ? self::sortedDeploymentIds($containers) : [];
$chosenDeployment = $groupDeployments
? self::pickDeployment($deploymentIds, self::normalizeDeployment($deployment))
: '0';
$visible = array_values(array_filter(
$containers,
fn (array $container): bool => $container['deployment'] === $chosenDeployment,
));
if ($visible === []) {
return self::emptySelection();
}
$groups = self::groupByService($visible);
$chosenKey = self::pickServiceKey($groups, $service);
$sources = self::sourcesFor($groups[$chosenKey]);
return [
'deployments' => $groupDeployments ? self::deploymentOptions($deploymentIds, $containers) : [],
'services' => self::serviceOptions($groups),
'deployment' => $chosenDeployment,
'service' => count($groups) > 1 ? $chosenKey : '',
'sources' => $sources,
'streamKey' => self::streamKey($sources),
];
}
/**
* @return array{
* deployments: list<empty>,
* services: list<empty>,
* deployment: string,
* service: string,
* sources: list<empty>,
* streamKey: string,
* }
*/
private static function emptySelection(): array
{
return [
'deployments' => [],
'services' => [],
'deployment' => '0',
'service' => '',
'sources' => [],
'streamKey' => '',
];
}
/**
* @param list<array{uuid: string, name: string, containers: list<array<string, mixed>>}> $servers
* @return list<array{name: string, key: string, state: string, deployment: string, server: string, serverName: string, serverIndex: int}>
*/
private static function containers(array $servers, bool $groupDeployments): array
{
$containers = [];
foreach (array_values($servers) as $index => $server) {
foreach ($server['containers'] ?? [] as $container) {
if (! is_array($container)) {
continue;
}
$name = (string) data_get($container, 'Names', '');
if (! ValidationPatterns::isValidContainerName($name)) {
continue;
}
$labels = self::labels($container);
$state = strtolower(trim((string) data_get($container, 'State', 'running')));
$containers[] = [
'name' => $name,
'key' => self::serviceKey($name, $labels),
'state' => $state !== '' ? $state : 'running',
'deployment' => self::deploymentId($labels, $groupDeployments),
'server' => (string) ($server['uuid'] ?? ''),
'serverName' => (string) ($server['name'] ?? ''),
'serverIndex' => $index,
];
}
}
return $containers;
}
/**
* @param array<string, mixed> $container
*/
private static function labels(array $container): Collection
{
$raw = $container['Labels'] ?? null;
if (is_array($raw)) {
return collect($raw);
}
if (! is_string($raw) || trim($raw) === '') {
return collect();
}
$parsed = format_docker_labels_to_json($raw);
return $parsed instanceof Collection ? $parsed : collect();
}
private static function serviceKey(string $containerName, Collection $labels): string
{
$compose = trim((string) $labels->get('com.docker.compose.service', ''));
if ($compose !== '') {
return $compose;
}
$name = trim((string) $labels->get('coolify.name', ''));
if ($name !== '') {
return $name;
}
$headline = str($containerName)->beforeLast('-')->headline()->toString();
return $headline !== '' ? $headline : $containerName;
}
private static function deploymentId(Collection $labels, bool $groupDeployments): string
{
if (! $groupDeployments) {
return '0';
}
return self::normalizeDeployment((string) $labels->get('coolify.pullRequestId', ''));
}
private static function normalizeDeployment(string $deployment): string
{
if ($deployment === '' || ! ctype_digit($deployment) || (int) $deployment === 0) {
return '0';
}
return (string) (int) $deployment;
}
/**
* @param list<array{deployment: string}> $containers
* @return list<string>
*/
private static function sortedDeploymentIds(array $containers): array
{
$ids = array_values(array_unique(array_column($containers, 'deployment')));
usort($ids, function (string $left, string $right): int {
if ($left === '0') {
return -1;
}
if ($right === '0') {
return 1;
}
return (int) $right <=> (int) $left;
});
return $ids;
}
/**
* @param list<string> $deploymentIds
*/
private static function pickDeployment(array $deploymentIds, string $preferred): string
{
if (in_array($preferred, $deploymentIds, true)) {
return $preferred;
}
if (in_array('0', $deploymentIds, true)) {
return '0';
}
return $deploymentIds[0] ?? '0';
}
/**
* Deployments show the same status dot and caption as services, for all their containers.
*
* @param list<string> $ids
* @param list<array{deployment: string, state: string}> $containers
* @return list<array{value: string, label: string, dot: string, state?: string}>
*/
private static function deploymentOptions(array $ids, array $containers): array
{
return array_map(function (string $id) use ($containers): array {
$states = array_column(array_filter(
$containers,
fn (array $container): bool => $container['deployment'] === $id,
), 'state');
return self::statusOption($id, $id === '0' ? 'Production' : 'PR #'.$id, $states);
}, $ids);
}
/**
* @param list<string> $states
* @return array{value: string, label: string, dot: string, state?: string}
*/
private static function statusOption(string $value, string $label, array $states): array
{
$option = ['value' => $value, 'label' => $label, 'dot' => self::serviceDot($states)];
$state = self::serviceStateLabel($states);
if ($state !== null) {
$option['state'] = $state;
}
return $option;
}
/**
* @param list<array{key: string}> $containers
* @return array<string, list<array{name: string, key: string, state: string, deployment: string, server: string, serverName: string, serverIndex: int}>>
*/
private static function groupByService(array $containers): array
{
$groups = [];
foreach ($containers as $container) {
$groups[$container['key']][] = $container;
}
uksort($groups, fn (string $left, string $right): int => strcasecmp($left, $right));
return $groups;
}
/**
* @param array<string, list<array{state: string}>> $groups
*/
private static function pickServiceKey(array $groups, string $service): string
{
if ($service !== '' && isset($groups[$service])) {
return $service;
}
foreach ($groups as $key => $containers) {
foreach ($containers as $container) {
if ($container['state'] === 'running') {
return $key;
}
}
}
return (string) array_key_first($groups);
}
/**
* @param array<string, list<array{state: string}>> $groups
* @return list<array{value: string, label: string, dot: string, description?: string, state?: string}>
*/
private static function serviceOptions(array $groups): array
{
$options = [];
foreach ($groups as $key => $containers) {
$option = self::statusOption($key, $key, array_column($containers, 'state'));
if (count($containers) > 1) {
$option['description'] = count($containers).' containers';
}
$options[] = $option;
}
return $options;
}
/**
* @param list<string> $states
*/
private static function serviceDot(array $states): string
{
$allRunning = true;
$anyLive = false;
foreach ($states as $state) {
if ($state !== 'running') {
$allRunning = false;
}
if (in_array($state, ['running', 'starting', 'restarting', 'degraded'], true)) {
$anyLive = true;
}
}
if ($allRunning) {
return 'success';
}
if (! $anyLive) {
return 'error';
}
return 'warning';
}
/**
* A running service needs no caption. Anything else is named: Stopped when
* nothing is running, the transitional state when that is the only problem,
* and Degraded when the containers disagree.
*
* @param list<string> $states
*/
private static function serviceStateLabel(array $states): ?string
{
$problems = [];
$anyLive = false;
foreach ($states as $state) {
$state = strtolower($state);
if (in_array($state, ['running', 'starting', 'restarting', 'degraded'], true)) {
$anyLive = true;
}
if ($state !== 'running') {
$problems[$state] = true;
}
}
if ($problems === []) {
return null;
}
$problemStates = array_keys($problems);
if ($problemStates === ['starting']) {
return 'Starting';
}
if ($problemStates === ['restarting']) {
return 'Restarting';
}
if ($problemStates === ['degraded']) {
return 'Degraded';
}
if (! $anyLive) {
return 'Stopped';
}
return 'Degraded';
}
/**
* @param list<array{name: string, server: string, serverName: string, serverIndex: int}> $containers
* @return list<array{server: string, container: string, label: string}>
*/
private static function sourcesFor(array $containers): array
{
usort($containers, function (array $left, array $right): int {
$byServer = $left['serverIndex'] <=> $right['serverIndex'];
return $byServer !== 0 ? $byServer : strcmp($left['name'], $right['name']);
});
$serverCounts = [];
foreach ($containers as $container) {
$serverCounts[$container['server']] = ($serverCounts[$container['server']] ?? 0) + 1;
}
$multipleServers = count($serverCounts) > 1;
$sources = [];
foreach ($containers as $container) {
$sources[] = [
'server' => $container['server'],
'container' => $container['name'],
'label' => self::sourceLabel($container, $multipleServers, $serverCounts[$container['server']], count($containers)),
];
}
return self::uniqueLabels($sources);
}
/**
* @param array{name: string, server: string, serverName: string} $container
*/
private static function sourceLabel(array $container, bool $multipleServers, int $onServer, int $total): string
{
if ($total < 2) {
return $container['name'];
}
$parts = [];
if ($multipleServers) {
$name = trim($container['serverName']);
$parts[] = $name !== '' ? $name : $container['server'];
}
if ($onServer > 1 || ! $multipleServers) {
$parts[] = $container['name'];
}
return implode(' ', $parts);
}
/**
* @param list<array{server: string, container: string, label: string}> $sources
* @return list<array{server: string, container: string, label: string}>
*/
private static function uniqueLabels(array $sources): array
{
$counts = array_count_values(array_column($sources, 'label'));
foreach ($sources as $index => $source) {
if ($counts[$source['label']] < 2) {
continue;
}
if (! str_contains($source['label'], $source['container'])) {
$sources[$index]['label'] = trim($source['label'].' '.$source['container']);
}
}
$counts = array_count_values(array_column($sources, 'label'));
foreach ($sources as $index => $source) {
if (($counts[$source['label']] ?? 0) < 2) {
continue;
}
$sources[$index]['label'] = $source['label'].' '.substr($source['server'], 0, 8);
}
return $sources;
}
/**
* @param list<array{server: string, container: string}> $sources
*/
private static function streamKey(array $sources): string
{
return implode('|', array_map(
fn (array $source): string => $source['server'].':'.$source['container'],
$sources,
));
}
}