A Nextcloud app that collects per-instance statistics (users, files, shares, system, Talk, Deck, Mail, Calendar, Activity, Forms, Contacts, Richdocuments) and serves them over a public REST API for external monitoring systems to pull on demand. Also ships an admin dashboard and a per-group personal dashboard.
Pull-only. Each Nextcloud install is autonomous: it collects locally, stores snapshots in appdata, and exposes them on request. There is no central instance and no push-forward.
Supported: Nextcloud 30-33, PHP 8.1+.
- A background
TimedJobruns on a configurable interval (5min / 15min / hourly / daily / weekly). - Each run loops over enabled collectors, gathers their metrics, and stores a JSON snapshot in Nextcloud appdata.
- External monitoring systems authenticate with a Bearer API key and pull the latest or a date-range of snapshots.
- Users in authorized groups can view a read-only personal dashboard inside Nextcloud.
git clone https://github.com/sara-nl/nextcloud-statistics.git stats_collector
cd stats_collector
npm install
npm run build
mv stats_collector /var/www/nextcloud/apps/
occ app:enable stats_collector(Not yet published; install from source.)
After enabling the app, run the interactive wizard:
occ stats_collector:setupOr configure non-interactively:
occ stats_collector:configure \
--cron-interval=hourly \
--instance-label="Production NC"
occ stats_collector:metrics --enable-all-collectors
occ stats_collector:collect # test it onceExternal monitoring systems need a Bearer token. Generate one in the admin UI:
-
Go to Admin settings -> Administration -> Stats Collector.
-
Open the API Keys tab.
-
Click Generate API key, give it a label, and copy the key shown once (it is hashed at rest and cannot be recovered).
-
Use it with any HTTP client:
curl -H "Authorization: Bearer YOUR_KEY" \ https://nc.example.com/index.php/apps/stats_collector/api/v1/snapshots/latest
Keys can be revoked from the same tab. Brute-force protection is enabled on the pull endpoints.
Everything that can be done in the UI is also exposed via occ and the public REST API. Below are recipes for the patterns that come up most.
# 1) Install
git -C /var/www/nextcloud/apps clone https://github.com/sara-nl/nextcloud-statistics.git stats_collector
cd /var/www/nextcloud/apps/stats_collector
npm ci && npm run build
chown -R www-data:www-data /var/www/nextcloud/apps/stats_collector
OCC="sudo -u www-data php /var/www/nextcloud/occ"
# 2) Enable + configure
$OCC app:enable stats_collector
$OCC stats_collector:configure \
--cron-interval=hourly \
--instance-label="$(hostname -f)" \
--retention-days=90
# 3) Pick metrics (see "Choosing metrics" below for the per-metric API)
$OCC stats_collector:metrics --enable-all-collectors
# 4) Sanity check
$OCC stats_collector:status
$OCC stats_collector:collect --preview | head -40API keys are stored encrypted at rest and the plaintext is returned exactly once. By default the server generates a fresh 64-char hex key (random_bytes(32)); pass --key= to register a key you already minted yourself (Vault, KMS, password manager). Either way --quiet-key writes only the plaintext to stdout for piping.
Pick any label that helps you identify the consumer (the monitoring system, dashboard, or person using it). The label is purely cosmetic — it does not gate anything.
OCC="sudo -u www-data php /var/www/nextcloud/occ"
# 1. Let the server mint a key (recommended — strong by construction)
NEW_KEY=$($OCC stats_collector:api-key create --label="your-label" --quiet-key)
echo "$NEW_KEY" | vault kv put secret/monitoring/nc-statscollector key=-
# 2. Or register your own pre-generated key (must be 32+ chars)
MY_KEY=$(openssl rand -hex 32)
$OCC stats_collector:api-key create --label="your-label" --key="$MY_KEY"
# Human-friendly output (full record, key shown once)
$OCC stats_collector:api-key create --label="your-label"
# List existing keys (only the prefix is stored unencrypted)
$OCC stats_collector:api-key list
# Revoke by label or by id
$OCC stats_collector:api-key revoke --label="your-label"
$OCC stats_collector:api-key revoke --id="key_abc123..."Labels must be unique. create exits non-zero if the label already exists rather than silently issuing a duplicate — pair it with revoke --label=... first if you want to rotate.
Three granularity levels, all non-interactive:
OCC="sudo -u www-data php /var/www/nextcloud/occ"
# All available collectors, all of their metrics (broadest)
$OCC stats_collector:metrics --enable-all-collectors
# One collector, all of its metrics
$OCC stats_collector:metrics users --enable-all
$OCC stats_collector:metrics talk --disable-all
# Specific metrics within a collector (comma-separated)
$OCC stats_collector:metrics users --enable=total_users,active_24h,active_7d
$OCC stats_collector:metrics files --enable=total_files,total_storage_bytes
$OCC stats_collector:metrics shares --disable=federated_sharesDiscover available metric ids per collector:
$OCC stats_collector:metrics # list all collectors + how many active
$OCC stats_collector:metrics users --list # list metric ids for the 'users' collector--enable is additive (existing enabled metrics stay enabled). --disable removes ids from the active set. Unknown ids fail with a clear error and a hint to run --list.
A typical Ansible task to pin an exact metric set across a fleet:
- name: Stats Collector — minimal Prometheus set
command: >
php /var/www/nextcloud/occ stats_collector:metrics {{ item.collector }}
--disable-all
become_user: www-data
loop:
- { collector: users }
- { collector: files }
- { collector: system }
changed_when: false
- name: Stats Collector — enable picked metrics
command: >
php /var/www/nextcloud/occ stats_collector:metrics {{ item.collector }}
--enable={{ item.metrics | join(',') }}
become_user: www-data
loop:
- { collector: users, metrics: [total_users, active_24h, active_7d] }
- { collector: files, metrics: [total_files, total_storage_bytes] }
- { collector: system, metrics: [nc_version, php_version, db_size_bytes] }
changed_when: trueThis pattern (disable-all then enable=<exact ids>) is the safest way to keep config reproducible: it converges to the declared set regardless of prior state.
The personal dashboard (see Two surfaces) is gated by group membership. Admins always see it; everyone else needs to be in one of the allowed groups. Manage the allowlist via Admin settings -> Stats Collector -> Access or non-interactively via occ:
OCC="sudo -u www-data php /var/www/nextcloud/occ"
# Show current allowlist + per-group member counts
$OCC stats_collector:groups list
# Replace the whole allowlist (recommended for declarative config)
$OCC stats_collector:groups set admin monitoring
# Incrementally add or remove
$OCC stats_collector:groups add data-team
$OCC stats_collector:groups remove monitoring
# Wipe the allowlist (only admins will see the dashboard)
$OCC stats_collector:groups clearGroup ids are space- or comma-separated. Unknown ids are rejected by default so typos fail loudly; pass --skip-validation to seed an id before the group exists. list flags allowed-but-deleted groups with missing so you notice when a group disappears under you.
Declarative Ansible task — set converges to the desired list regardless of prior state:
- name: Stats Collector — pin allowed groups
command: >
php /var/www/nextcloud/occ stats_collector:groups set
{{ statscollector_allowed_groups | join(' ') }}
become_user: www-data
changed_when: false- name: Enable Stats Collector
command: php /var/www/nextcloud/occ app:enable stats_collector
become_user: www-data
changed_when: false
- name: Configure Stats Collector
command: >
php /var/www/nextcloud/occ stats_collector:configure
--cron-interval={{ statscollector_cron_interval | default('hourly') }}
--instance-label="{{ inventory_hostname }}"
--retention-days={{ statscollector_retention_days | default(90) }}
become_user: www-data
changed_when: false
- name: Mint API key
command: >
php /var/www/nextcloud/occ stats_collector:api-key create
--label="{{ statscollector_api_key_label | default('your-label') }}"
--quiet-key
become_user: www-data
register: sc_apikey
no_log: true
changed_when: true
failed_when: sc_apikey.rc != 0 and "already exists" not in sc_apikey.stderr
- name: Push API key to vault
community.hashi_vault.vault_write:
path: "secret/data/monitoring/nc-statscollector/{{ inventory_hostname }}"
data:
data:
key: "{{ sc_apikey.stdout }}"
no_log: true
when: sc_apikey.stdout | length > 0Pass statscollector_api_key from ansible-vault or a HashiCorp Vault lookup.
If your Nextcloud runs in k8s with a shared volume, run setup as a one-shot Job:
apiVersion: batch/v1
kind: Job
metadata:
name: stats-collector-bootstrap
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: occ
image: nextcloud:30-fpm
command: ["/bin/sh", "-c"]
args:
- |
set -e
php occ app:enable stats_collector
php occ stats_collector:configure --cron-interval=hourly --instance-label="$HOSTNAME"
php occ stats_collector:metrics --enable-all-collectors
# Register the key from the k8s Secret. Idempotent guard so re-runs
# of the Job don't fail on the duplicate-label check.
php occ stats_collector:api-key list | grep -q your-label \
|| php occ stats_collector:api-key create --label="your-label" --key="$SC_API_KEY"
env:
- name: SC_API_KEY
valueFrom:
secretKeyRef: { name: stats-collector-secrets, key: api_key }
volumeMounts:
- { name: nc-data, mountPath: /var/www/html }
securityContext:
runAsUser: 33 # www-data
volumes:
- { name: nc-data, persistentVolumeClaim: { claimName: nextcloud-data } }Hook the snapshot endpoint into whatever you already use:
# Healthcheck — fail if no snapshot in the last 2h
LAST=$(curl -fsS -H "Authorization: Bearer $SC_API_KEY" \
https://nc.example.com/index.php/apps/stats_collector/api/v1/snapshots/latest \
| jq -r '.timestamp')
AGE=$(( $(date -u +%s) - $(date -u -d "$LAST" +%s) ))
[ "$AGE" -lt 7200 ] || exit 1For Prometheus, a thin exporter that calls /snapshots/latest and re-emits the JSON as gauges works well; ship one per environment.
--cron-interval accepts: 5min, 15min, hourly, daily, weekly. Snapshots are written to Nextcloud appdata; retention is enforced before each collection.
All endpoints return plain JSON (no OCS wrapper). Authentication is Authorization: Bearer <key>.
| Method | Path | Description |
|---|---|---|
| GET | /index.php/apps/stats_collector/api/v1/snapshots |
List snapshot metadata. Query: from, to (ISO 8601), include_payload=true |
| GET | /index.php/apps/stats_collector/api/v1/snapshots/latest |
Latest complete snapshot |
| GET | /index.php/apps/stats_collector/api/v1/snapshots/{filename} |
Specific snapshot by filename |
Example:
curl -H "Authorization: Bearer YOUR_KEY" \
"https://nc.example.com/index.php/apps/stats_collector/api/v1/snapshots?from=2026-06-01T00:00:00Z&to=2026-06-15T00:00:00Z&include_payload=true"Snapshot retention defaults to 90 days; configurable via --retention on stats_collector:configure (0 = forever).
Stats Collector ships two distinct UIs. Keep them apart when granting access:
| Surface | Path | Who sees it | What it is |
|---|---|---|---|
| Admin settings | /settings/admin/stats_collector |
Admins only | Configuration: collectors, API keys, branding, allowed groups, retention. |
| Personal dashboard | /apps/stats_collector/ |
Admins + members of allowed_groups |
Read-only frontend: KPI cards, charts, sparklines, per-user preferences. |
When you grant a group access through stats_collector:groups, the members get a top-level "Stats Collector" entry in the Nextcloud navigation bar. Clicking it lands them on the personal dashboard — never on admin settings. They can view metrics and tweak their own layout (density, hidden sections, section order, pinned hero metrics, default spotlight metric), but they cannot change collectors, mint API keys, or touch anything else.
Users without access do not see the navigation icon at all.
| Command | Purpose |
|---|---|
stats_collector:setup |
Interactive setup wizard |
stats_collector:configure |
Non-interactive: set cron interval, instance label, retention days |
stats_collector:status |
Show current config + enabled collectors + snapshot info |
stats_collector:metrics |
Enable/disable metrics per collector (--enable-all, --enable=<ids>, --disable=<ids>) |
stats_collector:api-key |
Manage pull API keys (create, list, revoke; --key= to bring your own) |
stats_collector:groups |
Manage personal dashboard access (list, add, remove, set, clear) |
stats_collector:collect |
Manual run (--preview to inspect JSON, --no-store to skip persistence) |
stats_collector:reset |
Wipe all app config (--yes to skip prompt) |
| Collector | Required app | Examples |
|---|---|---|
| Users | core | Total, active (24h/7d/30d), disabled, per group |
| Files | core | Total files, storage bytes, mimetypes, created 24h |
| Shares | core | By type (user/group/link/email/federated/room) |
| System | core | NC + PHP version, DB type/size, installed apps |
| Talk | spreed | Rooms, messages, participants, calls |
| Deck | deck | Boards, cards, overdue, created 7d |
| Accounts, messages, mailboxes | ||
| Calendar | calendar | Calendars, events, upcoming |
| Activity | activity | Total, today, by type, active users |
| Forms | forms | Forms, submissions, active, shared |
| Contacts | contacts | Address books, contacts |
| Richdocuments | richdocuments | Documents, conversions |
Collectors only run when their required app is installed. Add a new one by implementing OCA\StatsCollector\Collector\ICollector and registering it in CollectorRegistry.
npm install
npm run dev # development build
npm run watch # watch mode
npm run build # production build- Frontend: Vue 3 Options API, single-file components, ApexCharts for charts, inline Lucide-style SVG icons (no font icon dependencies).
- PHP namespace:
OCA\StatsCollector(PSR-4 mapped tolib/). - No PHP tests, no composer install needed (Nextcloud's autoloader handles it).
- API keys are stored encrypted at rest via
OCP\Security\ICryptoand never returned in plaintext after creation. - The public pull endpoints have brute-force protection (
#[BruteForceProtection]) and strict input validation (ctype_digit on ids, ISO 8601 on dates, strict filename regex). - Sessions are closed on pull requests to keep them stateless.
- Personal dashboard access is gated by group membership (admin can always see it).
Report security issues via the contact in SECURITY.md.
AGPL-3.0