Manage now connects to existing Grafana/Prometheus/Alertmanager instances and never deploys its own stack. - docker-compose.yml / docker-compose.dev.yml: removed prometheus, loki, alloy, grafana, alertmanager, node-exporter services, the monitoring network, and observability named volumes; they now ship only backend + frontend. Dev frontend now joins the web network so the Vite dev proxy can reach the backend. - backend: alertmanager_url default is now empty; /api/monitoring/alerts and /alertmanager-status return graceful "not configured" responses when ALERTMANAGER_URL is unset. Added not-configured tests. - docker-compose.observability.yml: kept as the optional standalone example; header clarifies Manage does not deploy it. - Removed orphaned combined monitoring/prometheus/prometheus.yml (standalone stack uses prometheus.standalone.yml). - Docs (README, REQUIREMENTS decision log, monitoring-logging-design, observability-runbooks, context.md, MIGRATION_PLAN, frontend/README, CHANGELOG) updated to the connect-to-existing model. VITE_GRAFANA_URL / VITE_PROMETHEUS_URL remain as optional frontend deep-link overrides. .env.example still needs a manual update (safety policy blocks assistant edits): set ALERTMANAGER_URL empty/optional and move standalone-only vars out of the root file.
13 KiB
Code Context
Status (2026-06-23): Manage no longer deploys an observability stack. The root
docker-compose.yml/docker-compose.dev.ymlship only the backend and frontend; Grafana, Prometheus, Loki, Alertmanager, Alloy, and Node Exporter were removed from them. Manage connects to existing instances. The standalone example stack lives indocker-compose.observability.yml. Some snippets below still reference the former in-compose services and are kept as historical context; treatdocker-compose.observability.ymlas authoritative for the stack layout.
Files Retrieved
docker-compose.yml(lines 1–262) – production Compose stack; defines observability services and Traefik routing.docker-compose.dev.yml(lines 1–234) – development Compose stack; same observability services but with host ports exposed and auth disabled..env.example(lines 1–55) – template with all required environment variables for the stack, including Prometheus/Grafana/Alertmanager/Alloy/Loki and Node Exporter settings.monitoring/prometheus/prometheus.yml(all lines) – Prometheus scrape configuration including themanage-backend,node-exporter, and file-SD remote targets.monitoring/prometheus/rules/backup_alerts.yml(all lines) – Prometheus alerting rules for backup jobs and observability-stack health.monitoring/alertmanager/alertmanager.yml(all lines) – Alertmanager routing, email/webhook receivers, and inhibition rules.monitoring/grafana/grafana.ini(all lines) – Grafana server, embedding, and generic OAuth (Authentik) configuration.monitoring/alloy/config.alloy(all lines) – Alloy pipeline to discover Docker containers and push logs to Loki.monitoring/loki/loki.yml(all lines) – Single-node Loki configuration with filesystem storage and 30-day retention.backend/src/media_library_viewer_api/observability.py(all lines) – Prometheus metrics definitions and helper functions.backend/src/media_library_viewer_api/main.py(lines 1–128) – FastAPI entrypoint exposing/metricsand wiring request/observability middleware.backend/src/media_library_viewer_api/services/targets.py(all lines) – Backend writes file-SD target list for remote Node Exporters.backend/src/media_library_viewer_api/config.py(lines 1–88) – Settings includingprometheus_enabled,prometheus_file_sd_dir, andalertmanager_url.docs/monitoring-logging-design.md(all lines) – Architecture/design document describing the observability stack.docs/observability-runbooks.md(all lines) – Operational runbooks for the observability services.
Key Code
Backend /metrics endpoint
backend/src/media_library_viewer_api/main.py:
@app.middleware("http")
async def enforce_jwt_auth(request: Request, call_next):
if request.url.path in {"/api/health", "/api/version", "/metrics"}:
return await call_next(request)
return await require_jwt_auth(request, call_next)
@app.get("/metrics")
def metrics() -> Response:
"""Expose Prometheus metrics."""
data, content_type = metrics_payload()
return FastAPIResponse(content=data, media_type=content_type)
Metrics emitted by the backend
backend/src/media_library_viewer_api/observability.py:
REQUESTS_TOTAL = Counter("manage_api_requests_total", "Total API requests", ["method", "path", "status_code"])
REQUEST_DURATION = Histogram("manage_api_request_duration_seconds", "API request duration", ["method", "path"], ...)
SSH_COMMANDS_TOTAL = Counter("manage_ssh_commands_total", "Total SSH/local commands executed", ["machine_id", "action", "status"])
MEDIA_INDEX_BUILDS_TOTAL = Counter("manage_media_index_builds_total", "Total media index build attempts", ["status"])
BACKUP_RUNS_TOTAL = Counter("manage_backup_runs_total", "Total backup runs", ["job_name", "status"])
BACKUP_RUNS_LAST_SUCCESS = Gauge("manage_backup_runs_last_success_timestamp", "Unix timestamp of the last successful backup run per job", ["job_name"])
MAIL_QUEUE_SIZE = Counter("manage_mail_queue_messages_total", "Total messages enqueued", ["status"])
Prometheus scrape configuration
monitoring/prometheus/prometheus.yml:
scrape_configs:
- job_name: manage-backend
static_configs:
- targets:
- backend:8000
metrics_path: /metrics
scrape_interval: 15s
- job_name: node-exporter
static_configs:
- targets:
- node-exporter:9100
- job_name: node-exporter-remote
file_sd_configs:
- files:
- /etc/prometheus/file-sd/node_exporter_targets.json
refresh_interval: 30s
Backend-managed remote Node Exporter targets
backend/src/media_library_viewer_api/services/targets.py:
def build_node_exporter_targets(store: SettingsStore) -> list[dict[str, Any]]:
...
def write_prometheus_targets(store: SettingsStore, file_sd_dir: Path | None = None) -> Path:
...
file_path = file_sd_dir / "node_exporter_targets.json"
Architecture
The observability stack is a standard self-hosted Prometheus/Grafana/Loki/Alertmanager deployment running inside the same Docker Compose project as the Manage application:
- Prometheus pulls metrics from:
- The Manage FastAPI backend via
/metrics(job="manage-backend"). - The local Docker host via
node-exporter(job="node-exporter"). - Remote machines that have Node Exporter enabled through Manage settings (
job="node-exporter-remote"), discovered through a file-SD JSON file generated by the backend. - The observability services themselves: Loki, Alertmanager, Grafana, and Prometheus self-scrape.
- The Manage FastAPI backend via
- Grafana visualizes metrics and logs; it is configured for OAuth login through Authentik and is embedded in the Manage React UI via iframes.
- Loki stores logs; retention is 30 days.
- Alloy (Grafana Alloy) collects Docker container logs by mounting the Docker socket and the container log directory, then pushes them to Loki.
- Alertmanager routes alerts by severity (critical vs warning) and delivers email notifications (and optionally a webhook back to the backend).
The backend bridges the stack with the application:
- It exposes
/metrics(unauthenticated, along with/api/healthand/api/version). - On startup it writes
${PROMETHEUS_FILE_SD_DIR}/node_exporter_targets.jsonbased on enabled SSH machines in the settings store. - It provides proxy endpoints (
/api/monitoring/alerts,/api/monitoring/alertmanager-status,/api/monitoring/prometheus-targets) consumed by the frontend.
Start Here
Open monitoring/prometheus/prometheus.yml first to understand what is scraped and how the backend is wired, then read backend/src/media_library_viewer_api/observability.py to see the metric names and labels. For environment requirements, read .env.example.
Supervisor coordination
Not needed — this is a read-only scouting summary.
Monitoring/Observability Setup Summary
1. Observability services defined in Compose
Both docker-compose.yml and docker-compose.dev.yml define only the backend and frontend. The observability services (Prometheus, Loki, Grafana, Alertmanager, Alloy, Node Exporter) were extracted to the standalone docker-compose.observability.yml example stack and are no longer deployed by Manage. Summary of what remains in the app compose files:
| Service | Image | Internal endpoint | Purpose |
|---|---|---|---|
prometheus |
prom/prometheus:v2.55.1 |
http://prometheus:9090 |
Metrics TSDB and alert evaluator |
loki |
grafana/loki:3.1.1 |
http://loki:3100 |
Log aggregation |
alloy |
grafana/alloy:v1.5.0 |
http://alloy:12345 |
Docker log collection agent |
grafana |
grafana/grafana:11.3.1 |
http://grafana:3000 |
Dashboards and visualization |
alertmanager |
prom/alertmanager:v0.27.0 |
http://alertmanager:9093 |
Alert routing/delivery |
node-exporter |
prom/node-exporter:v1.8.2 |
http://node-exporter:9100 |
Host metrics for the Docker host |
backend |
Build from backend/Dockerfile |
http://backend:8000 |
FastAPI app exposing /metrics |
Differences:
- Production (
docker-compose.yml): services attach to an externalwebnetwork for Traefik, useexposeinstead of host ports for most services, and require OIDC/auth variables. - Development (
docker-compose.dev.yml): Prometheus/Grafana/Loki/Alertmanager/Node Exporter are published on host ports9090,3000,3100,9093,9100; auth is disabled (AUTH_ENABLED=false).
2. Required environment variables
From .env.example and the Compose files, the variables relevant to the observability stack are:
Backend / metrics
PROMETHEUS_ENABLED– enable metrics endpoint (set to"true"in both compose files).PROMETHEUS_FILE_SD_DIR– directory where the backend writesnode_exporter_targets.json(default/app/backend/.cache/prometheus-file-sd).ALERTMANAGER_URL– backend proxy target (defaulthttp://alertmanager:9093).ALERTMANAGER_WEBHOOK_URL– optional webhook receiver for Alertmanager.BACKEND_CACHE_DIR– host directory mounted into backend and Prometheus for file-SD.
Grafana
GRAFANA_APP_HOST– public hostname for Grafana (production; required).GRAFANA_APP_PORT– defaults to3000.GRAFANA_APP_NAME– defaults tografana.GRAFANA_ADMIN_USER/GRAFANA_ADMIN_PASSWORD– local admin credentials.GF_AUTH_GENERIC_OAUTH_CLIENT_IDGF_AUTH_GENERIC_OAUTH_CLIENT_SECRETGF_AUTH_GENERIC_OAUTH_AUTH_URLGF_AUTH_GENERIC_OAUTH_TOKEN_URLGF_AUTH_GENERIC_OAUTH_API_URL
Alertmanager
SMTP_HOST/SMTP_PORTSMTP_USERNAME/SMTP_PASSWORDSMTP_FROM_ADDRESSALERT_EMAIL_TO
Traefik / network (production)
BACKEND_APP_HOST/FRONTEND_APP_HOST/GRAFANA_APP_HOSTCERT_RESOLVER– e.g.letsencrypt
General
LOG_LEVEL/LOG_FORMAT– also passed to Grafana and backend.
3. Monitoring config files
| File | Purpose |
|---|---|
monitoring/prometheus/prometheus.yml |
Scrape jobs: backend /metrics, local node-exporter, remote node-exporter via file-SD, Loki, Alertmanager, Grafana, and self-scrape. |
monitoring/prometheus/rules/backup_alerts.yml |
Alert rules: BackupJobFailed, BackupJobStuck, PrometheusTargetMissing, AlertmanagerDown, GrafanaDown. |
monitoring/alertmanager/alertmanager.yml |
Routes alerts by severity, sends email to ALERT_EMAIL_TO, optional webhook to backend, and inhibits warnings when a critical alert fires. |
monitoring/grafana/grafana.ini |
Enables iframe embedding, OAuth via Authentik, role mapping from groups, and defaults users to Viewer. |
monitoring/alloy/config.alloy |
Discovers Docker containers, relabels container/stream labels, and writes logs to http://loki:3100/loki/api/v1/push. |
monitoring/loki/loki.yml |
Single-node Loki with filesystem storage, tsdb index, 30-day retention (720h). |
4. Backend metrics and Prometheus scraping
- The backend exposes Prometheus metrics at
/metricson port8000. - The endpoint is unauthenticated (bypassed in
enforce_jwt_auth). - Prometheus scrapes it as
job="manage-backend"withscrape_interval: 15s. - Key application metrics include:
manage_api_requests_total{method, path, status_code}manage_api_request_duration_seconds{method, path}manage_ssh_commands_total{machine_id, action, status}manage_media_index_builds_total{status}manage_backup_runs_total{job_name, status}manage_backup_runs_last_success_timestamp{job_name}manage_mail_queue_messages_total{status}
Remote Node Exporter targets are not static: the backend reads machine settings from SQLite and writes ${PROMETHEUS_FILE_SD_DIR}/node_exporter_targets.json. Prometheus reloads this file every 30 seconds via file_sd_configs.
5. Setup steps and gotchas
- Manage's own compose stack does not include observability services. To run a full local stack, bring up the app and the standalone observability example separately:
- Production:
docker compose -f docker-compose.yml up --build - Development:
docker compose -f docker-compose.dev.yml up --build
- Production:
- Production requires the external
webnetwork and Traefik already configured;docker-compose.dev.ymldoes not use Traefik and binds ports directly. - Export/copy
.env.exampleto.envand fill required values (OIDC_ISSUER_URL,OIDC_AUDIENCE,BACKEND_APP_HOST,FRONTEND_APP_HOST,GRAFANA_APP_HOST,CERT_RESOLVER, SMTP credentials, Grafana OAuth secrets). BACKEND_CACHE_DIRis shared between the backend and Prometheus so file-SD target updates are visible to Prometheus.- Alloy must run as
user: rootand mount/var/run/docker.sockand/var/lib/docker/containers; without these mounts it cannot collect Docker logs. - Node Exporter mounts the host root filesystem read-only (
/:/host:ro,rslave) to report host-level metrics; on production, this exposes the Docker host. - Grafana iframe embedding requires
allow_embedding = trueingrafana.iniand matching cookie settings; also ensure the reverse proxy/CSP permits embedding. - After changing Prometheus rules/config, trigger a reload with
curl -X POST http://localhost:9090/-/reload(production needs Traefik/network access). - Observability data is stored in named volumes:
prometheus_data,loki_data,grafana_data,alertmanager_data. Back them up as documented indocs/observability-runbooks.md. - Default retention is 30 days for both Prometheus TSDB and Loki logs.