feat(api): add endpoint to retrieve database and service logs (#6293)
This commit is contained in:
commit
15684fdbfe
8 changed files with 673 additions and 11 deletions
|
|
@ -2017,6 +2017,13 @@ public function application_by_uuid(Request $request)
|
||||||
default: 100,
|
default: 100,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'show_timestamps',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Show timestamps in the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(type: 'boolean', default: false),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
responses: [
|
responses: [
|
||||||
new OA\Response(
|
new OA\Response(
|
||||||
|
|
@ -2080,8 +2087,9 @@ public function logs_by_uuid(Request $request)
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$lines = $request->query->get('lines', 100) ?: 100;
|
$lines = normalizeLogLines($request->query('lines'));
|
||||||
$logs = getContainerLogs($application->destination->server, $container['ID'], $lines);
|
$showTimestamps = parseLogTimestampFlag($request->query('show_timestamps'));
|
||||||
|
$logs = getContainerLogs($application->destination->server, $container['ID'], $lines, $showTimestamps);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'logs' => $logs,
|
'logs' => $logs,
|
||||||
|
|
|
||||||
|
|
@ -2247,6 +2247,116 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
return response()->json(['message' => 'Invalid database type requested.'], 400);
|
return response()->json(['message' => 'Invalid database type requested.'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
summary: 'Get database logs.',
|
||||||
|
description: 'Get database logs by UUID.',
|
||||||
|
path: '/databases/{uuid}/logs',
|
||||||
|
operationId: 'get-database-logs-by-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Databases'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the database.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'lines',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Number of lines to show from the end of the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'integer',
|
||||||
|
format: 'int32',
|
||||||
|
default: 100,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'show_timestamps',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Show timestamps in the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(type: 'boolean', default: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Get database logs by UUID.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
'logs' => ['type' => 'string'],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 401,
|
||||||
|
ref: '#/components/responses/401',
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 400,
|
||||||
|
ref: '#/components/responses/400',
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 404,
|
||||||
|
ref: '#/components/responses/404',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function logs_by_uuid(Request $request)
|
||||||
|
{
|
||||||
|
$teamId = getTeamIdFromToken();
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
return invalidTokenResponse();
|
||||||
|
}
|
||||||
|
$uuid = $request->route('uuid');
|
||||||
|
if (! $uuid) {
|
||||||
|
return response()->json(['message' => 'UUID is required.'], 400);
|
||||||
|
}
|
||||||
|
$database = queryDatabaseByUuidWithinTeam($uuid, $teamId);
|
||||||
|
if (! $database) {
|
||||||
|
return response()->json(['message' => 'Database not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$containers = getCurrentDatabaseContainerStatus($database->destination->server, $database->id);
|
||||||
|
|
||||||
|
if ($containers->count() == 0) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Database is not running.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$container = $containers->first();
|
||||||
|
|
||||||
|
$status = getContainerStatus($database->destination->server, $container['Names']);
|
||||||
|
if ($status !== 'running') {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Database is not running.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = normalizeLogLines($request->query('lines'));
|
||||||
|
$showTimestamps = parseLogTimestampFlag($request->query('show_timestamps'));
|
||||||
|
$logs = getContainerLogs($database->destination->server, $container['ID'], $lines, $showTimestamps);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'logs' => $logs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
#[OA\Delete(
|
#[OA\Delete(
|
||||||
summary: 'Delete',
|
summary: 'Delete',
|
||||||
description: 'Delete database by UUID.',
|
description: 'Delete database by UUID.',
|
||||||
|
|
|
||||||
|
|
@ -731,6 +731,125 @@ public function service_by_uuid(Request $request)
|
||||||
return response()->json($this->removeSensitiveData($service));
|
return response()->json($this->removeSensitiveData($service));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
summary: 'Get service logs.',
|
||||||
|
description: 'Get logs for a specific service sub-resource by service UUID. The `sub_service_name` query parameter must match the `name` field of one of the service applications or databases returned by `GET /services/{uuid}`.',
|
||||||
|
path: '/services/{uuid}/logs',
|
||||||
|
operationId: 'get-service-logs-by-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Services'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the service.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'sub_service_name',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Sub-service name from `GET /services/{uuid}` under `applications[].name` or `databases[].name`. Do not use `human_name` or the Docker container name with the service UUID suffix.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', example: 'appwrite-console'),
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'lines',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Number of lines to show from the end of the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'integer',
|
||||||
|
format: 'int32',
|
||||||
|
default: 100,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'show_timestamps',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Show timestamps in the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(type: 'boolean', default: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Get service logs by UUID.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
'logs' => ['type' => 'string'],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 401,
|
||||||
|
ref: '#/components/responses/401',
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 400,
|
||||||
|
ref: '#/components/responses/400',
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 404,
|
||||||
|
ref: '#/components/responses/404',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function logs_by_uuid(Request $request)
|
||||||
|
{
|
||||||
|
$teamId = getTeamIdFromToken();
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
return invalidTokenResponse();
|
||||||
|
}
|
||||||
|
$uuid = $request->route('uuid');
|
||||||
|
if (! $uuid) {
|
||||||
|
return response()->json(['message' => 'UUID is required.'], 400);
|
||||||
|
}
|
||||||
|
$subServiceName = $request->query->get('sub_service_name');
|
||||||
|
if (! $subServiceName) {
|
||||||
|
return response()->json(['message' => 'Sub service name is required.'], 400);
|
||||||
|
}
|
||||||
|
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||||
|
if (! $service) {
|
||||||
|
return response()->json(['message' => 'Service not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = "{$subServiceName}-{$service->uuid}";
|
||||||
|
$containers = getCurrentServiceSubContainerStatus($service->destination->server, $service->id, $name);
|
||||||
|
$container = $containers->first();
|
||||||
|
|
||||||
|
if (! $container) {
|
||||||
|
return response()->json(['message' => 'Container not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = getContainerStatus($service->destination->server, $container['Names']);
|
||||||
|
if ($status !== 'running') {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Container is not running.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = normalizeLogLines($request->query('lines'));
|
||||||
|
$showTimestamps = parseLogTimestampFlag($request->query('show_timestamps'));
|
||||||
|
$logs = getContainerLogs($service->destination->server, $container['ID'], $lines, $showTimestamps);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'logs' => $logs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
#[OA\Delete(
|
#[OA\Delete(
|
||||||
summary: 'Delete',
|
summary: 'Delete',
|
||||||
description: 'Delete service by UUID.',
|
description: 'Delete service by UUID.',
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,36 @@ function getCurrentServiceContainerStatus(Server $server, int $id): Collection
|
||||||
return $containers;
|
return $containers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentDatabaseContainerStatus(Server $server, int $id): Collection
|
||||||
|
{
|
||||||
|
$containers = collect([]);
|
||||||
|
if (! $server->isSwarm()) {
|
||||||
|
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.databaseId={$id}' --format '{{json .}}' "], $server);
|
||||||
|
$containers = format_docker_command_output_to_json($containers);
|
||||||
|
|
||||||
|
return $containers->filter();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $containers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentServiceSubContainerStatus(Server $server, int $id, string $name): Collection
|
||||||
|
{
|
||||||
|
return filterServiceSubContainersByName(getCurrentServiceContainerStatus($server, $id), $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterServiceSubContainersByName(Collection $containers, string $name): Collection
|
||||||
|
{
|
||||||
|
return $containers->filter(function ($container) use ($name) {
|
||||||
|
$labels = data_get($container, 'Labels', []);
|
||||||
|
if (is_string($labels)) {
|
||||||
|
$labels = format_docker_labels_to_json($labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($labels)->get('coolify.name') === $name;
|
||||||
|
})->values();
|
||||||
|
}
|
||||||
|
|
||||||
function format_docker_command_output_to_json($rawOutput): Collection
|
function format_docker_command_output_to_json($rawOutput): Collection
|
||||||
{
|
{
|
||||||
$outputLines = explode(PHP_EOL, $rawOutput);
|
$outputLines = explode(PHP_EOL, $rawOutput);
|
||||||
|
|
@ -1247,18 +1277,38 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContainerLogs(Server $server, string $container_id, int $lines = 100): string
|
function normalizeLogLines(mixed $lines, int $default = 100, int $max = 10000): int
|
||||||
{
|
{
|
||||||
if ($server->isSwarm()) {
|
$lines = filter_var($lines, FILTER_VALIDATE_INT);
|
||||||
$output = instant_remote_process([
|
if ($lines === false || $lines <= 0) {
|
||||||
"docker service logs -n {$lines} {$container_id} 2>&1",
|
return $default;
|
||||||
], $server);
|
|
||||||
} else {
|
|
||||||
$output = instant_remote_process([
|
|
||||||
"docker logs -n {$lines} {$container_id} 2>&1",
|
|
||||||
], $server);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return min($lines, $max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLogTimestampFlag(mixed $showTimestamps): bool
|
||||||
|
{
|
||||||
|
return filter_var($showTimestamps, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildContainerLogsCommand(Server $server, string $container_id, int $lines = 100, bool $showTimestamps = false): string
|
||||||
|
{
|
||||||
|
$command = "docker logs -n {$lines}";
|
||||||
|
if ($server->isSwarm()) {
|
||||||
|
$command = "docker service logs -n {$lines}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($showTimestamps) {
|
||||||
|
$command .= ' --timestamps';
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{$command} ".escapeshellarg($container_id).' 2>&1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContainerLogs(Server $server, string $container_id, int $lines = 100, bool $showTimestamps = false): string
|
||||||
|
{
|
||||||
|
$output = instant_remote_process([buildContainerLogsCommand($server, $container_id, $lines, $showTimestamps)], $server);
|
||||||
$output = removeAnsiColors($output);
|
$output = removeAnsiColors($output);
|
||||||
|
|
||||||
return $output;
|
return $output;
|
||||||
|
|
|
||||||
168
openapi.json
168
openapi.json
|
|
@ -2675,6 +2675,16 @@
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"default": 100
|
"default": 100
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "show_timestamps",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Show timestamps in the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|
@ -5952,6 +5962,80 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"\/databases\/{uuid}\/logs": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Databases"
|
||||||
|
],
|
||||||
|
"summary": "Get database logs.",
|
||||||
|
"description": "Get database logs by UUID.",
|
||||||
|
"operationId": "get-database-logs-by-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the database.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lines",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Number of lines to show from the end of the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"default": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "show_timestamps",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Show timestamps in the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Get database logs by UUID.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"logs": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}\/executions\/{execution_uuid}": {
|
"\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}\/executions\/{execution_uuid}": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -11293,6 +11377,90 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"\/services\/{uuid}\/logs": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Services"
|
||||||
|
],
|
||||||
|
"summary": "Get service logs.",
|
||||||
|
"description": "Get logs for a specific service sub-resource by service UUID. The `sub_service_name` query parameter must match the `name` field of one of the service applications or databases returned by `GET \/services\/{uuid}`.",
|
||||||
|
"operationId": "get-service-logs-by-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the service.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sub_service_name",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Sub-service name from `GET \/services\/{uuid}` under `applications[].name` or `databases[].name`. Do not use `human_name` or the Docker container name with the service UUID suffix.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "appwrite-console"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lines",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Number of lines to show from the end of the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"default": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "show_timestamps",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Show timestamps in the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Get service logs by UUID.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"logs": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"\/services\/{uuid}\/envs": {
|
"\/services\/{uuid}\/envs": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
||||||
118
openapi.yaml
118
openapi.yaml
|
|
@ -1716,6 +1716,14 @@ paths:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
default: 100
|
default: 100
|
||||||
|
-
|
||||||
|
name: show_timestamps
|
||||||
|
in: query
|
||||||
|
description: 'Show timestamps in the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: 'Get application logs by UUID.'
|
description: 'Get application logs by UUID.'
|
||||||
|
|
@ -3908,6 +3916,57 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
'/databases/{uuid}/logs':
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Databases
|
||||||
|
summary: 'Get database logs.'
|
||||||
|
description: 'Get database logs by UUID.'
|
||||||
|
operationId: get-database-logs-by-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the database.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
-
|
||||||
|
name: lines
|
||||||
|
in: query
|
||||||
|
description: 'Number of lines to show from the end of the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
default: 100
|
||||||
|
-
|
||||||
|
name: show_timestamps
|
||||||
|
in: query
|
||||||
|
description: 'Show timestamps in the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Get database logs by UUID.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
logs: { type: string }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
'/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}':
|
'/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}':
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -7150,6 +7209,65 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
'/services/{uuid}/logs':
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Services
|
||||||
|
summary: 'Get service logs.'
|
||||||
|
description: 'Get logs for a specific service sub-resource by service UUID. The `sub_service_name` query parameter must match the `name` field of one of the service applications or databases returned by `GET /services/{uuid}`.'
|
||||||
|
operationId: get-service-logs-by-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the service.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
-
|
||||||
|
name: sub_service_name
|
||||||
|
in: query
|
||||||
|
description: 'Sub-service name from `GET /services/{uuid}` under `applications[].name` or `databases[].name`. Do not use `human_name` or the Docker container name with the service UUID suffix.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: appwrite-console
|
||||||
|
-
|
||||||
|
name: lines
|
||||||
|
in: query
|
||||||
|
description: 'Number of lines to show from the end of the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
default: 100
|
||||||
|
-
|
||||||
|
name: show_timestamps
|
||||||
|
in: query
|
||||||
|
description: 'Show timestamps in the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Get service logs by UUID.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
logs: { type: string }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
'/services/{uuid}/envs':
|
'/services/{uuid}/envs':
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@
|
||||||
Route::post('/databases/{uuid}/backups', [DatabasesController::class, 'create_backup'])->middleware(['api.ability:write']);
|
Route::post('/databases/{uuid}/backups', [DatabasesController::class, 'create_backup'])->middleware(['api.ability:write']);
|
||||||
Route::patch('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'update_backup'])->middleware(['api.ability:write']);
|
Route::patch('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'update_backup'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
|
||||||
|
Route::get('/databases/{uuid}/logs', [DatabasesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
|
||||||
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', [DatabasesController::class, 'delete_execution_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', [DatabasesController::class, 'delete_execution_by_uuid'])->middleware(['api.ability:write']);
|
||||||
|
|
||||||
|
|
@ -195,6 +196,7 @@
|
||||||
Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']);
|
Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']);
|
||||||
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
|
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
||||||
|
Route::get('/services/{uuid}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
||||||
|
|
|
||||||
87
tests/Unit/Api/LogEndpointHelpersTest.php
Normal file
87
tests/Unit/Api/LogEndpointHelpersTest.php
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
|
||||||
|
it('normalizes requested log line counts', function () {
|
||||||
|
if (! function_exists('normalizeLogLines')) {
|
||||||
|
expect(function_exists('normalizeLogLines'))->toBeTrue();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(normalizeLogLines(null))->toBe(100)
|
||||||
|
->and(normalizeLogLines(''))->toBe(100)
|
||||||
|
->and(normalizeLogLines('abc'))->toBe(100)
|
||||||
|
->and(normalizeLogLines('0'))->toBe(100)
|
||||||
|
->and(normalizeLogLines('-5'))->toBe(100)
|
||||||
|
->and(normalizeLogLines('50'))->toBe(50)
|
||||||
|
->and(normalizeLogLines('50000'))->toBe(10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses show_timestamps query values as booleans', function () {
|
||||||
|
if (! function_exists('parseLogTimestampFlag')) {
|
||||||
|
expect(function_exists('parseLogTimestampFlag'))->toBeTrue();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(parseLogTimestampFlag('true'))->toBeTrue()
|
||||||
|
->and(parseLogTimestampFlag('1'))->toBeTrue()
|
||||||
|
->and(parseLogTimestampFlag(true))->toBeTrue()
|
||||||
|
->and(parseLogTimestampFlag('false'))->toBeFalse()
|
||||||
|
->and(parseLogTimestampFlag('0'))->toBeFalse()
|
||||||
|
->and(parseLogTimestampFlag(false))->toBeFalse()
|
||||||
|
->and(parseLogTimestampFlag('not-a-bool'))->toBeFalse()
|
||||||
|
->and(parseLogTimestampFlag(null))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds docker log commands with options before an escaped container id', function () {
|
||||||
|
if (! function_exists('buildContainerLogsCommand')) {
|
||||||
|
expect(function_exists('buildContainerLogsCommand'))->toBeTrue();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$server = new Server;
|
||||||
|
$server->settings = ['is_swarm_manager' => false];
|
||||||
|
|
||||||
|
expect(buildContainerLogsCommand($server, 'container-1', 25, true))
|
||||||
|
->toBe("docker logs -n 25 --timestamps 'container-1' 2>&1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds swarm service log commands with options before an escaped service id', function () {
|
||||||
|
if (! function_exists('buildContainerLogsCommand')) {
|
||||||
|
expect(function_exists('buildContainerLogsCommand'))->toBeTrue();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$server = new Server;
|
||||||
|
$server->settings = ['is_swarm_manager' => true];
|
||||||
|
|
||||||
|
expect(buildContainerLogsCommand($server, "service'name", 25, true))
|
||||||
|
->toBe("docker service logs -n 25 --timestamps 'service'\\''name' 2>&1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters service sub containers in PHP instead of using user input in shell filters', function () {
|
||||||
|
if (! function_exists('filterServiceSubContainersByName')) {
|
||||||
|
expect(function_exists('filterServiceSubContainersByName'))->toBeTrue();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$containers = collect([
|
||||||
|
['ID' => 'first', 'Labels' => 'coolify.serviceId=10,coolify.name=app-service-uuid,coolify.type=service'],
|
||||||
|
['ID' => 'second', 'Labels' => 'coolify.serviceId=10,coolify.name=db-service-uuid,coolify.type=service'],
|
||||||
|
['ID' => 'third', 'Labels' => ['coolify.name' => 'app-service-uuid']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filterServiceSubContainersByName($containers, 'app-service-uuid')->pluck('ID')->all())
|
||||||
|
->toBe(['first', 'third']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not interpolate the requested service name into the docker ps shell command', function () {
|
||||||
|
$source = file_get_contents(__DIR__.'/../../../bootstrap/helpers/docker.php');
|
||||||
|
|
||||||
|
expect($source)->not->toContain('coolify.name={$name}');
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue