# overleaf-cep monitoring (Prometheus + Grafana + exporters) Stack in `/data_1/docker/compose` (modeled on the OlliTeX toolkit compose from `/data_1/image_mining/the_diff/overleaf/toolkit/compose.yaml`), all of it served through the existing nginx facade. Container/service names are unprefixed (monitoring-only, added 2026-10-27; renamed from `ollitex-*` on 2026-10-27). ## Services (all on the shared external network `overleaf-network`) | service | role | |--------------------------------|----------------------------------------------------------------------| | `prometheus` | TSDB, 15d retention, `127.0.0.1:9090` (SSH-tunnel/admin only) | | `grafana` | UI, **no host port** — served under `https:///grafana/` | | `mongodb-exporter` | percona exporter for the stack Mongo (sharelatex db) | | `mongodb-exporter-probe` | busybox spider: /metrics must answer (exporter image has no shell) | | `redis-exporter` | oliver006 exporter for the stack Redis | | `redis-exporter-probe` | busybox spider for the redis exporter /metrics | | `mongo-probe` | mongosh ping — protocol-level Mongo liveness (stack's own image) | | `node-exporter` | host CPU/mem/fs signals (reads host `/`, `/proc`, `/sys` ro) | | `instance-sensor` | **PR yu-i-i/overleaf-cep#178 sensors as live gauges** (see below) | The repo's own helpers `down_grafana.sh` / `exec_grafana.sh` now address this `grafana` service directly. ## What is measured 1. **Overleaf app sensors** — the sharelatex image ships `@overleaf/metrics` on the web process (`overleaf:3000/metrics`, `app="web-api"`): `num_active_users` (live editor users), `timer_http_request` (summaries with quantiles), `mongo_command_started`, `nodejs_eventloop_lag_*`, GC/heap, `security_http_auth_check_credentials`, `editor_join_project`, `tpds_add_doc`, `lock_mongoTransaction_*`. (In-image side processes like compile `:4000` bind 127.0.0.1 inside their container — not scrapable.) 2. **Instance stats — PR #178, integrated into Prometheus/Grafana instead of the admin page** (`instance-sensor`, port 9464): - users: total + active(24h) — optional standard/guest split via `INSTANCE_STATS_SEGMENTATION=true` (PR#176 rule: `'guest-user'` in `users.adminRoles`) - projects: total + active(24h, `lastUpdated`) - files: total docs - storage: mongodb `dbStats.totalSize`, filestore `du` of `seaweedfs-volume/data` (ro), redis RAM (`used_memory`) + redis disk (AOF size) Computed fresh on every scrape (no hourly Mongo snapshot state). 3. **MongoDB** — connections, per-db data/storage size, oplog free space. 4. **Redis** — memory, clients, command rate, keyspace hit/miss, AOF. 5. **Host** — CPU, memory, root fs, load, uptime, network. 6. **Filestore (SeaweedFS master `:9327`, native prometheus)** — leader, under-replicated volumes, in-flight uploads/downloads. 7. **Scrape self-health** — `up` per job + the monitoring stack itself. ## URLs / access - Grafana: `https://psintern.neuro.uni-bremen.de/grafana/` login: `admin` / (see top section of `.env`, `GRAFANA_ADMIN_USER/PASSWORD`; unset → Grafana fallback `admin`/`admin`) - Prometheus: `http://127.0.0.1:9090` (local only — SSH tunnel from a laptop) - Dashboard: folder **Overleaf** → **Overleaf — instance overview** (file-provisioned; pinned datasource uid `prometheus`, `grafana/provisioning/datasources/prometheus.yml`) ## Git hygiene Data directories are git-ignored per folder: `grafana/.gitignore` → `grafana-data`, `prometheus/.gitignore` → `prometheus-data` (verified with `git check-ignore`). `.env` itself is in the root `.gitignore`. Keep new runtime dirs out of git by adding one line to the folder `.gitignore` as you add them. ## SMTP Grafana reuses the stack's Overleaf relay — **no separate Grafana mail vars**; `grafana/compose.yaml` reads the existing Overleaf settings as a single source of truth: | Grafana setting | taken from | |---|--------------------------------| | `GF_SMTP_ENABLED=true` | fixed | | `GF_SMTP_HOST` | `OVERLEAF_EMAIL_SMTP_HOST`:`OVERLEAF_EMAIL_SMTP_PORT` | | `GF_SMTP_USER` | `OVERLEAF_EMAIL_SMTP_USER` | | `GF_SMTP_PASSWORD` | `OVERLEAF_EMAIL_PASSWORD` | | `GF_SMTP_FROM_ADDRESS` | `OVERLEAF_EMAIL_FROM_ADDRESS` | | `GF_SMTP_FROM_NAME` | `"Overleaf Monitoring"` (display label only) | | `GF_SMTP_SKIP_VERIFY=false` | fixed | Used for Grafana's reset-password mail (verified live: sent via the relay, server responded `200 {"message":"Email sent"}`). Grafana 11's alerting has no built-in SMTP contact point — SMTP covers account e-mail; alert routing would need integrations (webhook/Slack/…). **Gotcha (learned 2026-10-27): the admin account's email must be set.** Grafana creates the admin user with email `admin@localhost`; the reset-mail endpoint `POST /api/user/password/send-reset-email {"userOrEmail": …}` matches users by that email and answers `200 "Email sent"` *even when nobody matches* (anti user-enumeration) — so it silently sends nothing. Fixed on two layers: 1. **self-healing on fresh data**: `GF_SECURITY_ADMIN_EMAIL` in `grafana/compose.yaml` = `${OVERLEAF_ADMIN_EMAIL:-overleaf@uni-bremen.de}` — honored only when Grafana *creates* the admin (i.e. after a grafana-data wipe); it never rewrites an existing user row. 2. **existing install**: `update user set email='overleaf@uni-bremen.de' where login='admin'` in `grafana-data/grafana.db` (grafana stopped). Verify with `select email from user where login='admin'`. The working relay itself was proven with a raw `smtplib.SMTP_SSL` test (TLS-465 + auth + RCPT all OK). **After a grafana-data wipe** (fresh admin creation): login = `GRAFANA_ADMIN_USER`/`GRAFANA_ADMIN_PASSWORD` from `.env`; e-mail = `GF_SECURITY_ADMIN_EMAIL` (default `overleaf@uni-bremen.de`); dashboards re-provision automatically (file provider); admin UI settings/annotations are lost. To disable SMTP: set `GF_SMTP_ENABLED: "false"` in `grafana/compose.yaml` + `docker compose up -d grafana`. ## Ops notes / gotchas (learned while bringing this up) - **percona/mongodb_exporter 0.43 ships most collectors OFF by default** (a default run answers only `mongodb_up` + go/process metrics). Working flags for this stack are in `mongodb-exporter/compose.yaml`: `--collector.diagnosticdata --collector.dbstats --collector.dbstatsfreestorage --collector.replicasetstatus` (verified against this mongo 9.0). - Bind-mount ownership: `prometheus/prometheus-data` must be owned by uid **65534**, `grafana/grafana-data` by uid **472** (both images run unprivileged; docker creates fresh bind-mount dirs as root). Pre-create + `chown` if the dirs are ever deleted. - Grafana subpath: `GF_SERVER_SERVE_FROM_SUB_PATH=true` + `GF_SERVER_ROOT_URL=https:///grafana/` — nginx serves `/grafana/` **without stripping** the prefix (see `nginx/nginx.conf`). After editing `nginx.conf`: `docker exec nginx nginx -s reload` (conf is bind-mounted, not baked in). - Grafana 11 login via API: `POST /grafana/login` with **JSON** `{user, password}` (the old `POST /api/login` is gone; form-encoded POSTs return `form-auth.invalid`). - Healthchecks: compose interpolates `${VAR}` inside healthcheck strings at load time — `MONGO_HOSTNAME`/`MONGO_PORT` therefore live in `.env`. - The `instance-sensor` image is a local build: `docker build -t overleaf-instance-sensor:local ./instance-sensor`. ## Adding/retiring Everything opt-in per service; remove a block from the root `compose.yaml` and `docker compose down` to retire one. Image pins: `.env` (commented block in the monitoring section).