#!/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}"