diff --git a/.env.observability.example b/.env.observability.example index b0eff2e..ca23ec3 100644 --- a/.env.observability.example +++ b/.env.observability.example @@ -20,15 +20,30 @@ GRAFANA_ROOT_URL= # ── Web server logs (Promtail) ─────────────────────────────────────────────────── # Directory Promtail scans (recursively) for *access*.log / *error*.log files. +# Must be a real, literal directory — this is a Docker bind-mount source, so +# shell globs (e.g. a trailing `/*/log`) do NOT work; Docker creates an empty +# directory with that literal name instead of expanding it. # Examples: # /var/log ← default; covers /var/log/virtualmin, # /var/log/nginx, /var/log/apache2, etc. -# /home/rubricmaker/web/*/log ← HestiaCP per-domain logs +# /home/rubricmaker/web ← HestiaCP per-domain logs (Promtail's +# own ** glob recurses into each +# domain's logs/ subfolder) # /var/log/virtualmin ← Virtualmin vhost logs # Combined docker-compose.yml stack: leave as default — Promtail's Docker scrape # job picks up the `app` container's stdout logs directly (see nginx.prod.conf). RUBRICMAKER_LOG_DIR=/var/log +# HestiaCP only: web//logs/*.log under RUBRICMAKER_LOG_DIR are usually +# symlinks to the panel's real webserver log dir, e.g. /var/log/apache2/domains +# (Apache) or /var/log/nginx/domains (Nginx) — check with +# `ls -la /home//web//logs/`. Set this to that real target dir so +# the symlinks resolve inside the container; it's mounted at the same absolute +# path. Leave unset/blank on non-HestiaCP setups — when blank, the compose +# file falls back to mounting `/var/log` read-only at the same path, which +# is harmless since Promtail only scrapes `/var/host-logs`. +# RUBRICMAKER_WEBSERVER_LOG_DIR=/var/log/apache2/domains + # ── client_logs (Part 1 app-level diagnostics) ─────────────────────────────────── # Optional: lets Grafana query the `client_logs` table (see README → "Stress-test diff --git a/.github/workflows/deploy-hestiacp.yml b/.github/workflows/deploy-hestiacp.yml index 2bdfdb7..9d9f2ac 100644 --- a/.github/workflows/deploy-hestiacp.yml +++ b/.github/workflows/deploy-hestiacp.yml @@ -31,6 +31,11 @@ jobs: VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }} VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }} SUPABASE_PUBLIC_URL: ${{ secrets.SUPABASE_PUBLIC_URL }} + # Optional diagnostic event stream to `client_logs` — see README → + # "Stress-test logging". Toggle via repo Settings → Secrets and + # variables → Actions → Variables (not a secret: it's a feature + # flag, not sensitive data). + VITE_STRESS_TEST_LOGGING: ${{ vars.VITE_STRESS_TEST_LOGGING }} - name: Deploy via rsync uses: burnett01/rsync-deployments@9.0.0 diff --git a/docker-compose.observability.yml b/docker-compose.observability.yml index 3fab11d..dd34cb0 100644 --- a/docker-compose.observability.yml +++ b/docker-compose.observability.yml @@ -33,6 +33,14 @@ services: # Host log directory for traditional Apache/Nginx + HestiaCP/Virtualmin # deployments (set RUBRICMAKER_LOG_DIR to the panel's log path). - ${RUBRICMAKER_LOG_DIR:-/var/log}:/var/host-logs:ro + # HestiaCP's per-domain web//logs/*.log files are symlinks to + # the panel's real webserver log dir (e.g. /var/log/apache2/domains) — + # mount that dir too, at the *same* path, so the symlinks resolve + # inside the container instead of "no such file or directory". Falls + # back to /var/log (already guaranteed to exist, unlike an Apache-only + # path) so leaving RUBRICMAKER_WEBSERVER_LOG_DIR unset on non-HestiaCP + # setups is a harmless no-op rather than creating a bogus empty dir. + - ${RUBRICMAKER_WEBSERVER_LOG_DIR:-/var/log}:${RUBRICMAKER_WEBSERVER_LOG_DIR:-/var/log}:ro # Optional: only relevant if the combined docker-compose.yml stack also # runs on this host — harmless if these paths don't exist. - /var/lib/docker/containers:/var/lib/docker/containers:ro @@ -76,13 +84,21 @@ volumes: grafana-data: # persists Grafana dashboards/users # ── Optional: self-hosted db on the same host ──────────────────────────────── -# If the combined docker-compose.yml stack (db, auth, rest, ...) also runs on -# this host and you want Grafana to reach its `db` container by name, join its -# network and set SUPABASE_DB_HOST=db: +# If a self-hosted Supabase stack also runs on this host and you want Grafana +# to reach its db container by name, join its Docker network. The network and +# container names depend on which self-hosted stack this is: +# - This repo's combined docker-compose.yml (`name: rubricmaker`): +# network rubricmaker_default, SUPABASE_DB_HOST=db:5432 +# - The official Supabase CLI / self-hosted compose stack: +# network supabase_default, SUPABASE_DB_HOST=supabase-db:5432 +# (connect to the `supabase-db` container directly, not the +# `supabase-pooler`/Supavisor container also on 5432/6543) +# Confirm with `docker network ls` / `docker ps` on the host — names can be +# customized in either stack's own compose file. # # networks: # default: -# name: rubricmaker_default +# name: supabase_default # external: true # # Otherwise (managed Supabase, or Supabase on another host), leave this diff --git a/docs/OBSERVABILITY_HESTIACP.md b/docs/OBSERVABILITY_HESTIACP.md index c41e7ae..9728dcd 100644 --- a/docs/OBSERVABILITY_HESTIACP.md +++ b/docs/OBSERVABILITY_HESTIACP.md @@ -102,14 +102,29 @@ Edit `.env.observability`: GRAFANA_ADMIN_PASSWORD= # HestiaCP per-domain log layout (see .env.observability.example for the -# Virtualmin equivalent). Promtail recurses into this path looking for -# *access*.log / *error*.log files. -RUBRICMAKER_LOG_DIR=/home/rubricmaker/web/*/log +# Virtualmin equivalent). This is a Docker bind-mount source, so it must be a +# real, literal directory — no shell globs (Docker won't expand a `*` in a +# host path; it just creates an empty dir with that literal name and mounts +# that). Point at the parent `web` dir; Promtail's own `**` glob in +# promtail-config.yml recurses into each domain's log/ folder from there. +RUBRICMAKER_LOG_DIR=/home/rubricmaker/web # Set in step 7, once the subdomain is live. GRAFANA_DOMAIN=observability.rubricmaker.example.com GRAFANA_ROOT_URL=https://observability.rubricmaker.example.com/ +# HestiaCP's web//logs/*.log files are usually symlinks to the +# panel's real webserver log dir — check with +# `ls -la /home/rubricmaker/web//logs/`. Mount that real target +# dir too (same absolute path) so the symlinks resolve inside the container. +# This doesn't cause duplicate log ingestion: Promtail's only webserver scrape +# path is RUBRICMAKER_LOG_DIR (`/var/host-logs/**` in promtail-config.yml) — +# this second mount isn't separately scraped, it just makes the symlink +# targets resolvable when Promtail follows them. +# Apache (most HestiaCP installs): +RUBRICMAKER_WEBSERVER_LOG_DIR=/var/log/apache2/domains +# Nginx-only HestiaCP installs would instead use /var/log/nginx/domains. + # Optional — only if you also want client_logs queryable from Grafana # (see README → "Stress-test logging"). # SUPABASE_DB_HOST= @@ -118,11 +133,17 @@ GRAFANA_ROOT_URL=https://observability.rubricmaker.example.com/ # SUPABASE_DB_PASSWORD= ``` -> **`RUBRICMAKER_LOG_DIR=/home/rubricmaker/web/*/log` vs `/var/log`:** -> HestiaCP writes per-domain logs to `/home//web//log/` *and* -> aggregates them under `/var/log/$WEB_SYSTEM/domains/`. Either path works; -> the `/home/.../web/*/log` glob is narrower if this HestiaCP user hosts -> multiple unrelated domains and you only want RubricMaker's logs. +> **`RUBRICMAKER_LOG_DIR=/home/rubricmaker/web` vs `/var/log`:** +> HestiaCP writes per-domain logs to `/home//web//logs/` *and* +> aggregates them under `/var/log/$WEB_SYSTEM/domains/`. Either parent path +> works as `RUBRICMAKER_LOG_DIR`, but the per-domain `logs/` entries are +> typically symlinks pointing at the latter — see `RUBRICMAKER_WEBSERVER_LOG_DIR` +> above, which mounts that real target dir so the symlinks don't dangle inside +> the container. To narrow to a single domain if this HestiaCP user hosts +> multiple unrelated ones, point `RUBRICMAKER_LOG_DIR` directly at that +> domain's own `logs/` dir (e.g. +> `/home/rubricmaker/web/rubricmaker.example.com/logs`) — but it must be a +> literal, existing path; wildcards don't work in a bind-mount source. ---