Added docker awslogs driver to log drains (#11972)

Co-authored-by: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
This commit is contained in:
Maxwell
2026-10-18 23:20:42 +02:00
committed by GitHub
co-authored by Andras Bacsai
parent 6731d83d9d
commit 0bc1288e81
28 changed files with 756 additions and 21 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ class StartClickhouse
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$container_name]['logging'] = generate_log_drain_configuration($this->database->destination->server);
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
+1 -1
View File
@@ -130,7 +130,7 @@ class StartDragonfly
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$container_name]['logging'] = generate_log_drain_configuration($this->database->destination->server);
}
if (count($this->database->ports_mappings_array) > 0) {
+1 -1
View File
@@ -132,7 +132,7 @@ class StartKeydb
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$container_name]['logging'] = generate_log_drain_configuration($this->database->destination->server);
}
if (count($this->database->ports_mappings_array) > 0) {
+1 -1
View File
@@ -123,7 +123,7 @@ class StartMariadb
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$container_name]['logging'] = generate_log_drain_configuration($this->database->destination->server);
}
if (count($this->database->ports_mappings_array) > 0) {
+1 -1
View File
@@ -130,7 +130,7 @@ class StartMongodb
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$container_name]['logging'] = generate_log_drain_configuration($this->database->destination->server);
}
if (count($this->database->ports_mappings_array) > 0) {
+1 -1
View File
@@ -125,7 +125,7 @@ class StartMysql
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$container_name]['logging'] = generate_log_drain_configuration($this->database->destination->server);
}
if (count($this->database->ports_mappings_array) > 0) {
+1 -1
View File
@@ -131,7 +131,7 @@ class StartPostgresql
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$container_name]['logging'] = generate_log_drain_configuration($this->database->destination->server);
}
if (count($this->database->ports_mappings_array) > 0) {
+1 -1
View File
@@ -133,7 +133,7 @@ class StartRedis
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$container_name]['logging'] = generate_log_drain_configuration($this->database->destination->server);
}
if (count($this->database->ports_mappings_array) > 0) {
+1 -1
View File
@@ -69,7 +69,7 @@ class StartSqlite
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$container_name]['logging'] = generate_log_drain_configuration($this->database->destination->server);
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
+9 -1
View File
@@ -22,9 +22,17 @@ class StartLogDrain
$type = 'axiom';
} elseif ($server->settings->is_logdrain_custom_enabled) {
$type = 'custom';
} elseif ($server->settings->is_logdrain_cloudwatch_enabled) {
$type = 'cloudwatch';
} else {
$type = 'none';
}
if ($type === 'cloudwatch') {
// Containers ship logs directly via Docker's awslogs driver; no Fluent Bit container is needed.
StopLogDrain::run($server);
return 'Amazon CloudWatch Logs uses the built-in Docker awslogs driver.';
}
if ($type !== 'none') {
StopLogDrain::run($server);
}
@@ -212,7 +220,7 @@ Files:
private function logDrainNetworkConnectCommands(Server $server): array
{
if (! $server->isLogDrainEnabled()) {
if (! $server->isFluentBitLogDrainEnabled()) {
return [];
}
+1 -1
View File
@@ -116,7 +116,7 @@ class StartService
return [];
}
if (! $service->destination?->server?->isLogDrainEnabled()) {
if (! $service->destination?->server?->isFluentBitLogDrainEnabled()) {
return [];
}
@@ -22,6 +22,10 @@ class ServerLogDrainsController extends Controller
'is_logdrain_custom_enabled',
'logdrain_custom_config',
'logdrain_custom_config_parser',
'is_logdrain_cloudwatch_enabled',
'logdrain_cloudwatch_region',
'logdrain_cloudwatch_group',
'logdrain_cloudwatch_stream_prefix',
];
private function findServerForTeam(int $teamId, string $uuid): ?Server
@@ -43,6 +47,10 @@ class ServerLogDrainsController extends Controller
'is_logdrain_axiom_enabled' => (bool) $settings->is_logdrain_axiom_enabled,
'logdrain_axiom_dataset_name' => $settings->logdrain_axiom_dataset_name,
'is_logdrain_custom_enabled' => (bool) $settings->is_logdrain_custom_enabled,
'is_logdrain_cloudwatch_enabled' => (bool) $settings->is_logdrain_cloudwatch_enabled,
'logdrain_cloudwatch_region' => $settings->logdrain_cloudwatch_region,
'logdrain_cloudwatch_group' => $settings->logdrain_cloudwatch_group,
'logdrain_cloudwatch_stream_prefix' => $settings->logdrain_cloudwatch_stream_prefix,
];
if ($this->canReadSensitive()) {
@@ -80,6 +88,10 @@ class ServerLogDrainsController extends Controller
new OA\Property(property: 'is_logdrain_custom_enabled', type: 'boolean'),
new OA\Property(property: 'logdrain_custom_config', type: 'string', description: 'Only present with read:sensitive.'),
new OA\Property(property: 'logdrain_custom_config_parser', type: 'string', description: 'Only present with read:sensitive.'),
new OA\Property(property: 'is_logdrain_cloudwatch_enabled', type: 'boolean'),
new OA\Property(property: 'logdrain_cloudwatch_region', type: 'string', nullable: true),
new OA\Property(property: 'logdrain_cloudwatch_group', type: 'string', nullable: true),
new OA\Property(property: 'logdrain_cloudwatch_stream_prefix', type: 'string', nullable: true, description: 'Prepended to the container name to form the log stream name.'),
],
type: 'object',
),
@@ -129,6 +141,10 @@ class ServerLogDrainsController extends Controller
new OA\Property(property: 'is_logdrain_custom_enabled', type: 'boolean'),
new OA\Property(property: 'logdrain_custom_config', type: 'string'),
new OA\Property(property: 'logdrain_custom_config_parser', type: 'string'),
new OA\Property(property: 'is_logdrain_cloudwatch_enabled', type: 'boolean'),
new OA\Property(property: 'logdrain_cloudwatch_region', type: 'string', nullable: true),
new OA\Property(property: 'logdrain_cloudwatch_group', type: 'string', nullable: true),
new OA\Property(property: 'logdrain_cloudwatch_stream_prefix', type: 'string', nullable: true, description: 'Prepended to the container name to form the log stream name.'),
],
type: 'object',
),
@@ -170,6 +186,10 @@ class ServerLogDrainsController extends Controller
'is_logdrain_custom_enabled' => 'boolean',
'logdrain_custom_config' => 'nullable|string',
'logdrain_custom_config_parser' => 'nullable|string',
'is_logdrain_cloudwatch_enabled' => 'boolean',
'logdrain_cloudwatch_region' => ['nullable', 'string', 'max:64', 'regex:/^[a-z]{2}(-[a-z]+)+-\d+$/'],
'logdrain_cloudwatch_group' => ['nullable', 'string', 'regex:/^[\.\-_\/#A-Za-z0-9]{1,512}$/'],
'logdrain_cloudwatch_stream_prefix' => ['nullable', 'string', 'max:256', 'regex:/^[A-Za-z0-9_\-\.\/#]*$/'],
]);
$extraFields = array_diff(array_keys($request->all()), self::ALLOWED_FIELDS);
@@ -226,6 +246,27 @@ class ServerLogDrainsController extends Controller
], 422);
}
if ($settings->is_logdrain_cloudwatch_enabled) {
if ($settings->is_logdrain_newrelic_enabled || $settings->is_logdrain_axiom_enabled || $settings->is_logdrain_custom_enabled || $settings->is_logdrain_highlight_enabled) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'is_logdrain_cloudwatch_enabled' => ['Amazon CloudWatch Logs cannot be enabled together with another log drain.'],
],
], 422);
}
$errors = [];
if (blank($settings->logdrain_cloudwatch_region)) {
$errors['logdrain_cloudwatch_region'] = ['The CloudWatch region is required when Amazon CloudWatch Logs is enabled.'];
}
if (blank($settings->logdrain_cloudwatch_group)) {
$errors['logdrain_cloudwatch_group'] = ['The CloudWatch log group is required when Amazon CloudWatch Logs is enabled.'];
}
if ($errors !== []) {
return response()->json(['message' => 'Validation failed.', 'errors' => $errors], 422);
}
}
$settings->save();
$server->refresh();
+1 -1
View File
@@ -4185,7 +4185,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}
if ($this->mainServer->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = generate_fluentd_configuration();
$docker_compose['services'][$this->container_name]['logging'] = generate_log_drain_configuration($this->mainServer);
}
if ($this->application->settings->is_gpu_enabled) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
+1 -1
View File
@@ -970,7 +970,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
private function checkLogDrainContainer()
{
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
if ($this->server->isFluentBitLogDrainEnabled() && $this->foundLogDrainContainer === false) {
StartLogDrain::dispatch($this->server);
}
}
+1 -1
View File
@@ -73,7 +73,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
CheckAndStartSentinelJob::dispatch($this->server);
}
if ($this->server->isLogDrainEnabled()) {
if ($this->server->isFluentBitLogDrainEnabled()) {
$this->checkLogDrainContainer();
}
+78
View File
@@ -6,6 +6,7 @@ use App\Actions\Server\StartLogDrain;
use App\Actions\Server\StopLogDrain;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -42,6 +43,18 @@ class LogDrains extends Component
#[Validate(['string', 'nullable'])]
public ?string $logDrainCustomConfigParser = null;
#[Validate(['boolean'])]
public bool $isLogDrainCloudwatchEnabled = false;
#[Validate(['string', 'nullable', 'max:64', 'regex:/^[a-z]{2}(-[a-z]+)+-\d+$/'])]
public ?string $logDrainCloudwatchRegion = null;
#[Validate(['string', 'nullable', 'regex:/^[\.\-_\/#A-Za-z0-9]{1,512}$/'])]
public ?string $logDrainCloudwatchGroup = null;
#[Validate(['string', 'nullable', 'max:256', 'regex:/^[A-Za-z0-9_\-\.\/#]*$/'])]
public ?string $logDrainCloudwatchStreamPrefix = null;
public function mount(string $server_uuid)
{
try {
@@ -99,6 +112,21 @@ class LogDrains extends Component
}
}
private function syncDataCloudwatch(bool $toModel = false): void
{
if ($toModel) {
$this->server->settings->is_logdrain_cloudwatch_enabled = $this->isLogDrainCloudwatchEnabled;
$this->server->settings->logdrain_cloudwatch_region = $this->logDrainCloudwatchRegion;
$this->server->settings->logdrain_cloudwatch_group = $this->logDrainCloudwatchGroup;
$this->server->settings->logdrain_cloudwatch_stream_prefix = $this->logDrainCloudwatchStreamPrefix;
} else {
$this->isLogDrainCloudwatchEnabled = $this->server->settings->is_logdrain_cloudwatch_enabled;
$this->logDrainCloudwatchRegion = $this->server->settings->logdrain_cloudwatch_region;
$this->logDrainCloudwatchGroup = $this->server->settings->logdrain_cloudwatch_group;
$this->logDrainCloudwatchStreamPrefix = $this->server->settings->logdrain_cloudwatch_stream_prefix;
}
}
private function syncData(bool $toModel = false, ?string $type = null): void
{
if ($toModel) {
@@ -109,10 +137,16 @@ class LogDrains extends Component
$this->syncDataAxiom($toModel);
} elseif ($type === 'custom') {
$this->syncDataCustom($toModel);
} elseif ($type === 'cloudwatch') {
$this->syncDataCloudwatch($toModel);
} else {
$this->syncDataNewRelic($toModel);
$this->syncDataAxiom($toModel);
$this->syncDataCustom($toModel);
$this->syncDataCloudwatch($toModel);
}
if ($this->enabledLogDrainCount() > 0) {
$this->server->settings->is_logdrain_highlight_enabled = false;
}
$this->auditLogDrain('updated');
$this->server->settings->save();
@@ -123,16 +157,27 @@ class LogDrains extends Component
$this->syncDataAxiom($toModel);
} elseif ($type === 'custom') {
$this->syncDataCustom($toModel);
} elseif ($type === 'cloudwatch') {
$this->syncDataCloudwatch($toModel);
} else {
$this->syncDataNewRelic($toModel);
$this->syncDataAxiom($toModel);
$this->syncDataCustom($toModel);
$this->syncDataCloudwatch($toModel);
}
}
}
public function customValidation()
{
if ($this->enabledLogDrainCount() > 1) {
$this->syncData();
throw ValidationException::withMessages([
'logDrain' => 'Only one log drain can be enabled at a time. Disable the current log drain first.',
]);
}
if ($this->isLogDrainNewRelicEnabled) {
try {
$this->validate([
@@ -164,6 +209,18 @@ class LogDrains extends Component
} catch (\Throwable $e) {
$this->isLogDrainCustomEnabled = false;
throw $e;
}
} elseif ($this->isLogDrainCloudwatchEnabled) {
try {
$this->validate([
'logDrainCloudwatchRegion' => ['required', 'max:64', 'regex:/^[a-z]{2}(-[a-z]+)+-\d+$/'],
'logDrainCloudwatchGroup' => ['required', 'regex:/^[\.\-_\/#A-Za-z0-9]{1,512}$/'],
'logDrainCloudwatchStreamPrefix' => ['string', 'nullable', 'max:256', 'regex:/^[A-Za-z0-9_\-\.\/#]*$/'],
]);
} catch (\Throwable $e) {
$this->isLogDrainCloudwatchEnabled = false;
throw $e;
}
}
@@ -192,6 +249,8 @@ class LogDrains extends Component
$previousNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled;
$previousAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled;
$previousCustomEnabled = $this->server->settings->is_logdrain_custom_enabled;
$previousCloudwatchEnabled = $this->server->settings->is_logdrain_cloudwatch_enabled;
$previousHighlightEnabled = $this->server->settings->is_logdrain_highlight_enabled;
try {
$this->authorize('update', $this->server);
@@ -206,6 +265,7 @@ class LogDrains extends Component
$this->isLogDrainNewRelicEnabled = $type === 'newrelic';
$this->isLogDrainAxiomEnabled = $type === 'axiom';
$this->isLogDrainCustomEnabled = $type === 'custom';
$this->isLogDrainCloudwatchEnabled = $type === 'cloudwatch';
}
$this->syncData(true);
@@ -224,6 +284,8 @@ class LogDrains extends Component
$this->server->settings->is_logdrain_newrelic_enabled = $previousNewRelicEnabled;
$this->server->settings->is_logdrain_axiom_enabled = $previousAxiomEnabled;
$this->server->settings->is_logdrain_custom_enabled = $previousCustomEnabled;
$this->server->settings->is_logdrain_cloudwatch_enabled = $previousCloudwatchEnabled;
$this->server->settings->is_logdrain_highlight_enabled = $previousHighlightEnabled;
$this->server->settings->save();
$this->syncData();
@@ -247,12 +309,23 @@ class LogDrains extends Component
return view('livewire.server.log-drains');
}
private function enabledLogDrainCount(): int
{
return count(array_filter([
$this->isLogDrainNewRelicEnabled,
$this->isLogDrainAxiomEnabled,
$this->isLogDrainCustomEnabled,
$this->isLogDrainCloudwatchEnabled,
]));
}
private function enabledProperty(string $type): string
{
return match ($type) {
'newrelic' => 'isLogDrainNewRelicEnabled',
'axiom' => 'isLogDrainAxiomEnabled',
'custom' => 'isLogDrainCustomEnabled',
'cloudwatch' => 'isLogDrainCloudwatchEnabled',
default => throw new \InvalidArgumentException('Unknown log drain type.'),
};
}
@@ -282,6 +355,11 @@ class LogDrains extends Component
'logDrainCustomConfig' => ['required'],
'logDrainCustomConfigParser' => ['string', 'nullable'],
]),
'cloudwatch' => $this->validate([
'logDrainCloudwatchRegion' => ['required', 'max:64', 'regex:/^[a-z]{2}(-[a-z]+)+-\d+$/'],
'logDrainCloudwatchGroup' => ['required', 'regex:/^[\.\-_\/#A-Za-z0-9]{1,512}$/'],
'logDrainCloudwatchStreamPrefix' => ['string', 'nullable', 'max:256', 'regex:/^[A-Za-z0-9_\-\.\/#]*$/'],
]),
default => throw new \InvalidArgumentException('Unknown log drain type.'),
};
}
+8
View File
@@ -2056,6 +2056,14 @@ $siteAddress {
}
public function isLogDrainEnabled()
{
return $this->isFluentBitLogDrainEnabled() || $this->settings->is_logdrain_cloudwatch_enabled;
}
/**
* Whether the active log drain relies on the coolify-log-drain Fluent Bit container.
*/
public function isFluentBitLogDrainEnabled(): bool
{
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled || $this->settings->is_logdrain_custom_enabled;
}
+9
View File
@@ -26,6 +26,7 @@ use OpenApi\Attributes as OA;
'is_cloudflare_tunnel' => ['type' => 'boolean'],
'is_jump_server' => ['type' => 'boolean'],
'is_logdrain_axiom_enabled' => ['type' => 'boolean'],
'is_logdrain_cloudwatch_enabled' => ['type' => 'boolean'],
'is_logdrain_custom_enabled' => ['type' => 'boolean'],
'is_logdrain_highlight_enabled' => ['type' => 'boolean'],
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
@@ -47,6 +48,9 @@ use OpenApi\Attributes as OA;
'is_usable' => ['type' => 'boolean'],
'logdrain_axiom_api_key' => ['type' => 'string'],
'logdrain_axiom_dataset_name' => ['type' => 'string'],
'logdrain_cloudwatch_region' => ['type' => 'string'],
'logdrain_cloudwatch_group' => ['type' => 'string'],
'logdrain_cloudwatch_stream_prefix' => ['type' => 'string'],
'logdrain_custom_config' => ['type' => 'string'],
'logdrain_custom_config_parser' => ['type' => 'string'],
'logdrain_highlight_project_id' => ['type' => 'string'],
@@ -113,6 +117,10 @@ class ServerSetting extends Model
'is_logdrain_custom_enabled',
'logdrain_custom_config',
'logdrain_custom_config_parser',
'is_logdrain_cloudwatch_enabled',
'logdrain_cloudwatch_region',
'logdrain_cloudwatch_group',
'logdrain_cloudwatch_stream_prefix',
'concurrent_builds',
'dynamic_timeout',
'force_disabled',
@@ -190,6 +198,7 @@ class ServerSetting extends Model
'compose_version_checked_at' => 'datetime',
'backup_compression_cpu_percentage' => 'integer',
'server_disk_usage_notification_interval_hours' => 'integer',
'is_logdrain_cloudwatch_enabled' => 'boolean',
];
/**
+2 -2
View File
@@ -1404,7 +1404,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if ($server->isLogDrainEnabled()) {
if ($resource->isLogDrainEnabled()) {
$logging = generate_fluentd_configuration();
$logging = generate_log_drain_configuration($server);
}
}
$volumes = collect(data_get($service, 'volumes', []));
@@ -2775,7 +2775,7 @@ function serviceParser(Service $resource): Collection
if ($server->isLogDrainEnabled()) {
if ($serviceAppsLogDrainEnabledMap->get($serviceName)) {
$logging = generate_fluentd_configuration();
$logging = generate_log_drain_configuration($server);
}
}
$volumes = collect(data_get($service, 'volumes', []));
+27 -2
View File
@@ -3574,7 +3574,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
}
if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
data_set($service, 'logging', generate_fluentd_configuration());
data_set($service, 'logging', generate_log_drain_configuration($resource->server));
}
if ($serviceLabels->count() > 0) {
if ($resource->is_container_label_escape_enabled) {
@@ -4406,7 +4406,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($server->isLogDrainEnabled()) {
if ($resource instanceof Application && $resource->isLogDrainEnabled()) {
data_set($service, 'logging', generate_fluentd_configuration());
data_set($service, 'logging', generate_log_drain_configuration($server));
}
}
if ($serviceLabels->count() > 0) {
@@ -4466,6 +4466,31 @@ function generate_fluentd_configuration(): array
];
}
function generate_log_drain_configuration(Server $server): array
{
if ($server->isFluentBitLogDrainEnabled()) {
return generate_fluentd_configuration();
}
$settings = $server->settings;
$options = [
'awslogs-region' => $settings->logdrain_cloudwatch_region,
'awslogs-group' => $settings->logdrain_cloudwatch_group,
'awslogs-create-group' => 'true',
// Like fluentd-async: a CloudWatch outage or missing credentials must not block or fail the container.
'mode' => 'non-blocking',
// The stream is named after the container; awslogs-stream is never set because it would override this tag.
'tag' => ($settings->logdrain_cloudwatch_stream_prefix ?? '').'{{.Name}}',
];
return [
'driver' => 'awslogs',
'options' => collect($options)
->reject(fn ($value) => blank($value))
->all(),
];
}
function isAssociativeArray($array)
{
if ($array instanceof Collection) {
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('is_logdrain_cloudwatch_enabled')->default(false);
$table->string('logdrain_cloudwatch_region')->nullable();
$table->string('logdrain_cloudwatch_group', 512)->nullable();
$table->string('logdrain_cloudwatch_stream_prefix')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn([
'is_logdrain_cloudwatch_enabled',
'logdrain_cloudwatch_region',
'logdrain_cloudwatch_group',
'logdrain_cloudwatch_stream_prefix',
]);
});
}
};
+56
View File
@@ -16893,6 +16893,28 @@
"logdrain_custom_config_parser": {
"description": "Only present with read:sensitive.",
"type": "string"
},
"is_logdrain_cloudwatch_enabled": {
"type": "boolean"
},
"logdrain_cloudwatch_region": {
"type": [
"string",
"null"
]
},
"logdrain_cloudwatch_group": {
"type": [
"string",
"null"
]
},
"logdrain_cloudwatch_stream_prefix": {
"description": "Prepended to the container name to form the log stream name.",
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -16966,6 +16988,28 @@
},
"logdrain_custom_config_parser": {
"type": "string"
},
"is_logdrain_cloudwatch_enabled": {
"type": "boolean"
},
"logdrain_cloudwatch_region": {
"type": [
"string",
"null"
]
},
"logdrain_cloudwatch_group": {
"type": [
"string",
"null"
]
},
"logdrain_cloudwatch_stream_prefix": {
"description": "Prepended to the container name to form the log stream name.",
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -24817,6 +24861,9 @@
"is_logdrain_axiom_enabled": {
"type": "boolean"
},
"is_logdrain_cloudwatch_enabled": {
"type": "boolean"
},
"is_logdrain_custom_enabled": {
"type": "boolean"
},
@@ -24887,6 +24934,15 @@
"logdrain_axiom_dataset_name": {
"type": "string"
},
"logdrain_cloudwatch_region": {
"type": "string"
},
"logdrain_cloudwatch_group": {
"type": "string"
},
"logdrain_cloudwatch_stream_prefix": {
"type": "string"
},
"logdrain_custom_config": {
"type": "string"
},
+21
View File
@@ -10755,6 +10755,10 @@ paths:
is_logdrain_custom_enabled: { type: boolean }
logdrain_custom_config: { description: 'Only present with read:sensitive.', type: string }
logdrain_custom_config_parser: { description: 'Only present with read:sensitive.', type: string }
is_logdrain_cloudwatch_enabled: { type: boolean }
logdrain_cloudwatch_region: { type: [string, 'null'] }
logdrain_cloudwatch_group: { type: [string, 'null'] }
logdrain_cloudwatch_stream_prefix: { description: 'Prepended to the container name to form the log stream name.', type: [string, 'null'] }
type: object
'401':
$ref: '#/components/responses/401'
@@ -10803,6 +10807,15 @@ paths:
type: string
logdrain_custom_config_parser:
type: string
is_logdrain_cloudwatch_enabled:
type: boolean
logdrain_cloudwatch_region:
type: [string, 'null']
logdrain_cloudwatch_group:
type: [string, 'null']
logdrain_cloudwatch_stream_prefix:
description: 'Prepended to the container name to form the log stream name.'
type: [string, 'null']
type: object
responses:
'200':
@@ -15888,6 +15901,8 @@ components:
type: boolean
is_logdrain_axiom_enabled:
type: boolean
is_logdrain_cloudwatch_enabled:
type: boolean
is_logdrain_custom_enabled:
type: boolean
is_logdrain_highlight_enabled:
@@ -15936,6 +15951,12 @@ components:
type: string
logdrain_axiom_dataset_name:
type: string
logdrain_cloudwatch_region:
type: string
logdrain_cloudwatch_group:
type: string
logdrain_cloudwatch_stream_prefix:
type: string
logdrain_custom_config:
type: string
logdrain_custom_config_parser:
@@ -34,7 +34,7 @@
['value' => false, 'label' => 'Disabled'],
['value' => true, 'label' => 'Enabled'],
]"
:disabled="$isLogDrainAxiomEnabled || $isLogDrainCustomEnabled || !auth()->user()->can('update', $server)" />
:disabled="$isLogDrainAxiomEnabled || $isLogDrainCustomEnabled || $isLogDrainCloudwatchEnabled || !auth()->user()->can('update', $server)" />
<x-forms.input canGate="update" :canResource="$server" type="password" required
id="logDrainNewRelicLicenseKey" label="License key"
:disabled="$server->isLogDrainEnabled()" />
@@ -53,7 +53,7 @@
['value' => false, 'label' => 'Disabled'],
['value' => true, 'label' => 'Enabled'],
]"
:disabled="$isLogDrainNewRelicEnabled || $isLogDrainCustomEnabled || !auth()->user()->can('update', $server)" />
:disabled="$isLogDrainNewRelicEnabled || $isLogDrainCustomEnabled || $isLogDrainCloudwatchEnabled || !auth()->user()->can('update', $server)" />
<x-forms.input canGate="update" :canResource="$server" type="password" required
id="logDrainAxiomApiKey" label="API key"
:disabled="$server->isLogDrainEnabled()" />
@@ -62,6 +62,29 @@
:disabled="$server->isLogDrainEnabled()" />
</div>
</x-application.settings-section>
<x-application.settings-section id="server-cloudwatch-drain-section" title="Amazon CloudWatch Logs"
helper="Send container logs directly to CloudWatch Logs using the Docker awslogs logging driver.<br><br>AWS credentials are read by the Docker daemon on this server, not by Coolify. Attach an IAM instance role, or configure <code>AWS_ACCESS_KEY_ID</code> and <code>AWS_SECRET_ACCESS_KEY</code> for the Docker service or in <code>/root/.aws/credentials</code>.<br><br>The credentials need <code>logs:CreateLogStream</code> and <code>logs:PutLogEvents</code>, plus <code>logs:CreateLogGroup</code> if the log group does not exist yet.<br><br>Delivery errors do not stop containers; they appear in the Docker daemon logs (<code>journalctl -u docker</code>). Redeploy or restart resources to apply the logging driver.">
<div class="mb-4 max-w-sm">
<x-forms.listbox canGate="update" :canResource="$server" id="isLogDrainCloudwatchEnabled" label="Status"
onChange="instantSave" :options="[
['value' => false, 'label' => 'Disabled'],
['value' => true, 'label' => 'Enabled'],
]"
:disabled="$isLogDrainNewRelicEnabled || $isLogDrainAxiomEnabled || $isLogDrainCustomEnabled || !auth()->user()->can('update', $server)" />
</div>
<div class="grid gap-4 lg:grid-cols-3">
<x-forms.input canGate="update" :canResource="$server" required
id="logDrainCloudwatchGroup" label="Log group" placeholder="/coolify/production"
:disabled="$server->isLogDrainEnabled()" />
<x-forms.input canGate="update" :canResource="$server"
id="logDrainCloudwatchStreamPrefix" label="Log stream prefix" placeholder="docker/"
helper="Optional. Log streams are named after the container, prefixed with this value (e.g. docker/&lt;container name&gt;)."
:disabled="$server->isLogDrainEnabled()" />
<x-forms.input canGate="update" :canResource="$server" required
id="logDrainCloudwatchRegion" label="Region" placeholder="us-east-1"
:disabled="$server->isLogDrainEnabled()" />
</div>
</x-application.settings-section>
<x-application.settings-section id="server-custom-drain-section" title="Custom Fluent Bit"
helper="Provide a custom Fluent Bit output and optional parser configuration.">
<div class="mb-4 max-w-sm">
@@ -70,7 +93,7 @@
['value' => false, 'label' => 'Disabled'],
['value' => true, 'label' => 'Enabled'],
]"
:disabled="$isLogDrainNewRelicEnabled || $isLogDrainAxiomEnabled || !auth()->user()->can('update', $server)" />
:disabled="$isLogDrainNewRelicEnabled || $isLogDrainAxiomEnabled || $isLogDrainCloudwatchEnabled || !auth()->user()->can('update', $server)" />
</div>
<div class="grid gap-4 lg:grid-cols-2">
<x-forms.textarea canGate="update" :canResource="$server" rows="8" required
@@ -188,6 +188,89 @@ describe('Log drains API', function () {
->and($settings->logdrain_axiom_api_key)->toBe('axiom-key-123');
});
test('PATCH enables the Amazon CloudWatch Logs drain', function () {
Queue::fake();
$this->withHeaders(serverSubsystemsHeaders())
->patchJson("/api/v1/servers/{$this->server->uuid}/log-drains", [
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_group' => '/coolify/prod',
'logdrain_cloudwatch_region' => 'us-east-1',
'logdrain_cloudwatch_stream_prefix' => 'docker/',
])
->assertOk()
->assertJsonPath('is_logdrain_cloudwatch_enabled', true)
->assertJsonPath('logdrain_cloudwatch_group', '/coolify/prod')
->assertJsonPath('logdrain_cloudwatch_stream_prefix', 'docker/');
$settings = $this->server->settings->fresh();
expect($settings->is_logdrain_cloudwatch_enabled)->toBeTrue()
->and($settings->logdrain_cloudwatch_region)->toBe('us-east-1');
});
test('PATCH rejects enabling CloudWatch without a log group', function () {
Queue::fake();
$this->withHeaders(serverSubsystemsHeaders())
->patchJson("/api/v1/servers/{$this->server->uuid}/log-drains", [
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_region' => 'us-east-1',
])
->assertUnprocessable()
->assertJsonValidationErrors('logdrain_cloudwatch_group');
expect($this->server->settings->fresh()->is_logdrain_cloudwatch_enabled)->toBeFalse();
});
test('PATCH rejects enabling CloudWatch without a region', function () {
Queue::fake();
$this->withHeaders(serverSubsystemsHeaders())
->patchJson("/api/v1/servers/{$this->server->uuid}/log-drains", [
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_group' => 'coolify',
])
->assertUnprocessable()
->assertJsonValidationErrors('logdrain_cloudwatch_region');
expect($this->server->settings->fresh()->is_logdrain_cloudwatch_enabled)->toBeFalse();
});
test('PATCH rejects CloudWatch values that could break the compose file', function (string $field, mixed $value) {
Queue::fake();
$this->withHeaders(serverSubsystemsHeaders())
->patchJson("/api/v1/servers/{$this->server->uuid}/log-drains", [
$field => $value,
])
->assertUnprocessable()
->assertJsonValidationErrors($field);
})->with([
'prefix with interpolation' => ['logdrain_cloudwatch_stream_prefix', 'docker/${HOME}'],
'prefix with colon' => ['logdrain_cloudwatch_stream_prefix', 'docker:'],
'prefix with template braces' => ['logdrain_cloudwatch_stream_prefix', '{{.ID}}'],
'invalid region' => ['logdrain_cloudwatch_region', 'not a region'],
'group with spaces' => ['logdrain_cloudwatch_group', 'my group'],
]);
test('PATCH rejects enabling CloudWatch together with another log drain', function () {
Queue::fake();
$this->server->settings->update([
'is_logdrain_axiom_enabled' => true,
'logdrain_axiom_dataset_name' => 'ds',
'logdrain_axiom_api_key' => 'key',
]);
$this->withHeaders(serverSubsystemsHeaders())
->patchJson("/api/v1/servers/{$this->server->uuid}/log-drains", [
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_group' => 'coolify',
'logdrain_cloudwatch_region' => 'us-east-1',
])
->assertUnprocessable()
->assertJsonValidationErrors('is_logdrain_cloudwatch_enabled');
});
test('other-team log drains endpoints return 404', function () {
$this->withHeaders(serverSubsystemsHeaders())
->getJson("/api/v1/servers/{$this->otherServer->uuid}/log-drains")
@@ -0,0 +1,244 @@
<?php
use App\Actions\Server\StartLogDrain;
use App\Actions\Server\StopLogDrain;
use App\Livewire\Server\LogDrains;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Process;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->team = $this->user->teams()->first();
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->actingAs($this->user);
session(['currentTeam' => $this->team]);
});
it('enables the CloudWatch drain and persists its options', function () {
StartLogDrain::mock()->shouldReceive('handle')->once()->andReturn('ok');
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', '/coolify/prod')
->set('logDrainCloudwatchRegion', 'us-east-1')
->set('logDrainCloudwatchStreamPrefix', 'docker/')
->call('toggleLogDrain', 'cloudwatch')
->assertHasNoErrors()
->assertSet('isLogDrainCloudwatchEnabled', true);
$settings = $this->server->settings->fresh();
expect($settings->is_logdrain_cloudwatch_enabled)->toBeTrue()
->and($settings->logdrain_cloudwatch_group)->toBe('/coolify/prod')
->and($settings->logdrain_cloudwatch_region)->toBe('us-east-1')
->and($settings->logdrain_cloudwatch_stream_prefix)->toBe('docker/');
});
it('requires a log group before enabling CloudWatch', function () {
StartLogDrain::mock()->shouldNotReceive('handle');
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('isLogDrainCloudwatchEnabled', true)
->call('instantSave')
->assertDispatched('error')
->assertSet('isLogDrainCloudwatchEnabled', false);
expect($this->server->settings->fresh()->is_logdrain_cloudwatch_enabled)->toBeFalse();
});
it('requires a region before enabling CloudWatch', function () {
StartLogDrain::mock()->shouldNotReceive('handle');
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->call('toggleLogDrain', 'cloudwatch')
->assertDispatched('error')
->assertSet('isLogDrainCloudwatchEnabled', false);
expect($this->server->settings->fresh()->is_logdrain_cloudwatch_enabled)->toBeFalse();
});
it('rejects stream prefixes that could break the compose file', function (string $prefix) {
StartLogDrain::mock()->shouldNotReceive('handle');
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->set('logDrainCloudwatchRegion', 'us-east-1')
->set('logDrainCloudwatchStreamPrefix', $prefix)
->set('isLogDrainCloudwatchEnabled', true)
->call('instantSave')
->assertDispatched('error')
->assertSet('isLogDrainCloudwatchEnabled', false);
expect($this->server->settings->fresh()->is_logdrain_cloudwatch_enabled)->toBeFalse();
})->with(['docker/${HOME}', 'docker:', '{{.ID}}', 'with space']);
it('enables the CloudWatch drain from the status dropdown', function () {
StartLogDrain::mock()->shouldReceive('handle')->once()->andReturn('ok');
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->set('logDrainCloudwatchRegion', 'us-east-1')
->set('logDrainCloudwatchStreamPrefix', 'docker/')
->set('isLogDrainCloudwatchEnabled', true)
->call('instantSave')
->assertDispatched('success');
$settings = $this->server->settings->fresh();
expect($settings->is_logdrain_cloudwatch_enabled)->toBeTrue()
->and($settings->logdrain_cloudwatch_stream_prefix)->toBe('docker/');
});
it('disables other drains when CloudWatch is enabled', function () {
StartLogDrain::mock()->shouldReceive('handle')->andReturn('ok');
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => true,
'logdrain_newrelic_license_key' => 'abc123',
'logdrain_newrelic_base_uri' => 'https://log-api.newrelic.com',
]);
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->set('logDrainCloudwatchRegion', 'us-east-1')
->call('toggleLogDrain', 'cloudwatch')
->assertSet('isLogDrainNewRelicEnabled', false)
->assertSet('isLogDrainCloudwatchEnabled', true);
$settings = $this->server->settings->fresh();
expect($settings->is_logdrain_newrelic_enabled)->toBeFalsy()
->and($settings->is_logdrain_cloudwatch_enabled)->toBeTrue();
});
it('reverts the CloudWatch flag when starting the drain fails', function () {
StartLogDrain::mock()->shouldReceive('handle')->andThrow(new RuntimeException('runtime boom'));
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->set('logDrainCloudwatchRegion', 'us-east-1')
->call('toggleLogDrain', 'cloudwatch')
->assertSet('isLogDrainCloudwatchEnabled', false);
expect($this->server->settings->fresh()->is_logdrain_cloudwatch_enabled)->toBeFalse();
});
it('forbids team members from enabling CloudWatch', function () {
StartLogDrain::mock()->shouldNotReceive('handle');
$member = User::factory()->create();
$member->teams()->attach($this->team, ['role' => 'member']);
$this->actingAs($member);
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->set('logDrainCloudwatchRegion', 'us-east-1')
->call('toggleLogDrain', 'cloudwatch');
expect($this->server->settings->fresh()->is_logdrain_cloudwatch_enabled)->toBeFalse();
});
it('does not deploy the Fluent Bit container for CloudWatch', function () {
StopLogDrain::mock()->shouldReceive('handle')->once()->andReturn('removed');
$this->server->settings->update([
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_group' => 'coolify',
'logdrain_cloudwatch_region' => 'us-east-1',
]);
$result = StartLogDrain::run($this->server->fresh());
expect($result)->toContain('awslogs');
});
it('turns off the legacy Highlight drain when CloudWatch is enabled', function () {
StartLogDrain::mock()->shouldReceive('handle')->andReturn('ok');
$this->server->settings->update([
'is_logdrain_highlight_enabled' => true,
'logdrain_highlight_project_id' => 'legacy-project',
]);
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->set('logDrainCloudwatchRegion', 'us-east-1')
->set('isLogDrainCloudwatchEnabled', true)
->call('instantSave')
->assertDispatched('success');
$settings = $this->server->settings->fresh();
expect($settings->is_logdrain_highlight_enabled)->toBeFalsy()
->and($settings->is_logdrain_cloudwatch_enabled)->toBeTrue();
});
it('turns off the legacy Highlight drain when switching drains with toggleLogDrain', function () {
StartLogDrain::mock()->shouldReceive('handle')->andReturn('ok');
$this->server->settings->update(['is_logdrain_highlight_enabled' => true]);
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->set('logDrainCloudwatchRegion', 'us-east-1')
->call('toggleLogDrain', 'cloudwatch');
$settings = $this->server->settings->fresh();
expect($settings->is_logdrain_highlight_enabled)->toBeFalsy()
->and($settings->is_logdrain_cloudwatch_enabled)->toBeTrue();
});
it('keeps the legacy Highlight drain when saving without enabling another drain', function () {
StartLogDrain::mock()->shouldReceive('handle')->andReturn('ok');
$this->server->settings->update(['is_logdrain_highlight_enabled' => true]);
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->set('logDrainCloudwatchRegion', 'us-east-1')
->call('submit')
->assertDispatched('success');
expect($this->server->settings->fresh()->is_logdrain_highlight_enabled)->toBeTruthy();
});
it('rejects a save that would enable two log drains at once', function () {
StartLogDrain::mock()->shouldNotReceive('handle');
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => true,
'logdrain_newrelic_license_key' => 'abc123',
'logdrain_newrelic_base_uri' => 'https://log-api.newrelic.com',
]);
// The UI disables the CloudWatch dropdown here; this simulates a crafted request.
Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
->set('logDrainCloudwatchGroup', 'coolify')
->set('logDrainCloudwatchRegion', 'us-east-1')
->set('isLogDrainCloudwatchEnabled', true)
->call('instantSave')
->assertDispatched('error')
->assertSet('isLogDrainCloudwatchEnabled', false)
->assertSet('isLogDrainNewRelicEnabled', true);
$settings = $this->server->settings->fresh();
expect($settings->is_logdrain_cloudwatch_enabled)->toBeFalse()
->and($settings->is_logdrain_newrelic_enabled)->toBeTruthy();
});
it('keeps the Fluent Bit drain running if CloudWatch is also enabled, matching the containers logging config', function () {
Process::fake();
StopLogDrain::mock()->shouldReceive('handle')->andReturn('removed');
$this->server->update(['private_key_id' => PrivateKey::factory()->create(['team_id' => $this->team->id])->id]);
$this->server->settings->update([
'is_logdrain_axiom_enabled' => true,
'logdrain_axiom_dataset_name' => 'ds',
'logdrain_axiom_api_key' => 'key',
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_group' => 'coolify',
'logdrain_cloudwatch_region' => 'us-east-1',
]);
$server = $this->server->fresh();
expect(generate_log_drain_configuration($server)['driver'])->toBe('fluentd');
StartLogDrain::run($server);
Process::assertRan(fn ($process) => str_contains($process->command, 'log-drains/docker-compose.yml up -d'));
});
+26
View File
@@ -77,6 +77,32 @@ it('generates a compose file that passes the database files to the image', funct
->and($service['volumes'])->toBe(['sqlite-data-'.$this->database->uuid.':/var/lib/sqlite']);
});
it('uses the server log drain driver when log draining is enabled', function () {
$commands = [];
$executor = Mockery::mock(DatabaseStartCommandExecutor::class);
$executor->shouldReceive('execute')->once()->withArgs(function (array $captured) use (&$commands) {
$commands = $captured;
return true;
})->andReturn(new Activity);
app()->instance(DatabaseStartCommandExecutor::class, $executor);
$this->server->settings->update([
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_region' => 'us-east-1',
'logdrain_cloudwatch_group' => 'coolify',
]);
$this->database->update(['is_log_drain_enabled' => true]);
Server::flushIdentityMap();
(new StartSqlite)->handle($this->database->fresh(), new Activity);
preg_match("/^echo '([^']+)' \| base64 -d/", collect($commands)->first(fn (string $command) => str_contains($command, 'docker-compose.yml')), $matches);
$service = Yaml::parse(base64_decode($matches[1]))['services'][$this->database->uuid];
expect($service['logging']['driver'])->toBe('awslogs')
->and($service['logging']['options']['awslogs-group'])->toBe('coolify');
});
it('restores a gzipped backup with .restore into the first database file', function () {
$command = app(DatabaseImportCommandBuilder::class)->buildRestoreCommand($this->database, '/tmp/restore_1', false);
@@ -0,0 +1,77 @@
<?php
use App\Models\Server;
use App\Models\ServerSetting;
function serverWithLogDrainSettings(array $attributes): Server
{
$server = new Server;
$server->setRelation('settings', new ServerSetting($attributes));
return $server;
}
it('uses the fluentd driver when CloudWatch is not the active drain', function () {
$server = serverWithLogDrainSettings(['is_logdrain_axiom_enabled' => true]);
expect(generate_log_drain_configuration($server))->toBe(generate_fluentd_configuration());
});
it('uses the awslogs driver with every configured option', function () {
$server = serverWithLogDrainSettings([
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_region' => 'eu-west-1',
'logdrain_cloudwatch_group' => '/coolify/prod',
'logdrain_cloudwatch_stream_prefix' => 'docker/',
]);
expect(generate_log_drain_configuration($server))->toBe([
'driver' => 'awslogs',
'options' => [
'awslogs-region' => 'eu-west-1',
'awslogs-group' => '/coolify/prod',
'awslogs-create-group' => 'true',
'mode' => 'non-blocking',
'tag' => 'docker/{{.Name}}',
],
]);
});
it('names streams after the container when no prefix is set', function () {
$server = serverWithLogDrainSettings([
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_region' => 'us-east-1',
'logdrain_cloudwatch_group' => 'coolify',
]);
expect(generate_log_drain_configuration($server)['options']['tag'])->toBe('{{.Name}}');
});
it('does not fall back to a default region', function () {
$server = serverWithLogDrainSettings([
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_group' => 'coolify',
]);
expect(generate_log_drain_configuration($server)['options'])->not->toHaveKey('awslogs-region');
});
it('never sets awslogs-stream because it would override the container-name tag', function () {
$server = serverWithLogDrainSettings([
'is_logdrain_cloudwatch_enabled' => true,
'logdrain_cloudwatch_group' => 'coolify',
'logdrain_cloudwatch_stream_prefix' => 'apps/',
]);
expect(generate_log_drain_configuration($server)['options'])->not->toHaveKey('awslogs-stream');
});
it('reports CloudWatch as an enabled drain that does not use Fluent Bit', function () {
$cloudwatch = serverWithLogDrainSettings(['is_logdrain_cloudwatch_enabled' => true]);
$axiom = serverWithLogDrainSettings(['is_logdrain_axiom_enabled' => true]);
expect($cloudwatch->isLogDrainEnabled())->toBeTrue()
->and($cloudwatch->isFluentBitLogDrainEnabled())->toBeFalse()
->and($axiom->isLogDrainEnabled())->toBeTrue()
->and($axiom->isFluentBitLogDrainEnabled())->toBeTrue();
});