Skip to content
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
4 changes: 4 additions & 0 deletions php/base.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ COPY --chmod=755 common/ /
# opcache.file_cache needs the directory to exist and be writable
RUN mkdir -p /var/cache/opcache && chown www-data:www-data /var/cache/opcache

# The entrypoint installs the metrics listener config as www-data at runtime.
# The nginx variants already chown /etc/nginx, the frankenphp one doesn't.
RUN if [ -d /etc/frankenphp/caddyfile.d ]; then chown www-data:www-data /etc/frankenphp/caddyfile.d; fi

ARG TARGETARCH
ARG PHP_VERSION
ARG OS
Expand Down
2 changes: 1 addition & 1 deletion php/common/etc/entrypoint.d/40-prepare-laravel.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/sh

if [ -f "$APP_BASE_DIR/artisan" ]; then
echo "Preparing Laravel application..."
echo "🤖 Preparing Laravel application..."

cd $APP_BASE_DIR

Expand Down
4 changes: 2 additions & 2 deletions php/common/etc/entrypoint.d/45-warm-opcache.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# Populate the opcache file cache once per container.

if [ -f "$APP_BASE_DIR/artisan" ] && [ -w /var/cache/opcache ]; then
echo "Warming opcache file cache..."
echo "🤖 Warming opcache file cache..."
cd "$APP_BASE_DIR" || exit 0
php -d opcache.jit=off artisan --version >/dev/null 2>&1 || \
echo "opcache warm-up failed, continuing without it" >&2
echo "⚠️ WARNING: opcache warm-up failed, continuing without it" >&2
fi
13 changes: 13 additions & 0 deletions php/common/etc/entrypoint.d/50-metrics.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
# Expose the private metrics listener on port 8081, but only for Laravel apps:
# the endpoint boots the framework to read the queue depth.

[ -f "${APP_BASE_DIR:-/app}/artisan" ] || exit 0

echo "📊 Metrics enabled on port 8081"

if command -v nginx >/dev/null 2>&1; then
cp /usr/local/share/metrics/nginx.conf /etc/nginx/conf.d/metrics.conf
elif [ -f /etc/frankenphp/Caddyfile ]; then
cp /usr/local/share/metrics/metrics.caddyfile /etc/frankenphp/caddyfile.d/metrics.caddyfile
fi
9 changes: 9 additions & 0 deletions php/common/usr/local/share/metrics/metrics.caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Private metrics listener, installed by /etc/entrypoint.d/50-metrics.sh.
:8081 {
root * /usr/local/share/metrics

@metrics path /_metrics/queue-size
rewrite @metrics /queue-size.php

php_server
}
19 changes: 19 additions & 0 deletions php/common/usr/local/share/metrics/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Private metrics listener, installed by /etc/entrypoint.d/50-metrics.sh.
server {
listen 8081;

server_name _;
access_log off;

location = /_metrics/queue-size {
include fastcgi_params;
fastcgi_param SCRIPT_NAME /_metrics/queue-size;
fastcgi_param SCRIPT_FILENAME /usr/local/share/metrics/queue-size.php;
fastcgi_read_timeout 5s;
fastcgi_pass 127.0.0.1:9000;
}

location / {
return 404;
}
}
82 changes: 82 additions & 0 deletions php/common/usr/local/share/metrics/queue-size.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/**
* Queue depth for autoscalers (KEDA metrics-api and friends).
*
* Served on a private port by the web server of this container, see
* /etc/entrypoint.d/50-metrics.sh. Nothing routes that port from outside the
* pod, so there is no authentication here.
*
* GET /_metrics/queue-size[?queues=high,default][&connection=redis]
* -> {"name":"high,default","size":42}
*
* Queues and connection are auto-discovered from the app when not given, so
* apps need no configuration. Failures answer 500 on purpose: a scaler that
* reads 0 from a broken app scales the workers away and jobs stop running.
*/

use Illuminate\Http\Request;
use Illuminate\Support\Arr;

ini_set('default_socket_timeout', '3');

header('Content-Type: application/json');

if (parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) !== '/_metrics/queue-size') {
http_response_code(404);
echo json_encode(['error' => 'not found']);

return;
}

try {
$basePath = getenv('APP_BASE_DIR') ?: '/app';

require $basePath.'/vendor/autoload.php';

$request = Request::capture();

$app = require $basePath.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

// A "queue" is a name, a comma separated list of names, or an array of either.
$split = fn ($value) => collect(Arr::wrap($value))
->flatMap(fn ($item) => explode(',', (string) $item))
->map(fn (string $queue) => trim($queue))
->filter()
->values();

$connection = $request->query('connection');
// "queue" is accepted too: it is what artisan and the chart call it.
$queues = $split($request->query('queues', $request->query('queue', [])));

if ($queues->isEmpty()) {
// Detect queues from Horizon configuration
$supervisors = collect(config('horizon.environments.'.$app->environment()) ?: [])
->map(fn (array $options, string $name) => array_merge(config("horizon.defaults.{$name}", []), $options))
->whenEmpty(fn () => collect(config('horizon.defaults') ?: []));

foreach ($supervisors as $supervisor) {
$queues = $queues->merge($split($supervisor['queue'] ?? []));
$connection ??= $supervisor['connection'] ?? null;
}
}

$connection ??= config('queue.default');

$queues = $queues->unique()->values()
->whenEmpty(fn () => $split(config("queue.connections.{$connection}.queue", 'default')));

$queue = $app->make('queue')->connection($connection);

// Queue::size() is driver agnostic: on redis it counts pending + delayed +
// reserved, on database every row in "jobs". No per-driver branching, and
// delayed jobs are included so a scaled-to-zero worker still wakes up.
echo json_encode([
'name' => $queues->implode(','),
'size' => $queues->sum(fn (string $name) => $queue->size($name)),
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}