Statistics with Grafana/Prometheus with sensor based on Overleaf instance statistics- #178

This commit is contained in:
David Rotermund
2026-10-27 13:43:32 +00:00
parent 6f2cf1c060
commit 05b1b04513
57 changed files with 3456 additions and 22 deletions
+141
View File
@@ -0,0 +1,141 @@
# 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://<site>/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://<site>/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 <svc> down` to retire one. Image pins: `.env`
(commented block in the monitoring section).
@@ -87,6 +87,83 @@ services:
wakapi: wakapi:
condition: service_healthy condition: service_healthy
mongo-probe:
extends:
file: ./mongo-probe/compose.yaml
service: mongo-probe
depends_on:
mongo:
condition: service_healthy
mongodb-exporter:
extends:
file: ./mongodb-exporter/compose.yaml
service: mongodb-exporter
depends_on:
mongo:
condition: service_healthy
mongodb-exporter-probe:
extends:
file: ./mongodb-exporter-probe/compose.yaml
service: mongodb-exporter-probe
depends_on:
mongodb-exporter:
condition: service_healthy
redis-exporter:
extends:
file: ./redis-exporter/compose.yaml
service: redis-exporter
depends_on:
redis:
condition: service_healthy
redis-exporter-probe:
extends:
file: ./redis-exporter-probe/compose.yaml
service: redis-exporter-probe
depends_on:
redis-exporter:
condition: service_healthy
instance-sensor:
extends:
file: ./instance-sensor/compose.yaml
service: instance-sensor
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_healthy
node-exporter:
extends:
file: ./node-exporter/compose.yaml
service: node-exporter
prometheus:
extends:
file: ./prometheus/compose.yaml
service: prometheus
depends_on:
mongodb-exporter:
condition: service_healthy
redis-exporter:
condition: service_healthy
instance-sensor:
condition: service_healthy
node-exporter:
condition: service_healthy
grafana:
extends:
file: ./grafana/compose.yaml
service: grafana
depends_on:
prometheus:
condition: service_healthy
nginx: nginx:
extends: extends:
file: ./nginx/compose.yaml file: ./nginx/compose.yaml
@@ -96,6 +173,8 @@ services:
condition: service_healthy condition: service_healthy
wakapi-probe: wakapi-probe:
condition: service_healthy condition: service_healthy
grafana:
condition: service_healthy
networks: networks:
overleaf-network: overleaf-network:
@@ -0,0 +1,2 @@
docker compose down grafana
@@ -0,0 +1,2 @@
docker compose down instance-sensor
@@ -0,0 +1,2 @@
docker compose down mongo-probe
@@ -0,0 +1,2 @@
docker compose down mongodb-exporter-probe
@@ -0,0 +1,2 @@
docker compose down mongodb-exporter
@@ -0,0 +1,2 @@
docker compose down node-exporter
@@ -0,0 +1,2 @@
docker compose down prometheus
@@ -0,0 +1,2 @@
docker compose down redis-exporter-probe
@@ -0,0 +1,2 @@
docker compose down redis-exporter
+68 -22
View File
@@ -13,83 +13,129 @@
# from .env into here. # from .env into here.
# ============================================================================ # ============================================================================
# Image versions ##################
# Image versions #
##################
OVERLEAF_VERSION=ext-ce-v6.3.1 OVERLEAF_VERSION=ext-ce-v6.3.1
MONGO_VERSION=9.0 MONGO_VERSION=9.0
REDIS_VERSION=8.10-alpine REDIS_VERSION=8.10-alpine
GIT_BRIDE_VERSION=6.3.1 GIT_BRIDE_VERSION=6.3.1
# Global ######################
# Overleaf Global I. #
######################
# You need here the correct path (the compose.yaml is in /docker/compose_cep/overleaf) # You need here the correct path (the compose.yaml is in /docker/compose_cep/overleaf)
COMPOSE_DIRECTORY=/data_1/docker/compose COMPOSE_DIRECTORY=/data_1/docker/compose
TEX_LIVE_REPO=git.fb1.uni-bremen.de/davrot TEX_LIVE_REPO=git.fb1.uni-bremen.de/davrot
OVERLEAF_LEFT_FOOTER='[]' OVERLEAF_LEFT_FOOTER='[]'
OVERLEAF_LOGIN_SUPPORT_TEXT="<img src=\"ub/logo.svg\" height=\"250\"></br><a href=\"https://sso.fb1.uni-bremen.de\"> University of Bremen Overleaf Wiki </a>" OVERLEAF_LOGIN_SUPPORT_TEXT="<img src=\"ub/logo.svg\" height=\"250\"></br><a href=\"https://sso.fb1.uni-bremen.de\"> University of Bremen Overleaf Wiki </a>"
COMPILES_HOST_DIR=${COMPOSE_DIRECTORY}/overleaf/data/data/compiles COMPILES_HOST_DIR=${COMPOSE_DIRECTORY}/overleaf/data/data/compiles
COMPILES_HOST_DIR_CACHE=${COMPOSE_DIRECTORY}/overleaf/data/data/cache COMPILES_HOST_DIR_CACHE=${COMPOSE_DIRECTORY}/overleaf/data/data/cache
CONVERSION_SANDBOX_HOST_DIR=${COMPOSE_DIRECTORY}/overleaf/data/tmp/conversions CONVERSION_SANDBOX_HOST_DIR=${COMPOSE_DIRECTORY}/overleaf/data/tmp/conversions
OVERLEAF_OIDC_IS_ADMIN_FIELD_VALUE=davrot@uni-bremen.de OVERLEAF_OIDC_IS_ADMIN_FIELD_VALUE=davrot@uni-bremen.de
# Nginx SSL ###########
SSL_KEY_FILE=/etc/letsencrypt/live/psintern.neuro.uni-bremen.de/privkey.pem # Grafana #
SSL_CERT_FILE=/etc/letsencrypt/live/psintern.neuro.uni-bremen.de/fullchain.pem ###########
GRAFANA_ADMIN_USER=<ADMINUSER>
GRAFANA_ADMIN_PASSWORD=<PASSWORD>
# admin account email (only applies when the admin user is first created —
# i.e. on a fresh grafana-data; Grafana has no API to retrofit it later)
GF_SECURITY_ADMIN_EMAIL=<Reuse OVERLEAF_ADMIN_EMAIL>
# where the UI is reachable (nginx subpath; default fits this stack)
GRAFANA_ROOT_URL=https://<DOMAIN>/grafana/
#S3 Buckets # Monitoring image pins (bump + autoupdate on upgrade)
GRAFANA_IMAGE=grafana/grafana-oss:11.6.0
PROMETHEUS_IMAGE=prom/prometheus:v2.53.5
NODE_EXPORTER_IMAGE=prom/node-exporter:v1.8.2
# Grafana SMTP: no extra vars — grafana/compose.yaml reuses the Overleaf
# settings below (OVERLEAF_EMAIL_SMTP_HOST/PORT/USER, OVERLEAF_EMAIL_PASSWORD,
# OVERLEAF_EMAIL_FROM_ADDRESS) as a single source of truth.
# instance-sensor (PR yu-i-i/overleaf-cep#178 sensors as live gauges):
# split active users into standard/guest (PR#176 rule)
INSTANCE_STATS_SEGMENTATION=false
#############
# Nginx SSL #
#############
# If you use certbot
SSL_KEY_FILE=/etc/letsencrypt/live/<DOMAINNAME>/privkey.pem
SSL_CERT_FILE=/etc/letsencrypt/live/<DOMAINNAME>/fullchain.pem
##############
# S3 Buckets #
##############
# openssl rand -base64 30 | tr -dc 'a-zA-Z0-9' | head -c 20; echo # openssl rand -base64 30 | tr -dc 'a-zA-Z0-9' | head -c 20; echo
AWS_ACCESS_KEY_ID=<AWS_ACCESS_KEY_ID> AWS_ACCESS_KEY_ID=<AWS_ACCESS_KEY_ID>
# openssl rand -base64 60 | tr -dc 'a-zA-Z0-9' | head -c 40; echo # openssl rand -base64 60 | tr -dc 'a-zA-Z0-9' | head -c 40; echo
AWS_SECRET_ACCESS_KEY=<AWS_SECRET_ACCESS_KEY> AWS_SECRET_ACCESS_KEY=<AWS_SECRET_ACCESS_KEY>
# Zotero ##########
# Zotero #
##########
# (credentials from your Zotero account: Settings -> Advanced -> Credentials) # (credentials from your Zotero account: Settings -> Advanced -> Credentials)
ZOTERO_CLIENT_KEY=<ZOTERO_CLIENT_KEY> ZOTERO_CLIENT_KEY=<ZOTERO_CLIENT_KEY>
ZOTERO_CLIENT_SECRET=<ZOTERO_CLIENT_SECRET> ZOTERO_CLIENT_SECRET=<ZOTERO_CLIENT_SECRET>
# Github ##########
# Github #
##########
# (from your GitHub App settings) # (from your GitHub App settings)
GITHUB_SYNC_CLIENT_ID=<GITHUB_SYNC_CLIENT_ID> GITHUB_SYNC_CLIENT_ID=<GITHUB_SYNC_CLIENT_ID>
GITHUB_SYNC_CLIENT_SECRET=<GITHUB_SYNC_CLIENT_SECRET> GITHUB_SYNC_CLIENT_SECRET=<GITHUB_SYNC_CLIENT_SECRET>
###################
# Overleaf SECRET #
###################
# openssl rand -hex 32; echo # openssl rand -hex 32; echo
OVERLEAF_INVITE_TOKEN_SECRET=<OVERLEAF_INVITE_TOKEN_SECRET> OVERLEAF_INVITE_TOKEN_SECRET=<OVERLEAF_INVITE_TOKEN_SECRET>
#####################
# Overleaf SSO OIDC #
#####################
# Keycloak OpenID Connect Configuration # Keycloak OpenID Connect Configuration
OVERLEAF_OIDC_ISSUER=https://sso.fb1.uni-bremen.de/sso/realms/master OVERLEAF_OIDC_ISSUER=https://<SERVERURL>/sso/realms/master
# (from your SSO realm / client registration) # (from your SSO realm / client registration)
OVERLEAF_OIDC_CLIENT_ID=<OVERLEAF_OIDC_CLIENT_ID> OVERLEAF_OIDC_CLIENT_ID=<OVERLEAF_OIDC_CLIENT_ID>
OVERLEAF_OIDC_CLIENT_SECRET=<OVERLEAF_OIDC_CLIENT_SECRET> OVERLEAF_OIDC_CLIENT_SECRET=<OVERLEAF_OIDC_CLIENT_SECRET>
OVERLEAF_OIDC_IDENTITY_SERVICE_NAME='Log in with SSO FB1 Uni Bremen' OVERLEAF_OIDC_IDENTITY_SERVICE_NAME='Log in with SSO FB1 Uni Bremen'
##################
# Overleaf eMail #
##################
# eMail Account Configuration # eMail Account Configuration
# (SMTP password for the user in OVERLEAF_EMAIL_SMTP_USER) # (SMTP password for the user in OVERLEAF_EMAIL_SMTP_USER)
OVERLEAF_EMAIL_PASSWORD=<OVERLEAF_EMAIL_PASSWORD> OVERLEAF_EMAIL_PASSWORD=<SMTP PASSWORD>
OVERLEAF_EMAIL_FROM_ADDRESS=overleaf@uni-bremen.de OVERLEAF_EMAIL_FROM_ADDRESS=<EMAIL ADDRESS>
OVERLEAF_EMAIL_SMTP_HOST=smtp.uni-bremen.de OVERLEAF_EMAIL_SMTP_HOST=<SMTP SERVER>
OVERLEAF_EMAIL_SMTP_PORT=465 OVERLEAF_EMAIL_SMTP_PORT=465
OVERLEAF_EMAIL_SMTP_SECURE=true OVERLEAF_EMAIL_SMTP_SECURE=true
OVERLEAF_EMAIL_SMTP_USER=overleaf OVERLEAF_EMAIL_SMTP_USER=<SMTP USER>
OVERLEAF_EMAIL_SMTP_IGNORE_TLS=false OVERLEAF_EMAIL_SMTP_IGNORE_TLS=false
#######################
# Overleaf Global II. #
#######################
# Other Overleaf Configurations Configuration # Other Overleaf Configurations Configuration
OVERLEAF_SITE_URL=https://psintern.neuro.uni-bremen.de OVERLEAF_SITE_URL=https://<URL>
OVERLEAF_ADMIN_EMAIL=overleaf@uni-bremen.de OVERLEAF_ADMIN_EMAIL=<Reuse OVERLEAF_EMAIL_FROM_ADDRESS>
OVERLEAF_APP_NAME="University of Bremen -- Overleaf" OVERLEAF_APP_NAME="Overleaf"
OVERLEAF_NAV_TITLE="University of Bremen -- Overleaf" OVERLEAF_NAV_TITLE="Overleaf"
OVERLEAF_CUSTOM_EMAIL_FOOTER="University of Bremen -- Overleaf" OVERLEAF_CUSTOM_EMAIL_FOOTER="Overleaf"
########## ##########
# Wakapi # # Wakapi #
########## ##########
# openssl rand -base64 30 | tr -dc "a-zA-Z0-9" | head -c 24; echo # openssl rand -base64 30 | tr -dc "a-zA-Z0-9" | head -c 24; echo
PG_PASSWORD=<PG_PASSWORD> PG_PASSWORD=<PG_PASSWORD>
# HOST port for the wakapi web UI + API (3000 by convention; use 3001 if taken) # HOST port for the wakapi web UI + API (3000 by convention; use 3001 if taken)
WAKA_API_PORT=3000 WAKA_API_PORT=3000
# image pins (bump + autoupdate on upgrade) # image pins (bump + autoupdate on upgrade)
WAKA_API_IMAGE=ghcr.io/muety/wakapi:2.18.2 WAKA_API_IMAGE=ghcr.io/muety/wakapi:2.18.2
POSTGRES_IMAGE=postgres:18-alpine POSTGRES_IMAGE=postgres:18-alpine
+1
View File
@@ -0,0 +1 @@
docker exec -it grafana sh
+1
View File
@@ -0,0 +1 @@
docker exec -it instance-sensor sh
+1
View File
@@ -0,0 +1 @@
docker exec -it mongo-probe sh
@@ -0,0 +1 @@
docker exec -it mongodb-exporter-probe sh
+1
View File
@@ -0,0 +1 @@
docker exec -it node-exporter sh
+1
View File
@@ -0,0 +1 @@
docker exec -it prometheus sh
@@ -0,0 +1 @@
docker exec -it redis-exporter-probe sh
+1
View File
@@ -0,0 +1 @@
docker exec -it redis-exporter sh
@@ -0,0 +1 @@
grafana-data
@@ -0,0 +1,54 @@
services:
grafana:
image: ${GRAFANA_IMAGE:-grafana/grafana-oss:11.6.0}
container_name: grafana
hostname: grafana
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
# Admin ACCOUNT EMAIL — reuses the Overleaf admin mailbox (line ~61 of
# .env). NOTE: like the password, this env is honored ONLY when Grafana
# CREATES the admin user (fresh grafana-data); it never rewrites an
# existing user row. Default (Grafana shipped default) is
# admin@localhost — see MONITORING.md for the reset-mail gotcha.
GF_SECURITY_ADMIN_EMAIL: ${OVERLEAF_ADMIN_EMAIL:-overleaf@uni-bremen.de}
GF_ANALYTICS_REPORTING_ENABLED: "false"
GF_USERS_SIGNUP_ENABLED: "false"
# SMTP — reuses the stack's Overleaf relay settings (OVERLEAF_EMAIL_* in
# .env / env.sample): single source of truth, no Grafana-specific mail vars.
# GF_SMTP_FROM_NAME is the only grafana-local piece (display label).
GF_SMTP_ENABLED: "true"
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"
GF_SMTP_SKIP_VERIFY: "false"
# AJ-3 style subpath serving (owner 2026-10-08, reference toolkit):
# URLs/route generation relative to the /grafana/ root
GF_SERVER_ROOT_URL: "${GRAFANA_ROOT_URL:-https://psintern.neuro.uni-bremen.de/grafana/}"
GF_SERVER_SERVE_FROM_SUB_PATH: "true"
# x-frame-options deny -> sameorigin (iframe embedding from the same
# origin stays possible; the fine-grained allowlist is the CSP below)
GF_SECURITY_ALLOW_EMBEDDING: "true"
GF_SECURITY_CSP_FRAME_ANCESTORS: "'self'"
volumes:
- ./provisioning:/etc/grafana/provisioning
- ./dashboards:/var/lib/grafana/dashboards
- ./grafana-data:/var/lib/grafana
expose:
- "3000" # nginx is the sole facade (see nginx/nginx.conf location /grafana/)
healthcheck:
# grafana-oss ships wget (verified on the reference stack)
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/health"]
interval: 15s
timeout: 5s
retries: 8
start_period: 30s
networks:
- overleaf-network
networks:
overleaf-network:
external: true
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
# file-based dashboard provider (shared/deterministic: no in-UI editing
# required; dashboards ship with this compose tree) — reference:
# build-images/grafana/provisioning/dashboards/ollitex.yml
apiVersion: 1
providers:
- name: overleaf
folder: Overleaf
type: file
disableDeletion: false
updateIntervalSeconds: 30
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: false
@@ -0,0 +1,12 @@
# D22-style shared datasource provisioning (reference: build-images/grafana):
# pinned uid so every dashboard JSON can hard-reference it; url points at
# this stack's prometheus container name.
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
@@ -0,0 +1,20 @@
# overleaf-instance-sensor — self-contained prometheus gauge exporter for
# the PR yu-i-i/overleaf-cep#178 instance-statistics sensors.
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package.json ./
RUN npm install --omit=dev --no-audit --no-fund
COPY sensor.mjs ./
RUN chown -R node:node /app
EXPOSE 9464
USER node
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 \
CMD wget -q --spider --timeout=3 http://127.0.0.1:9464/metrics || exit 1
ENTRYPOINT ["node", "sensor.mjs"]
@@ -0,0 +1,52 @@
# instance-sensor — the overleaf-cep "instance statistics" sensors,
# exposed LIVE as Prometheus gauges (scrape-on-read) instead of the
# hourly-cron-Mongo-snapshots of PR yu-i-i/overleaf-cep#178. Same sensor
# definitions, prometheus-native delivery:
#
# users : total + active (last 24h), split standard/guest when
# INSTANCE_STATS_SEGMENTATION != false (guest = user with
# 'guest-user' in adminRoles — the PR#176/PR#178 definition)
# projects : total + active (last 24h, projects.lastUpdated)
# files : total documents (docs collection)
# storage : mongodb (dbStats.totalSize), filestore (du of the SeaweedFS
# data dir — the S3 filestore content), redis RAM (used_memory)
# + redis disk (AOF current size)
#
# Self-contained node image (mongodb + ioredis drivers) — does NOT depend on
# the overleaf image layout. Read-only on its inputs.
services:
instance-sensor:
build:
context: .
dockerfile: Dockerfile
image: ${INSTANCE_SENSOR_IMAGE:-overleaf-instance-sensor:local}
container_name: instance-sensor
hostname: instance-sensor
restart: unless-stopped
environment:
MONGO_URL: "mongodb://${MONGO_HOSTNAME:-mongo}:${MONGO_PORT:-27017}/${MONGODB_DATABASE:-sharelatex}"
REDIS_ADDR: "redis://${REDIS_HOSTNAME:-redis}:${REDIS_PORT:-6379}"
# du() of the SeaweedFS volume data dir, mounted read-only below
# (mirror of PR#178's du-based overleaf_storage)
FILESTORE_DATA_DIR: /hostdata/filestore
# 'standard'/'guest' split (PR#178/PR#176): 'false' (default, per PR)
# or 'true'
SEGMENTATION: ${INSTANCE_STATS_SEGMENTATION:-false}
ACTIVE_WINDOW_HOURS: "24"
PORT: "9464"
expose:
- "9464"
volumes:
- ${COMPOSE_DIRECTORY:-/data_1/docker/compose}/seaweedfs-volume/data:/hostdata/filestore:ro
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --timeout=5 http://127.0.0.1:9464/metrics >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
networks:
- overleaf-network
networks:
overleaf-network:
external: true
@@ -0,0 +1,12 @@
{
"name": "overleaf-instance-sensor",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Prometheus gauges for the overleaf-cep instance statistics (PR yu-i-i/overleaf-cep#178, live instead of hourly Mongo snapshots)",
"engines": { "node": ">=18" },
"dependencies": {
"ioredis": "^5.4.1",
"mongodb": "^6.10.0"
}
}
@@ -0,0 +1,214 @@
// overleaf-instance-sensor
// Live Prometheus gauges for the overleaf-cep instance statistics.
//
// Sensor definitions ported from PR yu-i-i/overleaf-cep#178
// (services/web/modules/instance-stats/.../collectInstanceStats.mjs):
// * users : total count + active (lastActive >= now-24h)
// * projects : total count + active (lastUpdated >= now-24h)
// * files : docs collection count
// * storage : mongodb dbStats.totalSize, overleaf filestore (du of the
// data dir), redis RAM (used_memory) + redis disk (aof size)
// * optional standard/guest split: guest = 'guest-user' in user.adminRoles
// (the PR#176 segmentation; PR#178 env INSTANCE_STATS_USER_SEGMENTATION_ENABLED)
//
// Instead of the PR's hourly snapshot docs in Mongo, we compute on every
// scrape (cheap counts on this scale; fresh values, no retention state).
// Each probe is isolated: a failing source emits its error counter but the
// rest of the endpoint still answers.
import http from 'node:http'
import { execFile } from 'node:child_process'
import { performance } from 'node:perf_hooks'
import { promisify } from 'node:util'
import { MongoClient } from 'mongodb'
import Redis from 'ioredis'
const execFileAsync = promisify(execFile)
const PORT = Number(process.env.PORT || 9464)
const MONGO_URL = process.env.MONGO_URL || 'mongodb://mongo:27017/sharelatex'
const REDIS_ADDR = process.env.REDIS_ADDR || 'redis://redis:6379'
const FILESTORE_DATA_DIR = process.env.FILESTORE_DATA_DIR || '/hostdata/filestore'
const SEGMENTATION = String(process.env.SEGMENTATION || 'false') === 'true'
const ACTIVE_WINDOW_MS = Number(process.env.ACTIVE_WINDOW_HOURS || 24) * 3600 * 1000
const GUEST_ADMIN_ROLE = 'guest-user' // PR#176/PR#178 guest definition
const errors = new Map() // name -> failed probe count
let lastSuccessSeconds = 0
const guestUserFilter = () => ({ adminRoles: { $in: [GUEST_ADMIN_ROLE] } })
async function duBytes(path) {
// byte-exact du of a directory (same semantics as PR#178's duBytes())
try {
const { stdout } = await execFileAsync('du', ['-sb', path])
const m = String(stdout).match(/(\d+)/)
return m ? Number(m[1]) : 0
} catch {
throw new Error(`du failed for ${path}`)
}
}
function parseRedisInfoInt(info, key) {
const m = String(info || '').match(new RegExp(`^${key}:(\\d+)`, 'm'))
return m ? Number(m[1]) : 0
}
async function collect() {
const nowMs = Date.now()
const oneAgo = new Date(nowMs - ACTIVE_WINDOW_MS)
const out = []
let mongoOk = true
try {
const client = new MongoClient(MONGO_URL, { serverSelectionTimeoutMS: 5000 })
await client.connect()
try {
const db = client.db() // db name comes from MONGO_URL
const users = db.collection('users')
const projects = db.collection('projects')
const docs = db.collection('docs')
const totalUsers = await users.countDocuments()
const activeUsers = await users.countDocuments({ lastActive: { $gte: oneAgo } })
out.push(
'# HELP overleaf_instance_users_total Total registered users (segment: all, plus standard/guest when segmentation is enabled).',
'# TYPE overleaf_instance_users_total gauge',
`overleaf_instance_users_total{segment="all"} ${totalUsers}`,
'# HELP overleaf_instance_users_active_24h Users active in the last 24 hours (lastActive >= now-24h).',
'# TYPE overleaf_instance_users_active_24h gauge',
`overleaf_instance_users_active_24h{segment="all"} ${activeUsers}`,
)
if (SEGMENTATION) {
const guestFilter = guestUserFilter()
const guestUsers = await users.countDocuments(guestFilter)
const activeGuest = await users.countDocuments({
...guestFilter,
lastActive: { $gte: oneAgo },
})
out.push(
`overleaf_instance_users_total{segment="standard"} ${totalUsers - guestUsers}`,
`overleaf_instance_users_total{segment="guest"} ${guestUsers}`,
`overleaf_instance_users_active_24h{segment="standard"} ${activeUsers - activeGuest}`,
`overleaf_instance_users_active_24h{segment="guest"} ${activeGuest}`,
)
}
const totalProjects = await projects.countDocuments()
const activeProjects = await projects.countDocuments({ lastUpdated: { $gte: oneAgo } })
const totalFiles = await docs.countDocuments()
out.push(
'# HELP overleaf_instance_projects_total Total projects.',
'# TYPE overleaf_instance_projects_total gauge',
`overleaf_instance_projects_total ${totalProjects}`,
'# HELP overleaf_instance_projects_active_24h Projects updated in the last 24 hours (lastUpdated >= now-24h).',
'# TYPE overleaf_instance_projects_active_24h gauge',
`overleaf_instance_projects_active_24h ${activeProjects}`,
'# HELP overleaf_instance_files_total Total documents (files) in the docs collection.',
'# TYPE overleaf_instance_files_total gauge',
`overleaf_instance_files_total ${totalFiles}`,
)
// PR#178: mongodb_storage = dbStats.totalSize (bytes)
let mongoBytes = 0
try {
const stats = await db.stats()
mongoBytes = Number(stats?.totalSize ?? 0)
} catch {
/* keep 0 */
}
out.push(
'# HELP overleaf_instance_storage_bytes Storage used, bytes. component: mongodb | filestore | redis_ram | redis_disk.',
'# TYPE overleaf_instance_storage_bytes gauge',
`overleaf_instance_storage_bytes{component="mongodb"} ${mongoBytes}`,
)
} finally {
await client.close()
}
} catch {
mongoOk = false
errors.set('mongo', (errors.get('mongo') || 0) + 1)
}
// filestore size (SeaweedFS data dir, mounted ro) --------------------------
let fileOk = true
try {
const bytes = await duBytes(FILESTORE_DATA_DIR)
out.push(`overleaf_instance_storage_bytes{component="filestore"} ${bytes}`)
} catch {
fileOk = false
errors.set('filestore', (errors.get('filestore') || 0) + 1)
}
// redis storage: RAM (used_memory) + disk (aof_current_size) ----------------
let redisOk = true
try {
const r = new Redis(REDIS_ADDR, {
maxRetriesPerRequest: 1,
connectTimeout: 5000,
lazyConnect: true,
})
try {
const usedMemory = parseRedisInfoInt(await r.info('memory'), 'used_memory')
let aof = 0
try {
aof = parseRedisInfoInt(await r.info('persistence'), 'aof_current_size')
} catch {
/* aof may be disabled — 0 is the true value */
}
out.push(
`overleaf_instance_storage_bytes{component="redis_ram"} ${usedMemory}`,
`overleaf_instance_storage_bytes{component="redis_disk"} ${aof}`,
)
} finally {
try {
r.disconnect()
} catch {
/* noop */
}
}
} catch {
redisOk = false
errors.set('redis', (errors.get('redis') || 0) + 1)
}
const up = mongoOk && fileOk && redisOk ? 1 : 0
if (up === 1) lastSuccessSeconds = (Date.now() / 1000) | 0
out.push(
'# HELP overleaf_instance_sensor_up 1 when the last scrape computed all probes successfully.',
'# TYPE overleaf_instance_sensor_up gauge',
`overleaf_instance_sensor_up ${up}`,
'# HELP overleaf_instance_sensor_last_success_timestamp_seconds Unix time of the last fully successful scrape.',
'# TYPE overleaf_instance_sensor_last_success_timestamp_seconds gauge',
`overleaf_instance_sensor_last_success_timestamp_seconds ${lastSuccessSeconds}`,
'# HELP overleaf_instance_sensor_errors_total Failed probe count since start (name=mongo|filestore|redis).',
'# TYPE overleaf_instance_sensor_errors_total counter',
)
for (const [name, n] of errors) {
out.push(`overleaf_instance_sensor_errors_total{name="${name}"} ${n}`)
}
return `${out.join('\n')}\n`
}
const server = http.createServer(async (req, res) => {
if (req.url !== '/metrics') {
res.writeHead(404, { 'content-type': 'text/plain' })
res.end('not found\n')
return
}
const t0 = performance.now()
const body = await collect()
const dt = (performance.now() - t0) / 1000
res.writeHead(200, {
'content-type': 'text/plain; version=0.0.4; charset=utf-8',
'content-length': Buffer.byteLength(body),
})
res.end(body)
})
server.listen(PORT, '0.0.0.0', () => {
console.log(`overleaf-instance-sensor listening on :${PORT}`)
})
+2
View File
@@ -0,0 +1,2 @@
docker compose logs -f grafana
+2
View File
@@ -0,0 +1,2 @@
docker compose logs -f instance-sensor
+2
View File
@@ -0,0 +1,2 @@
docker compose logs -f mongo-probe
@@ -0,0 +1,2 @@
docker compose logs -f mongodb-exporter-probe
+2
View File
@@ -0,0 +1,2 @@
docker compose logs -f mongodb-exporter
+2
View File
@@ -0,0 +1,2 @@
docker compose logs -f node-exporter
+2
View File
@@ -0,0 +1,2 @@
docker compose logs -f prometheus
@@ -0,0 +1,2 @@
docker compose logs -f redis-exporter-probe
+2
View File
@@ -0,0 +1,2 @@
docker compose logs -f redis-exporter
@@ -0,0 +1,26 @@
# mongo-probe — protocol-level liveness check of the stack Mongo.
# Reuses the stack's own mongo image: its mongosh is the canonical client.
# healthy == the db answers a real ping (same pattern as the OlliTeX
# toolkit mongo-probe).
services:
mongo-probe:
image: mongo:${MONGO_VERSION:-9.0}
container_name: mongo-probe
hostname: mongo-probe
restart: unless-stopped
command: ["sh", "-c", "sleep infinity"]
environment:
MONGO_HOSTNAME: ${MONGO_HOSTNAME:-mongo}
MONGO_PORT: ${MONGO_PORT:-27017}
healthcheck:
test: ["CMD-SHELL", "mongosh --quiet --host ${MONGO_HOSTNAME} --port ${MONGO_PORT} --eval 'db.adminCommand({ping:1})' >/dev/null 2>&1 || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
networks:
- overleaf-network
networks:
overleaf-network:
external: true
@@ -0,0 +1,22 @@
# mongodb-exporter-probe — HTTP serving proof for the mongodb exporter.
# The exporter image has no shell, so its /metrics reachability is checked
# here (busybox spider). healthy == /metrics answers.
services:
mongodb-exporter-probe:
image: ${PROBE_IMAGE:-busybox:1.36}
container_name: mongodb-exporter-probe
hostname: mongodb-exporter-probe
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --timeout=5 http://mongodb-exporter:9216/metrics >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
entrypoint: ["sh", "-c", "sleep infinity"]
networks:
- overleaf-network
networks:
overleaf-network:
external: true
@@ -0,0 +1,37 @@
# mongodb-exporter — percona/mongodb_exporter for the stack Mongo.
# NOTE (verified 2026-10 against this stack's mongo:9.0 replset "overleaf"):
# percona 0.43 ships the rich collectors OFF by default (default run answers
# only mongodb_up + go/process self metrics). The --collector.* toggles are
# therefore part of the supported surface here — keep them.
services:
mongodb-exporter:
image: ${MONGODB_EXPORTER_IMAGE:-percona/mongodb_exporter:0.43.0}
container_name: mongodb-exporter
hostname: mongodb-exporter
restart: unless-stopped
command:
- "--mongodb.uri=mongodb://${MONGO_HOSTNAME:-mongo}:${MONGO_PORT:-27017}/${MONGODB_DATABASE:-sharelatex}"
# verified metric families: mongodb_ss_* (connections, oplog time, ...),
# mongodb_dbstats_dataSize/storageSize/collections per database,
# mongodb_replicasetstatus_*, mongodb_fs_*
- "--collector.diagnosticdata"
- "--collector.dbstats"
- "--collector.dbstatsfreestorage"
- "--collector.replicasetstatus"
expose:
- "9216"
# scratch image: single binary, no shell — liveness = the binary still
# starts. The HTTP SERVING proof lives in mongodb-exporter-probe
# (same proven pattern as wakapi-probe).
healthcheck:
test: ["CMD", "/mongodb_exporter", "--version"]
interval: 10s
timeout: 5s
retries: 6
start_period: 15s
networks:
- overleaf-network
networks:
overleaf-network:
external: true
@@ -66,6 +66,26 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1; proxy_http_version 1.1;
} }
# Bundled Grafana (monitoring UI) — same-origin under /grafana/.
# Unlike /wakapi the prefix is NOT stripped: grafana runs with
# GF_SERVER_SERVE_FROM_SUB_PATH=true + GF_SERVER_ROOT_URL ending in
# /grafana/, so it serves its routes at /grafana/... itself; the raw
# :3000 port is not published to the host (grafana compose).
location = /grafana {
return 301 /grafana/;
}
location /grafana/ {
proxy_pass http://grafana:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 5m;
proxy_send_timeout 5m;
}
} }
} }
@@ -0,0 +1,29 @@
# node-exporter — host-level signals (CPU, memory, filesystem, ...).
# Reads the HOST's /proc, / and /sys read-only into the container
# (same flags as the OlliTeX toolkit node-exporter).
services:
node-exporter:
image: ${NODE_EXPORTER_IMAGE:-prom/node-exporter:v1.9.1}
container_name: node-exporter
hostname: node-exporter
restart: unless-stopped
command:
- "--path.procfs=/host/proc"
- "--path.rootfs=/host"
- "--path.sysfs=/host/sys"
ports:
- "127.0.0.1:${NODE_EXPORTER_PORT:-9100}:9100" # host admin only (SSH tunnel)
volumes:
- /:/host:ro
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:9100/metrics"]
start_period: 15s
interval: 30s
retries: 5
timeout: 5s
networks:
- overleaf-network
networks:
overleaf-network:
external: true
@@ -0,0 +1 @@
prometheus-data
@@ -0,0 +1,34 @@
# prometheus — Prometheus for the overleaf-cep stack.
# Static scrape config (prometheus.yml, mounted ro) — all targets live on
# the shared `overleaf-network`, so no host port publishing is needed for
# scraping; the 127.0.0.1:9090 binding is for local admin/SSH-tunnel only.
# Retention 15d (reference: OlliTeX toolkit compose).
services:
prometheus:
image: ${PROMETHEUS_IMAGE:-prom/prometheus:v2.53.5}
container_name: prometheus
hostname: prometheus
restart: unless-stopped
entrypoint: ["/bin/prometheus"]
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=${PROMETHEUS_RETENTION:-15d}"
- "--web.enable-lifecycle"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus-data:/prometheus
ports:
- "${PROMETHEUS_LISTEN_IP:-127.0.0.1}:${PROMETHEUS_PORT:-9090}:9090"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:9090/-/healthy"]
start_period: 20s
interval: 30s
retries: 5
timeout: 5s
networks:
- overleaf-network
networks:
overleaf-network:
external: true
@@ -0,0 +1,67 @@
# overleaf-cep — Prometheus scrape config
# Modeled on the OlliTeX toolkit compose (reference: toolkit/compose.yaml +
# build-images/prometheus/prometheus.yml), adapted to THIS stack's container
# names. All targets are inside `overleaf-network` (Prometheus scrapes from
# inside the docker network; the nginx edge does not forward /metrics).
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
stack: overleaf-cep
scrape_configs:
# --- the Overleaf app itself ------------------------------------------------
# The sharelatex image ships the @overleaf/metrics sensors on the web
# process (libraries/metrics): num_active_users, timer_http_request
# (summaries with quantiles), mongo_command_*, nodejs_* (event loop lag,
# GC, heap), security_http_auth_check_credentials, editor_join_project,
# tpds_add_doc, lock_mongoTransaction_*, ...
# Verified live: 0.0.0.0:3000 inside the container, samples carry
# app="web-api",host="overleaf". (Other in-image services e.g. compile
# :4000 are 127.0.0.1-bound and therefore not network-scrapable.)
- job_name: overleaf-web
metrics_path: /metrics
static_configs:
- targets: ["overleaf:3000"]
labels: { app: web-api }
# --- MongoDB (percona exporter) ----------------------------------------------
- job_name: overleaf-mongodb
metrics_path: /metrics
static_configs:
- targets: ["mongodb-exporter:9216"]
# --- Redis (oliver006 exporter) ------------------------------------------------
- job_name: overleaf-redis
metrics_path: /metrics
static_configs:
- targets: ["redis-exporter:9121"]
# --- Filestore (SeaweedFS master native prometheus, -metricsPort=9327) --------
- job_name: overleaf-filestore
metrics_path: /metrics
static_configs:
- targets: ["seaweedfs-master:9327"]
labels: { component: master }
# --- Instance stats sensor (PR yu-i-i/overleaf-cep#178, live as prometheus) ---
- job_name: overleaf-instance
metrics_path: /metrics
static_configs:
- targets: ["instance-sensor:9464"]
# --- Host-level signals ---------------------------------------------------------
- job_name: node
metrics_path: /metrics
static_configs:
- targets: ["node-exporter:9100"]
# --- The monitoring stack itself (self-up visibility in dashboards) -------------
- job_name: prometheus
static_configs:
- targets: ["prometheus:9090"]
- job_name: grafana
metrics_path: /metrics
static_configs:
- targets: ["grafana:3000"]
@@ -0,0 +1,22 @@
# redis-exporter-probe — HTTP serving proof for the redis exporter.
# Same sidecar contract as mongodb-exporter-probe:
# busybox spider on the exporter's in-container /metrics (9121).
services:
redis-exporter-probe:
image: ${PROBE_IMAGE:-busybox:1.36}
container_name: redis-exporter-probe
hostname: redis-exporter-probe
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --timeout=5 http://redis-exporter:9121/metrics >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
entrypoint: ["sh", "-c", "sleep infinity"]
networks:
- overleaf-network
networks:
overleaf-network:
external: true
@@ -0,0 +1,28 @@
# redis-exporter — oliver006/redis_exporter for the stack Redis.
# Rich out of the box (verified live: redis_up, redis_memory_used_bytes,
# redis_connected_clients, redis_commands_processed_total, redis_aof_current_size,
# redis_keyspace_*_total, ...).
services:
redis-exporter:
image: ${REDIS_EXPORTER_IMAGE:-oliver006/redis_exporter:v1.58.0-alpine}
container_name: redis-exporter
hostname: redis-exporter
restart: unless-stopped
environment:
REDIS_ADDR: "redis://${REDIS_HOSTNAME:-redis}:${REDIS_PORT:-6379}"
expose:
- "9121"
# alpine edition has no wget/nc applets — liveness = the exporter binary
# itself still runs (image entrypoint is /redis_exporter).
healthcheck:
test: ["CMD", "/redis_exporter", "--version"]
interval: 10s
timeout: 5s
retries: 6
start_period: 15s
networks:
- overleaf-network
networks:
overleaf-network:
external: true
@@ -0,0 +1,2 @@
docker compose up -d grafana
@@ -0,0 +1,2 @@
docker compose up -d instance-sensor
@@ -0,0 +1,2 @@
docker compose up -d mongo-probe
@@ -0,0 +1,2 @@
docker compose up -d mongodb-exporter-probe
@@ -0,0 +1,2 @@
docker compose up -d mongodb-exporter
@@ -0,0 +1,2 @@
docker compose up -d node-exporter
@@ -0,0 +1,2 @@
docker compose up -d prometheus
@@ -0,0 +1,2 @@
docker compose up -d redis-exporter-probe
@@ -0,0 +1,2 @@
docker compose up -d redis-exporter