tutorial

Monitoring Plone with Vigilmon

Plone is battle-tested enterprise CMS built on Zope — here's how to monitor its application health, ZODB Data.fs, ZEO server, blob storage, Varnish cache, Volto frontend, and Python process memory with Vigilmon.

Plone is one of the most battle-tested content management systems in the world — it powers government portals, university websites, and enterprise intranets that have been running continuously since the early 2000s. Built on the Zope application server with its unique ZODB object database, Plone's architecture is unlike any modern CMS: there's no SQL database, content lives as Python objects in a file-based database (Data.fs), and scaling is achieved through ZEO server clusters and Varnish caching rather than read replicas. When you self-host Plone, you're responsible for monitoring a stack that requires specific operational knowledge: ZODB packing schedules, blob storage growth, ZEO client connections, and Varnish cache efficiency. Vigilmon gives you comprehensive external monitoring across every layer of the Plone stack so your enterprise CMS stays available and healthy.

What You'll Set Up

  • Plone application health monitor (HTTP uptime)
  • ZODB Data.fs health and growth monitoring
  • ZEO server connectivity check
  • Blob storage disk usage alert
  • Varnish cache hit rate monitoring
  • Volto frontend (Plone 6) health check
  • Python process memory leak detection via cron heartbeat
  • ZODB pack schedule verification
  • Alert channels with appropriate thresholds

Prerequisites

  • Plone running via buildout, Docker, or pip install
  • Plone accessible over HTTP (directly or via nginx/Apache reverse proxy)
  • Varnish configured as a caching reverse proxy (recommended for production)
  • ZEO server (if using clustered deployment)
  • A free Vigilmon account

Step 1: Monitor Plone Application Health

The Plone/Zope process is the core of your CMS. Monitor it directly to catch crashes, failed migrations, and startup errors.

  1. Log in to vigilmon.online and click Add Monitor.
  2. Set Type to HTTP / HTTPS.
  3. URL: https://plone.yourdomain.com (or http://your-server-ip:8080/Plone for direct Zope access).
  4. Set Check interval to 1 minute.
  5. Set Expected HTTP status to 200.
  6. Under Keyword check, enter Plone to verify the CMS is actually serving content, not just that nginx responds.
  7. Enable Monitor SSL certificate and set expiry alert to 21 days.
  8. Click Save.

For a more targeted health check, monitor Plone's @@ok view which returns a simple 200 without rendering the full page:

https://plone.yourdomain.com/@@ok

This endpoint is lighter than a full page render and avoids caching complications.


Step 2: Monitor the Plone API Endpoint (Plone REST API)

Modern Plone deployments expose a REST API used by the Volto frontend and integrations. A healthy API response confirms Plone's database connectivity and request routing are working.

  1. Click Add MonitorHTTP / HTTPS.
  2. URL: https://plone.yourdomain.com/++api++/@site
  3. Method: GET
  4. Add header: Accept: application/json
  5. Expected HTTP status: 200
  6. Under Keyword check, enter "@id" — the Plone REST API response for the site root includes this JSON key.
  7. Check interval: 2 minutes
  8. Click Save.

A 500 on this endpoint with TCP up indicates a ZODB connectivity or middleware error.


Step 3: Monitor ZODB Data.fs Health

Plone's object database is stored in Data.fs. Monitor its accessibility and size to detect disk pressure and growth that signals the need for a pack operation.

Create a health sidecar script:

#!/usr/bin/env python3
# plone_probe.py — run as a systemd service or Docker sidecar alongside Plone
from flask import Flask, jsonify
import os
import time

app = Flask(__name__)

DATA_FS_PATH = "/var/lib/plone/var/filestorage/Data.fs"  # adjust to your buildout path
BLOB_DIR = "/var/lib/plone/var/blobstorage"
DISK_WARN_PERCENT = 80

@app.route('/data-fs')
def data_fs():
    try:
        if not os.path.exists(DATA_FS_PATH):
            return jsonify({"status": "missing", "path": DATA_FS_PATH}), 503
        
        stat = os.stat(DATA_FS_PATH)
        size_gb = stat.st_size / (1024 ** 3)
        age_hours = (time.time() - stat.st_mtime) / 3600
        
        # Check disk usage of the partition containing Data.fs
        import shutil
        disk = shutil.disk_usage(os.path.dirname(DATA_FS_PATH))
        disk_percent = (disk.used / disk.total) * 100
        
        status = "ok"
        warnings = []
        
        if disk_percent > DISK_WARN_PERCENT:
            status = "disk_high"
            warnings.append(f"Disk at {disk_percent:.1f}%")
        
        if age_hours > 24:
            warnings.append(f"Data.fs not written in {age_hours:.1f}h — Zope may not be persisting")
        
        return jsonify({
            "status": status,
            "size_gb": round(size_gb, 2),
            "disk_percent": round(disk_percent, 1),
            "warnings": warnings
        }), 200 if status == "ok" else 503
    except Exception as e:
        return jsonify({"status": "error", "error": str(e)}), 503

Add a Vigilmon monitor:

  1. Click Add MonitorHTTP / HTTPS.
  2. URL: http://your-server-ip:8090/data-fs
  3. Check interval: 10 minutes
  4. Expected HTTP status: 200

Step 4: Monitor ZEO Server Connectivity

If you're running a clustered Plone deployment with ZEO, the ZEO server is the single point of database access for all Plone instances. A ZEO server failure causes every Plone client to lose database access simultaneously.

  1. Click Add MonitorTCP Port.
  2. Host: your ZEO server host (e.g., localhost or a dedicated ZEO server IP).
  3. Port: 8100 (default ZEO port).
  4. Check interval: 1 minute
  5. Click Save.

Add a secondary HTTP check that validates Plone clients are actually connected to ZEO (a TCP port being open doesn't mean Plone has successfully checked out a ZODB connection):

@app.route('/zeo-connectivity')
def zeo_connectivity():
    import socket
    ZEO_HOST = "localhost"
    ZEO_PORT = 8100
    
    try:
        sock = socket.create_connection((ZEO_HOST, ZEO_PORT), timeout=5)
        sock.close()
        return jsonify({"status": "ok", "host": ZEO_HOST, "port": ZEO_PORT}), 200
    except Exception as e:
        return jsonify({"status": "unreachable", "error": str(e)}), 503

Step 5: Monitor Blob Storage Disk Usage

Plone stores uploaded files and images in a separate blob directory (blobstorage). Large file uploads accumulate quickly. Running out of blob storage space causes upload failures that appear as mysterious server errors to editors.

@app.route('/blob-storage')
def blob_storage():
    try:
        import shutil
        usage = shutil.disk_usage(BLOB_DIR)
        percent = (usage.used / usage.total) * 100
        free_gb = usage.free / (1024 ** 3)
        
        # Also count blob directory size
        total_blob_gb = 0
        for dirpath, dirnames, filenames in os.walk(BLOB_DIR):
            for f in filenames:
                try:
                    total_blob_gb += os.path.getsize(os.path.join(dirpath, f))
                except OSError:
                    pass
        total_blob_gb = total_blob_gb / (1024 ** 3)
        
        if percent > DISK_WARN_PERCENT:
            return jsonify({
                "status": "high",
                "disk_percent": round(percent, 1),
                "free_gb": round(free_gb, 2),
                "blob_size_gb": round(total_blob_gb, 2)
            }), 503
        return jsonify({
            "status": "ok",
            "disk_percent": round(percent, 1),
            "free_gb": round(free_gb, 2),
            "blob_size_gb": round(total_blob_gb, 2)
        }), 200
    except Exception as e:
        return jsonify({"status": "error", "error": str(e)}), 503

Add a Vigilmon monitor:

  1. Click Add MonitorHTTP / HTTPS.
  2. URL: http://your-server-ip:8090/blob-storage
  3. Check interval: 30 minutes
  4. Expected HTTP status: 200

Step 6: Monitor Varnish Cache Hit Rate

Production Plone deployments rely heavily on Varnish to handle anonymous user traffic without hitting Zope for every request. A drop in cache hit rate means your Zope process is fielding traffic it shouldn't and will slow under load.

Varnish exposes statistics via varnishstat. Create a monitoring endpoint:

import subprocess
import json

VARNISH_HIT_RATE_MIN = 0.80  # 80% hit rate minimum

@app.route('/varnish')
def varnish():
    try:
        result = subprocess.run(
            ["varnishstat", "-1", "-j"],
            capture_output=True, text=True, timeout=10
        )
        stats = json.loads(result.stdout)
        
        cache_hits = stats.get("MAIN.cache_hit", {}).get("value", 0)
        cache_misses = stats.get("MAIN.cache_miss", {}).get("value", 0)
        total = cache_hits + cache_misses
        
        if total == 0:
            return jsonify({"status": "ok", "hit_rate": None, "note": "no traffic yet"}), 200
        
        hit_rate = cache_hits / total
        
        if hit_rate < VARNISH_HIT_RATE_MIN:
            return jsonify({
                "status": "low_hit_rate",
                "hit_rate": round(hit_rate, 4),
                "hits": cache_hits,
                "misses": cache_misses
            }), 503
        return jsonify({
            "status": "ok",
            "hit_rate": round(hit_rate, 4),
            "hits": cache_hits,
            "misses": cache_misses
        }), 200
    except Exception as e:
        return jsonify({"status": "error", "error": str(e)}), 503

Add a Vigilmon monitor with Check interval: 5 minutes.


Step 7: Monitor Volto Frontend Health (Plone 6)

If you're running Plone 6 with the Volto React frontend, the frontend is a separate Node.js service. Editors and public users on Plone 6 see Volto — if it goes down, the site appears broken even with Plone running perfectly.

  1. Click Add MonitorHTTP / HTTPS.
  2. URL: https://plone.yourdomain.com (Volto typically serves on the root, with the Plone REST API under /++api++).
  3. Check interval: 1 minute
  4. Expected HTTP status: 200
  5. Under Keyword check, enter __NEXT_DATA__ — the Next.js/Volto page includes this script tag in rendered HTML, confirming Volto rendered the page (not Plone Classic).
  6. Click Save.

Optionally, add a direct check on the Volto Node.js process port if it's exposed separately:

  1. Click Add MonitorTCP Port.
  2. Host: localhost
  3. Port: 3000 (default Volto dev port; production is typically proxied through nginx)

Step 8: Track ZODB Pack Schedule via Heartbeat

ZODB's Data.fs accumulates old object revisions over time (every write creates a new revision, old ones persist until a pack). Without regular packing, Data.fs grows indefinitely. Monitor your pack schedule by pinging Vigilmon after each successful pack:

  1. Click Add MonitorCron Heartbeat.
  2. Set Expected interval to 720 hours (30 days — standard pack schedule for most Plone sites).
  3. Copy the heartbeat URL: https://vigilmon.online/heartbeat/abc123.
  4. Add the heartbeat ping to your pack script or cron job:
#!/bin/bash
# /usr/local/bin/plone-pack.sh — run monthly via cron
set -e

# Run ZODB pack (adjust path to your buildout)
/srv/plone/bin/zodbpack /srv/plone/etc/zodb.conf --days=7

# Ping Vigilmon on success
curl -sf "https://vigilmon.online/heartbeat/abc123" || true

echo "Pack complete: $(date)"

Add to crontab:

0 2 1 * * /usr/local/bin/plone-pack.sh >> /var/log/plone-pack.log 2>&1

If no heartbeat arrives within 31 days, Vigilmon alerts that your pack schedule has missed — before Data.fs grows to fill your disk.


Step 9: Monitor Python Process Memory

Plone/Zope can develop memory leaks, particularly in long-running instances with many add-ons, large ZODB caches, or connection pool buildup. Monitor the Zope process RSS to catch gradual memory growth before it triggers the OOM killer.

import psutil

MEMORY_WARN_PERCENT = 90

@app.route('/memory')
def memory():
    try:
        zope_rss_mb = 0
        zope_pid = None
        for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'memory_info']):
            cmdline = ' '.join(proc.info.get('cmdline') or [])
            if 'plone' in cmdline.lower() or 'zope' in cmdline.lower() or 'instance' in cmdline.lower():
                rss = proc.info['memory_info'].rss / (1024 * 1024)
                if rss > zope_rss_mb:
                    zope_rss_mb = rss
                    zope_pid = proc.info['pid']
        
        vm = psutil.virtual_memory()
        system_percent = vm.percent
        
        status = "ok"
        if system_percent > MEMORY_WARN_PERCENT:
            status = "high"
        
        return jsonify({
            "status": status,
            "zope_rss_mb": round(zope_rss_mb, 1),
            "zope_pid": zope_pid,
            "system_percent": round(system_percent, 1)
        }), 200 if status == "ok" else 503
    except Exception as e:
        return jsonify({"status": "error", "error": str(e)}), 503

If you observe zope_rss_mb growing steadily over days, schedule a supervisorctl restart plone during a maintenance window before the OOM killer forces an uncontrolled restart.


Step 10: Configure Alerting

In Vigilmon, go to Alert Channels and configure your notification channels:

  • Email: CMS team, IT operations
  • Slack: #plone-ops or #site-reliability via webhook
  • PagerDuty: for government or enterprise deployments where availability is an SLA requirement

Recommended thresholds:

| Monitor | Recommended threshold | |---|---| | Plone HTTP health | Alert immediately on first failure | | Plone REST API | Alert after 2 consecutive failures (4 min) | | Data.fs disk usage | Alert at >80% | | ZEO server TCP | Alert immediately | | Blob storage disk | Alert at >80% | | Varnish hit rate | Alert when hit rate drops below 80% | | Volto frontend | Alert after 2 consecutive failures | | ZODB pack heartbeat | Alert if no pack in 31 days | | Python memory | Alert when system memory >90% |


Conclusion

A complete Vigilmon setup for Plone covers every layer of its unique enterprise architecture:

  • Application health — catches Zope crashes and startup failures
  • ZODB Data.fs — monitors file growth and disk pressure before corruption risk
  • ZEO server — detects single-point-of-failure database access loss in clustered deployments
  • Blob storage — prevents upload failures from silent disk exhaustion
  • Varnish cache — detects cache invalidation issues that overload your Zope process
  • Volto frontend — catches Node.js failures that break the Plone 6 user experience
  • Pack schedule — enforces ZODB housekeeping before Data.fs fills your disk
  • Memory monitoring — detects Zope memory leaks before the OOM killer strikes

With these monitors in place, your Plone CMS has the operational visibility it needs to run reliably for the years — and decades — your organization depends on it.

Get started with a free Vigilmon account.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →