# Adding monitoring for an app There are two shapes. Pick the one that matches the app: 1. **Native** — the app speaks Prometheus on a `/metrics` endpoint. Just point Prometheus at it. 2. **Sidecar** — the app has no native /metrics. Run an exporter container alongside the app; Prometheus scrapes the exporter. Both use the same plumbing (`monitoringToggleAppConfig` + `monitoringRefreshAll`) and the same on-disk layout under `containers//resources/monitoring/`. The only thing that differs is what's inside the marker block in the app's compose. ## On-disk layout ``` containers// ├── .config # CFG__MONITORING=false (default off) ├── .sh # calls the two monitoring helpers ├── docker-compose.yml # marker block: env vars OR sidecar service └── resources/ └── monitoring/ ├── prometheus-scrape.yml # one or more scrape jobs └── grafana-dashboards/ └── .json # dashboard(s) to provision ``` When `CFG__MONITORING=true`, `monitoringRefreshAll` (in `scripts/network/monitoring/monitoring.sh`) copies the scrape fragment into `prometheus/scrape.d/.yml` and the dashboard(s) into `grafana/provisioning/dashboards/libreportal/`, then SIGHUPs Prometheus and restarts Grafana. ## The marker block Anywhere in the compose you put a block like this: ```yaml # >>> libreportal-monitoring >>> # …lines that should be uncommented when MONITORING=true… # <<< libreportal-monitoring <<< ``` `monitoringToggleAppConfig ` strips one leading `#` from every non-marker line in the range when monitoring is enabled, and re-adds it when disabled. The marker lines themselves are never touched. ## Native pattern For apps with built-in /metrics (e.g. traefik, gitea, headscale, grafana): - If the metrics endpoint is gated behind config (e.g. gitea needs `GITEA__metrics__ENABLED=true`), put the env vars / config lines inside a marker block in the compose or app config file. - Scrape fragment targets the app service directly: `targets: ['-service:']`. - If the endpoint requires a bearer token, generate it via the `CFG__METRICS_TOKEN=RANDOMIZEDPASSWORD` mechanism, mirror it into the scrape fragment in `.sh` with a sed substitution against a `_METRICS_TOKEN_PLACEHOLDER` (see `gitea.sh` for the pattern). ## Sidecar pattern For apps without native /metrics (pihole, adguard, wireguard, nextcloud, unbound, gluetun, vaultwarden, ollama): Add the exporter as a sibling service inside the marker block: ```yaml services: -service: … # >>> libreportal-monitoring >>> #-exporter: # container_name: -exporter # image: /-exporter: # restart: unless-stopped # environment: # - …pull credentials from existing CFG__* via #LIBREPORTAL tags… # networks: # DOCKER_NETWORK_DATA: # <<< libreportal-monitoring <<< ``` Rules: - **Service name**: `-exporter`. Prometheus scrapes `-exporter:`. No host port mapping — the metrics endpoint stays on the docker network only. - **Network**: attach to the same `DOCKER_NETWORK_DATA` as the main service. No explicit IP needed; service-name DNS works. - **Port**: add a `CFG__PORT_="-exporter|metrics|:|disabled|tcp|false|false|false|Metrics Exporter (sidecar, docker-network only)|"` entry next to the app's other port slots. `access=disabled` keeps it off the host. Reference the port from the marker block via `PORT_INTERNAL_DATA_` with the matching `#LIBREPORTAL|PORT_INTERNAL_TAG_|...` annotation — single source of truth, plus the port shows up in the webui port list. - **Scrape fragment port**: the scrape.yml carries the same `#LIBREPORTAL||` annotation as compose lines do — e.g. `- targets: ['-exporter:PORT_INTERNAL_DATA_6'] #LIBREPORTAL|PORT_INTERNAL_TAG_6|PORT_INTERNAL_DATA_6`. During `monitoringRefreshPrometheus`, the copied fragment is run through the framework's `tagsManagerUpdateUniversalTag` once per `PORT_INTERNAL_TAG_` found in the deployed compose, using the compose's resolved value. Idempotent (annotation records the resolved value, second run is a no-op), namespaced per app (each iteration passes that app's own compose), no bespoke sed. - **Freshness**: `monitoringResolveScrapeTags` reads from the **deployed** compose, which reflects the app's last install/reinstall, not the latest CFG. This is intentional — Prometheus scrapes the running container, which matches the deployed compose. To flush a `CFG__PORT_` change through to the scrape config, reinstall that app (which re-resolves its compose and then triggers a refresh at the end of its install). If a refresh ever runs against an unresolved annotation (placeholder text still in the value slot), the helper skips that tag and logs an `isNotice` naming the app + tag, plus an `isError` listing any leftover `PORT_INTERNAL_DATA_` in the generated scrape file — so drift surfaces in the install log instead of as a silent Prometheus scrape failure. - **Credentials**: pull from the app's existing `CFG__*` vars via the `#LIBREPORTAL||` substitution mechanism. Do not introduce a new admin/user just for the exporter — reuse the one the app already has. - **No new CFG vars** unless the exporter genuinely needs something the app config doesn't already expose. If you do add one (e.g. an exporter-specific API token), use the `RANDOMIZEDPASSWORD` convention so it auto-generates. - **Token sync**: if the scrape config needs a credential value (e.g. a bearer token), put a `_METRICS_TOKEN_PLACEHOLDER` literal in the scrape fragment and `sed`-replace it from the install script when `monitoringAppEnabled "$app_name"` is true. - **Shared network namespace** (rare): some exporters need to share the main service's network namespace (e.g. wireguard exporter needs to see the WG interface). Use `network_mode: "service:-service"` inside the marker block. Prometheus still reaches it via `-service:` since the namespace is shared. ## Install-script hooks Two calls go into `installXxx()`'s install branch: ```bash # After dockerComposeSetupFile, before dockerComposeUpdateAndStartApp: monitoringToggleAppConfig "$app_name" "docker-compose.yml" # If the scrape fragment has a placeholder that needs the runtime credential: if monitoringAppEnabled "$app_name"; then if [[ -n "$CFG__" ]]; then sudo sed -i "s|__PLACEHOLDER|${CFG__}|g" \ "$containers_dir$app_name/resources/monitoring/prometheus-scrape.yml" fi fi # Near the end of the install, after the app and exporter are running: monitoringRefreshAll ``` `monitoringRefreshAll` is self-correcting: it gathers from every app with `CFG__MONITORING=true`, drops everything else from `scrape.d/` and `dashboards/`, then SIGHUPs Prometheus + restarts Grafana. Safe to call from any app's install regardless of which apps have monitoring on. ## Dashboard Drop a Grafana dashboard JSON in `resources/monitoring/grafana-dashboards/.json`. The datasource UID is `Prometheus` (provisioned by `monitoringRefreshGrafana`). Keep it minimal — a few stat panels for headline numbers and one timeseries for activity is enough for v1. Users can swap in a community dashboard later via Grafana's import flow. ## Reference implementations - Native, no auth: `containers/traefik/` (compose-level config block toggle) - Native, bearer token: `containers/gitea/` (token generated + synced into scrape fragment) - Native, self-monitoring: `containers/grafana/`, `containers/prometheus/` - Sidecar, env-only auth: `containers/pihole/`, `containers/adguard/`, `containers/vaultwarden/`, `containers/ollama/`, `containers/gluetun/` - Sidecar, shared netns: `containers/wireguard/` - Sidecar, requires app-side config change: `containers/unbound/` (enables remote-control in unbound.conf via a second marker block)