Ship production optimization M1-M12.5A: queues, metrics, HTTP observability, and vector search reliability.

Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
This commit is contained in:
2026-08-25 07:58:47 +02:00
parent f52879edbb
commit 8a80aae21e
114 changed files with 9293 additions and 284 deletions
+68
View File
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* Read-only analyzer for /var/log/nginx/skinbase-performance.log
*
* Usage:
* php scripts/analyze-http-performance.php --file=/var/log/nginx/skinbase-performance.log --since=24h --top=20 --host=skinbase.org
*/
use App\Support\Http\HttpPerformanceLogAnalyzer;
use App\Support\Http\HttpUriNormalizer;
require __DIR__.'/../vendor/autoload.php';
$options = [
'file' => '/var/log/nginx/skinbase-performance.log',
'since' => '24h',
'min_requests' => 1,
'top' => 20,
'status' => null,
'uri' => null,
'host' => null,
'slow_ms' => null,
];
foreach (array_slice($argv, 1) as $arg) {
if (! str_starts_with($arg, '--')) {
fwrite(STDERR, "Unknown argument: {$arg}\n");
exit(2);
}
[$key, $value] = array_pad(explode('=', substr($arg, 2), 2), 2, '1');
$map = [
'file' => 'file',
'since' => 'since',
'min-requests' => 'min_requests',
'top' => 'top',
'status' => 'status',
'uri' => 'uri',
'host' => 'host',
'slow-ms' => 'slow_ms',
];
if (! isset($map[$key])) {
fwrite(STDERR, "Unknown option: --{$key}\n");
exit(2);
}
$options[$map[$key]] = $value;
}
$file = (string) $options['file'];
if (! is_readable($file)) {
fwrite(STDERR, "Cannot read {$file}\n");
exit(1);
}
$analyzer = new HttpPerformanceLogAnalyzer(new HttpUriNormalizer());
$result = $analyzer->analyze($file, [
'since' => $options['since'],
'min_requests' => (int) $options['min_requests'],
'top' => (int) $options['top'],
'status' => $options['status'],
'uri' => $options['uri'],
'host' => $options['host'],
'slow_ms' => $options['slow_ms'] !== null ? (float) $options['slow_ms'] : null,
]);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env bash
# Skinbase.org M0 production baseline collector
# READ-ONLY / non-destructive / secret-safe
#
# Run on the production application host from the live app directory, e.g.:
# cd /opt/www/virtual/SkinbaseNova && sudo -u www-data bash scripts/collect-production-baseline.sh
#
# Never prints: passwords, tokens, keys, APP_KEY, DSNs, cookies, session payloads, .env dumps.
# Does not: write to MySQL, flush Redis, restart services, clear caches, run jobs, migrate, deploy.
set -u
set -o pipefail
umask 077
APPROVED_ENV_KEYS='^(APP_ENV|APP_DEBUG|CACHE_STORE|SESSION_DRIVER|QUEUE_CONNECTION|SCOUT_DRIVER|FILESYSTEM_DISK|UPLOAD_QUEUE_DERIVATIVES|DOWNLOAD_ACCEL_ENABLED|DOWNLOAD_ACCEL_PATH|SITEMAPS_BUILD_ON_REQUEST|SITEMAPS_FALLBACK_TO_LIVE_BUILD|SITEMAPS_PREGENERATED_ENABLED|SITEMAPS_PREFER_PUBLISHED_RELEASE|REDIS_CLIENT|HOMEPAGE_CACHE_STORE|BROADCAST_CONNECTION|SCOUT_QUEUE_NAME|HORIZON_PATH)$'
redact() {
sed -E \
-e 's/(password|passwd|secret|token|apikey|api_key|access_key|private_key|dsn|authorization)[=:][[:space:]]*[^[:space:]]+/\1=[REDACTED]/Ig' \
-e 's#mysql://[^[:space:]]+#mysql://[REDACTED]#g' \
-e 's#redis://[^[:space:]]+#redis://[REDACTED]#g'
}
run() {
local name="$1"
shift
echo "=== ${name} ===" >>"${LOG}"
echo "\$ $*" >>"${LOG}"
if "$@" >>"${OUT}/${name}.txt" 2>>"${OUT}/${name}.err"; then
echo "OK ${name}" | tee -a "${LOG}"
else
echo "FAIL ${name} exit=$?" | tee -a "${LOG}"
fi
}
sql() {
local name="$1"
local query="$2"
echo "=== sql:${name} ===" >>"${LOG}"
if command -v php >/dev/null 2>&1 && [[ -f artisan ]]; then
php artisan db:show --counts=false >/dev/null 2>&1 || true
php -r '
require "vendor/autoload.php";
$app = require "bootstrap/app.php";
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$q = $argv[1];
try {
$rows = Illuminate\Support\Facades\DB::select($q);
echo json_encode($rows, JSON_PRETTY_PRINT), PHP_EOL;
} catch (Throwable $e) {
fwrite(STDERR, "SQL_ERROR " . $e->getMessage() . PHP_EOL);
exit(1);
}
' -- "$query" >"${OUT}/sql-${name}.json" 2>"${OUT}/sql-${name}.err" && echo "OK sql:${name}" | tee -a "${LOG}" || echo "FAIL sql:${name}" | tee -a "${LOG}"
else
echo "SKIP sql:${name} (no php/artisan)" | tee -a "${LOG}"
fi
}
ROOT="$(pwd)"
if [[ -f artisan ]]; then
ROOT="$(pwd)"
elif [[ -f ../artisan ]]; then
cd ..
ROOT="$(pwd)"
fi
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
OUT="${ROOT}/storage/app/optimization-baseline/${STAMP}"
mkdir -p "${OUT}"
LOG="${OUT}/collector.log"
touch "${LOG}"
echo "Skinbase M0 collector start ${STAMP}" | tee -a "${LOG}"
echo "cwd=${ROOT}" | tee -a "${LOG}"
# --- host ---
run 01-hostname hostname
run 01b-hostname-f hostname -f
run 02-uname uname -a
run 03-os-release cat /etc/os-release
run 04-lscpu lscpu
run 05-free free -h
run 06-lsblk lsblk
run 07-df df -h
run 08-uptime uptime
run 09-vmstat vmstat 1 5
if command -v iostat >/dev/null 2>&1; then
run 10-iostat iostat -x 1 5
else
echo "SKIP iostat" | tee -a "${LOG}"
fi
run 11-ps-cpu ps aux --sort=-%cpu
run 12-ps-mem ps aux --sort=-%mem
# keep only heads in summary copies
if [[ -f "${OUT}/11-ps-cpu.txt" ]]; then head -n 30 "${OUT}/11-ps-cpu.txt" >"${OUT}/11-ps-cpu-top30.txt"; fi
if [[ -f "${OUT}/12-ps-mem.txt" ]]; then head -n 30 "${OUT}/12-ps-mem.txt" >"${OUT}/12-ps-mem-top30.txt"; fi
run 13-ps-stack bash -c "ps aux | egrep 'nginx|php-fpm|redis|mysql|mysqld|meili|horizon|queue:work|supervisord|ssr.js|reverb|artisan' | grep -v grep || true"
run 14-systemctl-running systemctl --type=service --state=running --no-pager
# --- nginx ---
run 15-nginx-v nginx -v
run 16-nginx-V nginx -V
if command -v nginx >/dev/null 2>&1; then
if nginx -T >/dev/null 2>&1; then
nginx -T 2>"${OUT}/17-nginx-T.err" | redact >"${OUT}/17-nginx-T.txt" || true
echo "OK 17-nginx-T" | tee -a "${LOG}"
elif sudo -n nginx -T >/dev/null 2>&1; then
sudo -n nginx -T 2>"${OUT}/17-nginx-T.err" | redact >"${OUT}/17-nginx-T.txt" || true
echo "OK 17-nginx-T via sudo -n" | tee -a "${LOG}"
else
echo "SKIP 17-nginx-T (need permission)" | tee -a "${LOG}"
fi
fi
# --- php ---
run 18-php-v php -v
run 19-php-ini php --ini
run 20-php-m php -m
run 21-php-limits bash -c "php -i | egrep 'memory_limit|max_execution_time|max_input_time|upload_max_filesize|post_max_size|max_file_uploads|realpath_cache_size|realpath_cache_ttl' || true"
run 22-php-opcache bash -c "php -i | grep -i opcache || true"
run 23-php-fpm-ps bash -c "ps aux | grep php-fpm | grep -v grep || true"
run 24-php-units systemctl list-units --no-pager
# filter later
# copy fpm pool configs if readable
mkdir -p "${OUT}/php-fpm"
for f in /etc/php/*/fpm/pool.d/*.conf /etc/php-fpm.d/*.conf /usr/local/etc/php-fpm.d/*.conf; do
if [[ -r "$f" ]]; then
cp "$f" "${OUT}/php-fpm/$(basename "$f")" || true
fi
done
# --- laravel non-secret env ---
if [[ -f .env ]]; then
grep -E "${APPROVED_ENV_KEYS}" .env >"${OUT}/25-env-approved.txt" 2>"${OUT}/25-env-approved.err" || true
echo "OK 25-env-approved (filtered)" | tee -a "${LOG}"
else
echo "SKIP 25-env-approved (no .env in cwd)" | tee -a "${LOG}"
fi
if [[ -f artisan ]]; then
php artisan about --only=environment,cache,drivers,storage 2>"${OUT}/26-artisan-about.err" | redact >"${OUT}/26-artisan-about.txt" || echo "FAIL artisan about" | tee -a "${LOG}"
php artisan horizon:status >"${OUT}/27-horizon-status.txt" 2>"${OUT}/27-horizon-status.err" || true
php artisan queue:failed --json >"${OUT}/28-queue-failed.json" 2>"${OUT}/28-queue-failed.err" || php artisan queue:failed >"${OUT}/28-queue-failed.txt" 2>"${OUT}/28-queue-failed.err" || true
php artisan schedule:list >"${OUT}/29-schedule-list.txt" 2>"${OUT}/29-schedule-list.err" || true
php artisan route:list --columns=method,uri,name,action 2>/dev/null | egrep -i 'debugbar|telescope|clockwork' >"${OUT}/30-debug-routes.txt" || true
fi
# laravel cache files
ls -la bootstrap/cache >"${OUT}/31-bootstrap-cache.txt" 2>"${OUT}/31-bootstrap-cache.err" || true
ls -la storage/framework/views | head -n 20 >"${OUT}/32-compiled-views.txt" 2>"${OUT}/32-compiled-views.err" || true
ls -la public/sitemaps | head -n 50 >"${OUT}/33-public-sitemaps.txt" 2>"${OUT}/33-public-sitemaps.err" || true
ls -la public/sitemap.xml public/robots.txt >"${OUT}/34-public-seo-files.txt" 2>"${OUT}/34-public-seo-files.err" || true
# composer install mode
if [[ -f vendor/composer/installed.json ]]; then
php -r '$j=json_decode(file_get_contents("vendor/composer/installed.json"), true); echo "dev-package-count="; $n=0; foreach (($j["packages-dev"] ?? []) as $p) { $n++; } echo $n, PHP_EOL; echo "packages-count=", count($j["packages"] ?? []), PHP_EOL;' >"${OUT}/35-composer-dev-state.txt" 2>"${OUT}/35-composer-dev-state.err" || true
fi
# --- mysql ---
run 36-mysql-version mysql --version
sql 37-version "SELECT VERSION() AS version"
sql 38-vars "SHOW VARIABLES WHERE Variable_name IN ('innodb_buffer_pool_size','innodb_buffer_pool_instances','max_connections','thread_cache_size','tmp_table_size','max_heap_table_size','slow_query_log','slow_query_log_file','long_query_time','log_queries_not_using_indexes','performance_schema')"
sql 39-status "SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected','Threads_running','Max_used_connections','Slow_queries','Queries','Questions','Uptime')"
sql 40-dbsize "SELECT table_schema, ROUND(SUM(data_length + index_length)/1024/1024,1) AS total_mb, ROUND(SUM(data_length)/1024/1024,1) AS data_mb, ROUND(SUM(index_length)/1024/1024,1) AS index_mb FROM information_schema.tables WHERE table_schema = DATABASE() GROUP BY table_schema"
sql 41-largest "SELECT table_name, table_rows, ROUND(data_length/1024/1024,1) AS data_mb, ROUND(index_length/1024/1024,1) AS index_mb, ROUND((data_length+index_length)/1024/1024,1) AS total_mb FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY data_length+index_length DESC LIMIT 50"
sql 42-cache-sessions "SELECT table_name, table_rows, ROUND((data_length+index_length)/1024/1024,1) AS mb FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name IN ('cache','cache_locks','sessions')"
sql 43-innodb-reads "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'"
sql 44-innodb-rows "SHOW GLOBAL STATUS LIKE 'Innodb_rows_%'"
sql 45-tmp "SHOW GLOBAL STATUS LIKE 'Created_tmp%'"
sql 46-handler "SHOW GLOBAL STATUS LIKE 'Handler_read%'"
for tbl in artworks artwork_stats artwork_metric_snapshots_hourly rank_artwork_scores tags artwork_view_events artwork_downloads; do
sql "index-${tbl}" "SHOW INDEX FROM \`${tbl}\`"
done
# --- redis ---
if command -v redis-cli >/dev/null 2>&1; then
redis-cli INFO server >"${OUT}/50-redis-server.txt" 2>"${OUT}/50-redis-server.err" || true
redis-cli INFO memory >"${OUT}/51-redis-memory.txt" 2>"${OUT}/51-redis-memory.err" || true
redis-cli INFO stats >"${OUT}/52-redis-stats.txt" 2>"${OUT}/52-redis-stats.err" || true
redis-cli INFO clients >"${OUT}/53-redis-clients.txt" 2>"${OUT}/53-redis-clients.err" || true
redis-cli INFO keyspace >"${OUT}/54-redis-keyspace.txt" 2>"${OUT}/54-redis-keyspace.err" || true
for q in default search vision recommendations discovery mail notifications broadcasts forum-security forum-moderation; do
echo -n "queues:${q}: " >>"${OUT}/55-redis-queue-llen.txt"
redis-cli LLEN "queues:${q}" >>"${OUT}/55-redis-queue-llen.txt" 2>>"${OUT}/55-redis-queue-llen.err" || echo "fail" >>"${OUT}/55-redis-queue-llen.txt"
done
echo "OK redis-cli probes" | tee -a "${LOG}"
else
echo "SKIP redis-cli" | tee -a "${LOG}"
fi
# --- processes / workers ---
run 56-queue-ps bash -c "ps aux | grep -E 'queue:work|horizon|ssr.js|reverb' | grep -v grep || true"
run 57-supervisor bash -c "command -v supervisorctl >/dev/null && supervisorctl status || echo 'no supervisorctl'"
run 58-cron-user bash -c "crontab -l || echo 'no user crontab'"
run 59-systemd-timers systemctl list-timers --no-pager
# --- meilisearch ---
run 60-meili-ps bash -c "ps aux | grep -i meili | grep -v grep || true"
if command -v curl >/dev/null 2>&1; then
curl -sS --max-time 5 http://127.0.0.1:7700/health >"${OUT}/61-meili-health.txt" 2>"${OUT}/61-meili-health.err" || true
curl -sS --max-time 5 http://127.0.0.1:7700/version >"${OUT}/62-meili-version.txt" 2>"${OUT}/62-meili-version.err" || true
fi
# --- public HTTP from the server itself ---
if command -v curl >/dev/null 2>&1; then
curl -sI --max-time 15 https://skinbase.org/ >"${OUT}/70-head-home.txt" 2>"${OUT}/70-head-home.err" || true
curl -sI --max-time 15 https://skinbase.org/sitemap.xml >"${OUT}/71-head-sitemap.txt" 2>"${OUT}/71-head-sitemap.err" || true
curl -sI --max-time 15 https://skinbase.org/robots.txt >"${OUT}/72-head-robots.txt" 2>"${OUT}/72-head-robots.err" || true
fi
# strip cookies if any slipped into HTTP dumps
for f in "${OUT}"/*.txt; do
[[ -f "$f" ]] || continue
sed -i -E 's/^(Set-Cookie:).*/\1 [REDACTED]/I' "$f" 2>/dev/null || true
done
echo "DONE output=${OUT}" | tee -a "${LOG}"
echo "${OUT}"
+3
View File
@@ -329,6 +329,9 @@ build_rsync_args() {
--exclude "/var/php-sessions"
--exclude "/oldSite"
--exclude "/vendor"
--exclude ".config"
--exclude ".copilot"
--exclude ".vscode"
-e "$ssh_bin"
)
}